diff --git a/.gitignore b/.gitignore index 14e0bf6..5eb000f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ out/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ +**/broadcast/** + # Docs docs/ diff --git a/.gitmodules b/.gitmodules index b60817e..b3c9f6e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,8 +1,8 @@ [submodule "lib/openzeppelin-contracts"] - branch = v4.8.2 + branch = v5.0.1 path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std - branch = v1.5.2 + branch = v1.7.6 diff --git a/foundry.toml b/foundry.toml index a1baf7e..c0c4411 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,5 +5,6 @@ out = 'out' test = 'test' cache_path = 'cache' script = 'script' +solc_version = '0.8.23' # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/lib/forge-std b/lib/forge-std index 2b58ecb..ae570fe 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index d00acef..01ef448 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit d00acef4059807535af0bd0dd0ddf619747a044b +Subproject commit 01ef448981be9d20ca85f2faf6ebdf591ce409f3 diff --git a/management/.env.example b/management/.env.example new file mode 100644 index 0000000..abf88fd --- /dev/null +++ b/management/.env.example @@ -0,0 +1,14 @@ +# .env file +PRIVATE_KEY=your_private_key_here +BASE_RPC_URL=your_base_network_rpc_url +BASE_PATCHWORK_OWNER=base_patchwork_owner_address +BASE_PATCHWORK_ADDRESS=base_patchwork_address +BASE_CHAIN_ID=8453 +SEPOLIA_RPC_URL=your_sepolia_network_rpc_url +SEPOLIA_PATCHWORK_OWNER=sepolia_patchwork_owner_address +SEPOLIA_CHAIN_ID=11155111 +SEPOLIA_PATCHWORK_ADDRESS=0xeAB0ceC50d344c1256fB983fee6f87D6A040d329 +BASE_SEPOLIA_RPC_URL=https://base-sepolia.g.alchemy.com/v2/6p1q3ciiOzYUwMaNdysskDHdLLV56sGx +BASE_SEPOLIA_PATCHWORK_OWNER=0x435498Afe7E9b01f92D673fADA429fAeA08870aA +BASE_SEPOLIA_CHAIN_ID=11155111 +BASE_SEPOLIA_PATCHWORK_ADDRESS=0xeAB0ceC50d344c1256fB983fee6f87D6A040d329 diff --git a/management/README.md b/management/README.md new file mode 100644 index 0000000..f0031dc --- /dev/null +++ b/management/README.md @@ -0,0 +1,23 @@ +Create a .env file in the format of .env.example + +To deoploy on sepolia: +./deploy.sh sepolia + +To deploy on base: +./deploy.sh base + +Include --broadcast when you are ready to deploy + +To verify on ether scan: + +./verify.sh base + + or + + ./verify.sh sepolia + +manual verification: + +forge verify-contract 0x635BDfB811Ef377a759231Ac3A2746814A93a7B8 src/PatchworkProtocol.sol:PatchworkProtocol --optimizer-runs 200 --constructor-args "0x0000000000000000000000007239aec2fa59303ba68bece386be2a9ddc72e63b" --show-standard-json-input > etherscan.json +patch manually etherscan.json : "optimizer":{"enabled":true,"runs":100} -> "optimizer":{"enabled":true,"runs":100},"viaIR":true (or something of that sort) +upload json to etherscan manually \ No newline at end of file diff --git a/management/constructor-args.txt b/management/constructor-args.txt new file mode 100644 index 0000000..3ea0442 --- /dev/null +++ b/management/constructor-args.txt @@ -0,0 +1 @@ +0x7239aEc2fA59303BA68BEcE386BE2A9dDC72e63B \ No newline at end of file diff --git a/management/deploy.s.sol b/management/deploy.s.sol new file mode 100644 index 0000000..fea752c --- /dev/null +++ b/management/deploy.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Script } from "forge-std/Script.sol"; +import "forge-std/console.sol"; +import { PatchworkProtocol } from "../src/PatchworkProtocol.sol"; +import { PatchworkProtocolAssigner } from "../src/PatchworkProtocolAssigner.sol"; + +contract DeterministicPatchworkDeploy is Script { + + address internal constant _DETERMINISTIC_CREATE2_FACTORY = 0x7A0D94F55792C434d74a40883C6ed8545E406D12; + + function run() public { + address patchworkOwner = vm.envAddress("PATCHWORK_OWNER"); + vm.startBroadcast(); + + bytes memory creationCode = type(PatchworkProtocolAssigner).creationCode; + bytes memory creationBytecode = abi.encodePacked(creationCode, abi.encode(patchworkOwner)); + (bool success, bytes memory returnData) = _DETERMINISTIC_CREATE2_FACTORY.call(creationBytecode); + require(success, "Failed to deploy Assigner Module"); + address assignerAddress = address(uint160(bytes20(returnData))); + console.log("Deployed Assigner module contract at: ", assignerAddress); + + creationCode = type(PatchworkProtocol).creationCode; + creationBytecode = abi.encodePacked(creationCode, abi.encode(patchworkOwner), abi.encode(assignerAddress)); + (success, returnData) = _DETERMINISTIC_CREATE2_FACTORY.call(creationBytecode); + require(success, "Failed to deploy Patchwork"); + address patchworkAddress = address(uint160(bytes20(returnData))); + console.log("Deployed Patchwork contract at: ", patchworkAddress); + + vm.stopBroadcast(); + } +} diff --git a/management/deploy.sh b/management/deploy.sh new file mode 100755 index 0000000..bcff986 --- /dev/null +++ b/management/deploy.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# deploy.sh + +# Usage instructions +if [ $# -lt 1 ]; then + echo "Usage: $0 [--broadcast]" + echo "Supported networks: base, sepolia" + exit 1 +fi + +# Load the .env file +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +else + echo ".env file not found" + exit 1 +fi + +NETWORK=$1 +PATCHWORK_OWNER="" +RPC_URL="" + +case $NETWORK in + "base") + PATCHWORK_OWNER=$BASE_PATCHWORK_OWNER + RPC_URL=$BASE_RPC_URL + ;; + "sepolia") + PATCHWORK_OWNER=$SEPOLIA_PATCHWORK_OWNER + RPC_URL=$SEPOLIA_RPC_URL + ;; + "base-sepolia") + PATCHWORK_OWNER=$BASE_SEPOLIA_PATCHWORK_OWNER + RPC_URL=$BASE_SEPOLIA_RPC_URL + ;; + *) + echo "Network not supported" + exit 1 + ;; +esac + +# Check for broadcast flag +if [[ " $* " =~ " --broadcast " ]]; then + echo "Broadcasting is enabled" + forge_options="$forge_options --broadcast" +fi + +# Export the owner address as an environment variable +export PATCHWORK_OWNER + +# Execute the Solidity script with the environment variable +forge script $forge_options --optimize --optimizer-runs 200 ./deploy.s.sol:DeterministicPatchworkDeploy \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY diff --git a/management/encodeConstructorArgs.s.sol b/management/encodeConstructorArgs.s.sol new file mode 100644 index 0000000..f235e0a --- /dev/null +++ b/management/encodeConstructorArgs.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import { PatchworkProtocol } from "src/PatchworkProtocol.sol"; + +contract EncodeConstructorArgs is Script { + function run() public { + address patchworkOwner = 0x7239aEc2fA59303BA68BEcE386BE2A9dDC72e63B; + bytes memory encodedArgs = abi.encode(patchworkOwner); + console.log("Encoded Constructor Args:", vm.toString(encodedArgs)); + } +} diff --git a/management/verify.sh b/management/verify.sh new file mode 100755 index 0000000..134354c --- /dev/null +++ b/management/verify.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# verify.sh + +# Usage instructions +if [ $# -lt 1 ]; then + echo "Usage: $0 " + echo "Supported networks: base, sepolia" + exit 1 +fi + +NETWORK=$1 + +# Load the .env file +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +else + echo ".env file not found" + exit 1 +fi + +# Set the network-specific contract address and chain ID +CONTRACT_ADDRESS="" +ASSIGNER_CONTRACT_ADDRESS="" +CHAIN_ID="" + +case $NETWORK in + "base") + CONTRACT_ADDRESS=$BASE_PATCHWORK_ADDRESS + ASSIGNER_CONTRACT_ADDRESS=$BASE_PATCHWORK_ASSIGNER_ADDRESS + CHAIN_ID=$BASE_CHAIN_ID + ;; + "sepolia") + CONTRACT_ADDRESS=$SEPOLIA_PATCHWORK_ADDRESS + ASSIGNER_CONTRACT_ADDRESS=$SEPOLIA_PATCHWORK_ASSIGNER_ADDRESS + CHAIN_ID=$SEPOLIA_CHAIN_ID + ;; + *) + echo "Network not supported" + exit 1 + ;; +esac + + +COMPILER_VERSION="0.8.23+commit.f704f362" +OPTIMIZER_RUNS=200 + +forge verify-contract $ASSIGNER_CONTRACT_ADDRESS src/PatchworkProtocolAssigner.sol:PatchworkProtocolAssigner \ + --constructor-args "0x0000000000000000000000007239aec2fa59303ba68bece386be2a9ddc72e63b" \ + --chain-id $CHAIN_ID \ + --compiler-version $COMPILER_VERSION \ + --optimizer-runs $OPTIMIZER_RUNS \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + --watch + +CLEANED_ASSIGNER_CONTRACT_ADDRESS=$(echo "$ASSIGNER_CONTRACT_ADDRESS" | sed 's/^0x//') + +forge verify-contract $CONTRACT_ADDRESS src/PatchworkProtocol.sol:PatchworkProtocol \ + --constructor-args "0x0000000000000000000000007239aec2fa59303ba68bece386be2a9ddc72e63b000000000000000000000000$CLEANED_ASSIGNER_CONTRACT_ADDRESS"\ + --chain-id $CHAIN_ID \ + --compiler-version $COMPILER_VERSION \ + --optimizer-runs $OPTIMIZER_RUNS \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + --watch + diff --git a/remappings.txt b/remappings.txt index 69b1596..90b2e67 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,2 @@ @openzeppelin/=lib/openzeppelin-contracts/ -@patchwork/=src/ forge-std/=lib/forge-std/src/ diff --git a/src/Patchwork1155Patch.sol b/src/Patchwork1155Patch.sol new file mode 100644 index 0000000..8c71134 --- /dev/null +++ b/src/Patchwork1155Patch.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./Patchwork721.sol"; +import "./interfaces/IPatchwork1155Patch.sol"; + +/** +@title Patchwork1155Patch +@dev Base implementation of IPatchwork1155Patch +@dev It extends the functionalities of Patchwork721 and implements the IPatchwork1155Patch interface. +*/ +abstract contract Patchwork1155Patch is Patchwork721, IPatchwork1155Patch { + + /// @dev Mapping from token ID to the canonical address of the NFT that this patch is applied to. + mapping(uint256 => PatchTarget) internal _targetsById; + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchwork1155Patch).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @notice stores a patch + @param tokenId the tokenId of the patch + @param target the patch target + */ + function _storePatch(uint256 tokenId, PatchTarget memory target) internal virtual { + _targetsById[tokenId] = target; + } + + /** + @dev See {ERC721-_burn} + */ + function _burnPatch(uint256 tokenId) internal virtual { + PatchTarget storage target = _targetsById[tokenId]; + address originalAddress = target.addr; + uint256 originalTokenId = target.tokenId; + address account = target.account; + IPatchworkProtocol(_manager).patchBurned1155(originalAddress, originalTokenId, account, address(this)); + delete _targetsById[tokenId]; + _burn(tokenId); + } +} + +/** +@title PatchworkReversible1155Patch +@dev Patchwork1155Patch with reverse lookup function +*/ +abstract contract PatchworkReversible1155Patch is Patchwork1155Patch, IPatchworkReversible1155Patch { + /// @dev Mapping of hash of original address + token ID + account for reverse lookups + mapping(bytes32 => uint256) internal _idsByTargetHash; // hash of patched addr+tokenid+account to tokenId + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkReversible1155Patch).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @dev See {IPatchwork1155Patch-getTokenIdByTarget} + */ + function getTokenIdByTarget(PatchTarget memory target) public view virtual returns (uint256 tokenId) { + return _idsByTargetHash[keccak256(abi.encode(target))]; + } + + /** + @notice stores a patch + @param tokenId the tokenId of the patch + @param target the patch target + */ + function _storePatch(uint256 tokenId, PatchTarget memory target) internal virtual override { + _targetsById[tokenId] = target; + _idsByTargetHash[keccak256(abi.encode(target))] = tokenId; + } + + /** + @dev See {ERC721-_burn} + */ + function _burnPatch(uint256 tokenId) internal virtual override { + PatchTarget storage target = _targetsById[tokenId]; + delete _idsByTargetHash[keccak256(abi.encode(target))]; + super._burnPatch(tokenId); + } +} \ No newline at end of file diff --git a/src/Patchwork721.sol b/src/Patchwork721.sol new file mode 100644 index 0000000..a3b1b2a --- /dev/null +++ b/src/Patchwork721.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./interfaces/IPatchwork721.sol"; +import "./interfaces/IERC4906.sol"; +import "./interfaces/IPatchworkProtocol.sol"; + +/** +@title Patchwork721 Abstract Contract +@dev This abstract contract defines the core functionalities for the Patchwork721. + It inherits from the standard ERC721, as well as the IPatchwork721 and IERC4906 interfaces. +*/ +abstract contract Patchwork721 is ERC721, IPatchwork721, IERC4906, Ownable { + + /// @dev The scope name of this 721. + string internal _scopeName; + + /// @dev Our manager (PatchworkProtocol). + address internal immutable _manager; + + /// @dev A mapping to keep track of permissions for each address. + mapping(address => uint256) internal _permissionsAllow; + + /// @dev A mapping for storing metadata associated with each token ID. + mapping(uint256 => uint256[]) internal _metadataStorage; + + /// @dev A mapping for storing freeze nonces of each token ID. + mapping(uint256 => uint256) internal _freezeNonces; + + /// @dev A mapping indicating whether a specific token ID is frozen. + mapping(uint256 => bool) internal _freezes; + + /// @dev A mapping indicating whether a specific token ID is locked. + mapping(uint256 => bool) internal _locks; + + /** + @notice Creates a new instance of the Patchwork721 contract with the provided parameters. + @dev msg.sender will be initial owner + @param scopeName_ The scope name. + @param name_ The ERC-721 name. + @param symbol_ The ERC-721 symbol. + @param manager_ The address that will be set as the manager (PatchworkProtocol). + @param owner_ The address that will be set as the owner + */ + constructor( + string memory scopeName_, + string memory name_, + string memory symbol_, + address manager_, + address owner_ + ) ERC721(name_, symbol_) Ownable(owner_) { + _scopeName = scopeName_; + _manager = manager_; + } + + /** + @dev See {IPatchwork721-getScopeName} + */ + function getScopeName() public view virtual returns (string memory) { + return _scopeName; + } + + /** + @dev See {IPatchwork721-storePackedMetadataSlot} + */ + function storePackedMetadataSlot(uint256 tokenId, uint256 slot, uint256 data) public virtual mustHaveTokenWriteAuth(tokenId) { + _metadataStorage[tokenId][slot] = data; + } + + /** + @dev See {IPatchwork721-storePackedMetadata} + */ + function storePackedMetadata(uint256 tokenId, uint256[] memory data) public virtual mustHaveTokenWriteAuth(tokenId) { + _metadataStorage[tokenId] = data; + } + + /** + @dev See {IPatchwork721-loadPackedMetadataSlot} + */ + function loadPackedMetadataSlot(uint256 tokenId, uint256 slot) public virtual view returns (uint256) { + return _metadataStorage[tokenId][slot]; + } + + /** + @dev See {IPatchwork721-loadPackedMetadata} + */ + function loadPackedMetadata(uint256 tokenId) public virtual view returns (uint256[] memory) { + return _metadataStorage[tokenId]; + } + + // Does msg.sender have permission to write to our top level storage? + function _checkWriteAuth() internal virtual view returns (bool allow) { + return (msg.sender == owner()); + } + + // Does msg.sender have permission to write to this token's data? + function _checkTokenWriteAuth(uint256 /*tokenId*/) internal virtual view returns (bool allow) { + return (msg.sender == owner() || msg.sender == _manager); + } + + /** + @dev See {IPatchwork721-setPermissions} + */ + function setPermissions(address to, uint256 permissions) public virtual mustHaveWriteAuth { + _permissionsAllow[to] = permissions; + emit PermissionChange(to, permissions); + } + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override(ERC721, IERC165) returns (bool) { + return interfaceID == type(IPatchwork721).interfaceId || + interfaceID == type(IERC5192).interfaceId || + interfaceID == type(IERC4906).interfaceId || + interfaceID == type(IPatchworkScoped).interfaceId || + ERC721.supportsInterface(interfaceID); + } + + /** + @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override(ERC721, IERC721) { + IPatchworkProtocol(_manager).applyTransfer(from, to, tokenId); + super.transferFrom(from, to, tokenId); + } + + /** + @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override(ERC721, IERC721) { + IPatchworkProtocol(_manager).applyTransfer(from, to, tokenId); + super.safeTransferFrom(from, to, tokenId, data); + } + + /** + @notice transfers a token with a known freeze nonce + @dev reverts if the token is not frozen or if the current freeze nonce does not match the provided nonce + @dev See {IERC721-transferFrom}. + */ + function transferFromWithFreezeNonce(address from, address to, uint256 tokenId, uint256 nonce) public mustBeFrozenWithNonce(tokenId, nonce) { + transferFrom(from, to, tokenId); + } + + /** + @notice transfers a token with a known freeze nonce + @dev reverts if the token is not frozen or if the current freeze nonce does not match the provided nonce + @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFromWithFreezeNonce(address from, address to, uint256 tokenId, uint256 nonce) public mustBeFrozenWithNonce(tokenId, nonce) { + safeTransferFrom(from, to, tokenId); + } + + /** + @notice transfers a token with a known freeze nonce + @dev reverts if the token is not frozen or if the current freeze nonce does not match the provided nonce + @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFromWithFreezeNonce(address from, address to, uint256 tokenId, bytes memory data, uint256 nonce) public mustBeFrozenWithNonce(tokenId, nonce) { + safeTransferFrom(from, to, tokenId, data); + } + + /** + @dev See {IPatchwork721-getFreezeNonce} + */ + function getFreezeNonce(uint256 tokenId) public view virtual returns (uint256 nonce) { + return _freezeNonces[tokenId]; + } + + /** + @dev See {IPatchwork721-setFrozen} + */ + function setFrozen(uint256 tokenId, bool frozen_) public virtual mustBeTokenOwner(tokenId) { + bool _frozen = _freezes[tokenId]; + if (_frozen != frozen_) { + if (frozen_) { + _freezes[tokenId] = true; + emit Frozen(tokenId); + } else { + _freezeNonces[tokenId]++; + _freezes[tokenId] = false; + emit Thawed(tokenId); + } + } + } + + /** + @dev See {IPatchwork721-frozen} + */ + function frozen(uint256 tokenId) public view virtual returns (bool) { + return _freezes[tokenId]; + } + + /** + @dev See {IPatchwork721-locked} + */ + function locked(uint256 tokenId) public view virtual returns (bool) { + return _locks[tokenId]; + } + + /** + @dev See {IPatchwork721-setLocked} + */ + function setLocked(uint256 tokenId, bool locked_) public virtual mustBeTokenOwner(tokenId) { + bool _locked = _locks[tokenId]; + if (_locked != locked_) { + _locks[tokenId] = locked_; + if (locked_) { + emit Locked(tokenId); + } else { + emit Unlocked(tokenId); + } + } + } + + modifier mustHaveWriteAuth { + if (!_checkWriteAuth()) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + _; + } + + modifier mustHaveTokenWriteAuth(uint256 tokenId) { + if (!_checkTokenWriteAuth(tokenId)) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + _; + } + + modifier mustBeTokenOwner(uint256 tokenId) { + if (msg.sender != ownerOf(tokenId)) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + _; + } + + modifier mustBeFrozenWithNonce(uint256 tokenId, uint256 nonce) { + if (!frozen(tokenId)) { + revert IPatchworkProtocol.NotFrozen(address(this), tokenId); + } + if (getFreezeNonce(tokenId) != nonce) { + revert IPatchworkProtocol.IncorrectNonce(address(this), tokenId, nonce); + } + _; + } + + modifier mustBeManager() { + if (msg.sender != _manager) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + _; + } +} \ No newline at end of file diff --git a/src/PatchworkAccountPatch.sol b/src/PatchworkAccountPatch.sol new file mode 100644 index 0000000..a63a0c9 --- /dev/null +++ b/src/PatchworkAccountPatch.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./interfaces/IPatchworkAccountPatch.sol"; +import "./interfaces/IPatchworkProtocol.sol"; +import "./Patchwork721.sol"; + +/** +@title PatchworkAccountPatch +@dev Base implementation of IPatchworkAccountPatch +@dev It extends the functionalities of Patchwork721 and implements the IPatchworkAccountPatch interface. +*/ +abstract contract PatchworkAccountPatch is Patchwork721, IPatchworkAccountPatch { + + /// @dev Mapping from token ID to the address of the NFT that this patch is applied to. + mapping(uint256 => address) internal _targetsById; + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkAccountPatch).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @notice stores a patch + @param tokenId the tokenId of the patch + @param target the account we are patching + */ + function _storePatch(uint256 tokenId, address target) internal virtual { + // PatchworkProtocol handles uniqueness assertion + _targetsById[tokenId] = target; + } + + /** + @dev See {ERC721-_burn} + */ + function _burnPatch(uint256 tokenId) internal virtual { + address originalAddress = _targetsById[tokenId]; + IPatchworkProtocol(_manager).patchBurnedAccount(originalAddress, address(this)); + delete _targetsById[tokenId]; + super._burn(tokenId); + } + +} + +/** +@title PatchworkReversibleAccountPatch +@dev PatchworkAccountPatch with reverse lookup function +*/ +abstract contract PatchworkReversibleAccountPatch is PatchworkAccountPatch, IPatchworkReversibleAccountPatch { + /// @dev Mapping of original address to token Ids for reverse lookups + mapping(address => uint256) internal _idsByTarget; + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkReversibleAccountPatch).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @dev See {IPatchworkAccountPatch-getTokenIdByTarget} + */ + function getTokenIdByTarget(address target) public view virtual returns (uint256 tokenId) { + return _idsByTarget[target]; + } + + /** + @notice stores a patch + @param tokenId the tokenId of the patch + @param target the account we are patching + */ + function _storePatch(uint256 tokenId, address target) internal virtual override { + // PatchworkProtocol handles uniqueness assertion + _targetsById[tokenId] = target; + _idsByTarget[target] = tokenId; + } + + /** + @dev See {ERC721-_burn} + */ + function _burnPatch(uint256 tokenId) internal virtual override { + address target = _targetsById[tokenId]; + delete _idsByTarget[target]; + super._burnPatch(tokenId); + } +} \ No newline at end of file diff --git a/src/PatchworkFragmentMulti.sol b/src/PatchworkFragmentMulti.sol new file mode 100644 index 0000000..eff9bd6 --- /dev/null +++ b/src/PatchworkFragmentMulti.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./Patchwork721.sol"; +import "./interfaces/IPatchworkMultiAssignable.sol"; + +/** +@title PatchworkFragmentMulti +@dev base implementation of a Multi-relation Fragment is IPatchworkAssignable +*/ +abstract contract PatchworkFragmentMulti is Patchwork721, IPatchworkMultiAssignable { + + struct AssignmentStorage { + mapping(bytes32 => uint256) index; + Assignment[] assignments; + } + + // Only presence-checking is available here + + /// A mapping from token IDs in this contract to their assignments. + mapping(uint256 => AssignmentStorage) internal _assignmentStorage; + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkAssignable).interfaceId || + interfaceID == type(IPatchworkMultiAssignable).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @dev See {IPatchworkAssignableNFT-assign} + */ + function assign(uint256 ourTokenId, address to, uint256 tokenId) public virtual mustHaveTokenWriteAuth(ourTokenId) { + AssignmentStorage storage store = _assignmentStorage[ourTokenId]; + (bool present,, bytes32 targetHash) = _assignmentIndexOf(store, to, tokenId); + if (present) { + revert IPatchworkProtocol.FragmentAlreadyAssigned(address(this), ourTokenId); + } + Assignment[] storage assignments = store.assignments; + uint256 idx = assignments.length; + assignments.push(Assignment(to, tokenId)); + store.index[targetHash] = idx; + } + + /** + @notice Unassigns a token + @param ourTokenId ID of our token + */ + function unassign(uint256 ourTokenId, address target, uint256 targetTokenId) public virtual mustHaveTokenWriteAuth(ourTokenId) { + AssignmentStorage storage store = _assignmentStorage[ourTokenId]; + (bool present, uint256 index, bytes32 targetHash) = _assignmentIndexOf(store, target, targetTokenId); + if (!present) { + revert IPatchworkProtocol.FragmentNotAssigned(address(this), ourTokenId); + } + Assignment[] storage assignments = store.assignments; + if (assignments.length > 1) { + // move the last element of the array into this index + Assignment storage a = assignments[assignments.length-1]; + assignments[index] = a; + store.index[keccak256(abi.encodePacked(a.tokenAddr, a.tokenId))] = index; + } + // shorten the array by 1 + assignments.pop(); + // delete the index + delete store.index[targetHash]; + } + + function isAssignedTo(uint256 ourTokenId, address target, uint256 targetTokenId) public view virtual returns (bool) { + (bool present,,) = _assignmentIndexOf(_assignmentStorage[ourTokenId], target, targetTokenId); + return present; + } + + function _assignmentIndexOf(AssignmentStorage storage store, address target, uint256 targetTokenId) internal view returns (bool present, uint256 index, bytes32 targetHash) { + targetHash = keccak256(abi.encodePacked(target, targetTokenId)); + uint256 storageIndex = store.index[targetHash]; + Assignment[] storage assignments = store.assignments; + if (storageIndex == 0) { + // Either the first element or does not exist + if (assignments.length > 0) { + // there is an assignment of some kind - need to check if it's this one + if (assignments[0].tokenAddr == target && assignments[0].tokenId == targetTokenId) { + return (true, 0, targetHash); + } + } + } else { + // There is definitely an index to this. + return (true, storageIndex, targetHash); + } + return (false, 0, targetHash); + } + + + /** + @dev See {IPatchwork721-getAssignmentCount} + */ + function getAssignmentCount(uint256 tokenId) public view returns (uint256) { + return _assignmentStorage[tokenId].assignments.length; + } + + /** + @dev See {IPatchwork721-getAssignments} + */ + function getAssignments(uint256 tokenId, uint256 offset, uint256 count) external view returns (Assignment[] memory) { + AssignmentStorage storage store = _assignmentStorage[tokenId]; + Assignment[] storage assignments = store.assignments; + if (offset >= assignments.length) { + return new Assignment[](0); + } + // Determine the actual count of assignments to return + uint256 retCount = count; + if (offset + count > assignments.length) { + retCount = assignments.length - offset; + } + // Fetch assignments + Assignment[] memory page = new Assignment[](retCount); + for (uint256 i = 0; i < retCount; i++) { + page[i] = assignments[offset + i]; + } + return page; + } + + /** + @dev See {IPatchworAssignable-allowAssignment} + */ + function allowAssignment(uint256 /*ourTokenId*/, address /*target*/, uint256 /*targetTokenId*/, address /*targetOwner*/, address /*by*/, string memory /*scopeName*/) pure virtual public returns (bool) { + // By default allow multi assignments public + return true; + } +} \ No newline at end of file diff --git a/src/PatchworkFragmentSingle.sol b/src/PatchworkFragmentSingle.sol new file mode 100644 index 0000000..b5b6d7a --- /dev/null +++ b/src/PatchworkFragmentSingle.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./Patchwork721.sol"; +import "./interfaces/IPatchworkSingleAssignable.sol"; + +/** +@title PatchworkFragmentSingle +@dev base implementation of a Single-relation Fragment is IPatchworkSingleAssignable +*/ +abstract contract PatchworkFragmentSingle is Patchwork721, IPatchworkSingleAssignable { + + /// A mapping from token IDs in this contract to their assignments. + mapping(uint256 => Assignment) internal _assignments; + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkAssignable).interfaceId || + interfaceID == type(IPatchworkSingleAssignable).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @dev See {IPatchworkAssignableNFT-assign} + */ + function assign(uint256 ourTokenId, address to, uint256 tokenId) public virtual mustHaveTokenWriteAuth(tokenId) { + // One time use policy + Assignment storage a = _assignments[ourTokenId]; + if (a.tokenAddr != address(0)) { + revert IPatchworkProtocol.FragmentAlreadyAssigned(address(this), ourTokenId); + } + a.tokenAddr = to; + a.tokenId = tokenId; + emit Locked(ourTokenId); + } + + /** + @dev See {IPatchworkAssignableNFT-unassign} + */ + function unassign(uint256 tokenId) public virtual mustHaveTokenWriteAuth(tokenId) { + if (_assignments[tokenId].tokenAddr == address(0)) { + revert IPatchworkProtocol.FragmentNotAssigned(address(this), tokenId); + } + updateOwnership(tokenId); + delete _assignments[tokenId]; + emit Unlocked(tokenId); + } + + /** + @dev See {IPatchworAssignable-allowAssignment} + */ + function allowAssignment(uint256 ourTokenId, address /*target*/, uint256 /*targetTokenId*/, address targetOwner, address /*by*/, string memory /*scopeName*/) virtual public view returns (bool) { + // By default only allow single assignments to be to the same owner as the target + // Warning - Changing this without changing the other ownership logic in this contract to reflect this will make ownership inconsistent + return targetOwner == ownerOf(ourTokenId); + } + + /** + @dev See {IPatchworkAssignableNFT-updateOwnership} + */ + function updateOwnership(uint256 tokenId) public virtual { + Assignment storage assignment = _assignments[tokenId]; + if (assignment.tokenAddr != address(0)) { + address owner_ = ownerOf(tokenId); + address curOwner = super.ownerOf(tokenId); + if (owner_ != curOwner) { + // Parent ownership has changed, update our ownership to reflect this + ERC721._transfer(curOwner, owner_, tokenId); + } + } + } + + /** + @dev owned by the assignment's owner + @dev See {IERC721-ownerOf} + */ + function ownerOf(uint256 tokenId) public view virtual override(ERC721, IERC721) returns (address) { + // If assigned, it's owned by the assignment, otherwise normal owner + Assignment storage assignment = _assignments[tokenId]; + if (assignment.tokenAddr != address(0)) { + return IERC721(assignment.tokenAddr).ownerOf(assignment.tokenId); + } + return super.ownerOf(tokenId); + } + + /** + @dev See {IPatchworkAssignableNFT-unassignedOwnerOf} + */ + function unassignedOwnerOf(uint256 tokenId) public virtual view returns (address) { + return super.ownerOf(tokenId); + } + + /** + @dev See {IPatchworkAssignableNFT-getAssignedTo} + */ + function getAssignedTo(uint256 ourTokenId) public virtual view returns (address, uint256) { + Assignment storage a = _assignments[ourTokenId]; + return (a.tokenAddr, a.tokenId); + } + + /** + @dev See {IPatchworkAssignableNFT-onAssignedTransfer} + */ + function onAssignedTransfer(address from, address to, uint256 tokenId) public virtual { + require(msg.sender == _manager); + emit Transfer(from, to, tokenId); + } + + /** + @dev See {IPatchwork721-locked} + */ + function locked(uint256 tokenId) public view virtual override returns (bool) { + // Locked when assigned (implicit) or if explicitly locked + return _assignments[tokenId].tokenAddr != address(0) || super.locked(tokenId); + } + + /** + @dev See {IPatchwork721-setLocked} + */ + function setLocked(uint256 tokenId, bool locked_) public virtual override { + if (msg.sender != ownerOf(tokenId)) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + require(_assignments[tokenId].tokenAddr == address(0), "cannot setLocked assigned fragment"); + super.setLocked(tokenId, locked_); + } +} \ No newline at end of file diff --git a/src/PatchworkLiteRef.sol b/src/PatchworkLiteRef.sol new file mode 100644 index 0000000..c9d2274 --- /dev/null +++ b/src/PatchworkLiteRef.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "./interfaces/IPatchworkLiteRef.sol"; +import "./interfaces/IPatchworkProtocol.sol"; + +/** +@title PatchworkLiteRef +@dev base implementation of IPatchworkLiteRef +*/ +abstract contract PatchworkLiteRef is IPatchworkLiteRef, ERC165 { + + /// A mapping from reference IDs to their associated addresses. + mapping(uint8 => address) internal _referenceAddresses; + + /// A reverse mapping from addresses to their corresponding reference IDs. + mapping(address => uint8) internal _referenceAddressIds; + + /// A mapping indicating which reference IDs have been redacted. + mapping(uint8 => bool) internal _redactedReferenceIds; + + /// The ID that will be used for the next reference added. + uint8 internal _nextReferenceId; + + /** + @dev Constructor for the PatchworkLiteRef contract. Initializes the next reference ID to 1 to differentiate unregistered references. + */ + constructor() { + _nextReferenceId = 1; // Start at 1 so we can identify if we already have one registered + } + + /** + @notice implements a permission check for functions of this abstract class to use + @return allow true if write is allowed, false if not + */ + function _checkWriteAuth() internal virtual returns (bool allow); + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkLiteRef).interfaceId || + ERC165.supportsInterface(interfaceID); + } + + /** + @dev See {IPatchworkLiteRef-registerReferenceAddress} + */ + function registerReferenceAddress(address addr) public virtual _mustHaveWriteAuth returns (uint8 id) { + uint8 refId = _nextReferenceId; + if (_nextReferenceId == 255) { + revert IPatchworkProtocol.OutOfIDs(); + } + _nextReferenceId++; + if (_referenceAddressIds[addr] != 0) { + revert IPatchworkProtocol.FragmentAlreadyRegistered(addr); + } + _referenceAddresses[refId] = addr; + _referenceAddressIds[addr] = refId; + emit Register(address(this), addr, refId); + return refId; + } + + /** + @dev See {IPatchworkLiteRef-getReferenceId} + */ + function getReferenceId(address addr) public virtual view returns (uint8 id, bool redacted) { + uint8 refId = _referenceAddressIds[addr]; + return (refId, _redactedReferenceIds[refId]); + } + + /** + @dev See {IPatchworkLiteRef-getReferenceAddress} + */ + function getReferenceAddress(uint8 id) public virtual view returns (address addr, bool redacted) { + return (_referenceAddresses[id], _redactedReferenceIds[id]); + } + + /** + @dev See {IPatchworkLiteRef-redactReferenceAddress} + */ + function redactReferenceAddress(uint8 id) public virtual _mustHaveWriteAuth { + if (_referenceAddresses[id] == address(0)) { + revert IPatchworkProtocol.FragmentUnregistered(address(0)); + } + _redactedReferenceIds[id] = true; + emit Redact(address(this), _referenceAddresses[id]); + } + + /** + @dev See {IPatchworkLiteRef-unredactReferenceAddress} + */ + function unredactReferenceAddress(uint8 id) public virtual _mustHaveWriteAuth { + if (_referenceAddresses[id] == address(0)) { + revert IPatchworkProtocol.FragmentUnregistered(address(0)); + } + _redactedReferenceIds[id] = false; + emit Unredact(address(this), _referenceAddresses[id]); + } + + /** + @dev See {IPatchworkLiteRef-getLiteReference} + */ + function getLiteReference(address addr, uint256 tokenId) public virtual view returns (uint64 liteRef, bool redacted) { + uint8 refId = _referenceAddressIds[addr]; + if (refId == 0) { + return (0, false); + } + if (tokenId > type(uint56).max) { + revert IPatchworkProtocol.UnsupportedTokenId(tokenId); + } + return (uint64(uint256(refId) << 56 | tokenId), _redactedReferenceIds[refId]); + } + + /** + @dev See {IPatchworkLiteRef-getReferenceAddressAndTokenId} + */ + function getReferenceAddressAndTokenId(uint64 liteRef) public virtual view returns (address addr, uint256 tokenId) { + // <8 bits of refId, 56 bits of tokenId> + uint8 refId = uint8(liteRef >> 56); + tokenId = liteRef & 0x00FFFFFFFFFFFFFF; // 64 bit mask + return (_referenceAddresses[refId], tokenId); + } + + /** + @dev See {IPatchworkLiteRef-loadAllStaticReferences} + */ + function loadAllStaticReferences(uint256 tokenId) public virtual view returns (address[] memory addresses, uint256[] memory tokenIds) {} + + /** + @dev See {IPatchworkLiteRef-getDynamicReferenceCount} + */ + function getDynamicReferenceCount(uint256 tokenId) public virtual view returns (uint256 count) {} + + /** + @dev See {IPatchworkLiteRef-loadDynamicReferencePage} + */ + function loadDynamicReferencePage(uint256 tokenId, uint256 offset, uint256 count) public virtual view returns (address[] memory addresses, uint256[] memory tokenIds) {} + + modifier _mustHaveWriteAuth { + if (!_checkWriteAuth()) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + _; + } +} \ No newline at end of file diff --git a/src/PatchworkNFTBase.sol b/src/PatchworkNFTBase.sol deleted file mode 100644 index 32fd01a..0000000 --- a/src/PatchworkNFTBase.sol +++ /dev/null @@ -1,569 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "./PatchworkNFTInterface.sol"; -import "./PatchworkProtocol.sol"; -import "./IERC4906.sol"; - -/** -@title PatchworkNFT Abstract Contract -@dev This abstract contract defines the core functionalities for the PatchworkNFT. - It inherits from the standard ERC721, as well as the IPatchworkNFT and IERC4906 interfaces. -*/ -abstract contract PatchworkNFT is ERC721, IPatchworkNFT, IERC4906 { - - /// @dev The scope name for the NFT. - string internal _scopeName; - - /// @dev The address that denotes the owner of the contract. - address internal _owner; - - /// @dev The address that manages the NFTs (PatchworkProtocol). - address internal _manager; - - /// @dev A mapping to keep track of permissions for each address. - mapping(address => uint256) internal _permissionsAllow; - - /// @dev A mapping for storing metadata associated with each NFT token ID. - mapping(uint256 => uint256[]) internal _metadataStorage; - - /// @dev A mapping for storing freeze nonces of each NFT token ID. - mapping(uint256 => uint256) internal _freezeNonces; - - /// @dev A mapping indicating whether a specific NFT token ID is frozen. - mapping(uint256 => bool) internal _freezes; - - /// @dev A mapping indicating whether a specific NFT token ID is locked. - mapping(uint256 => bool) internal _locks; - - /** - * @notice Creates a new instance of the PatchworkNFT contract with the provided parameters. - * @param scopeName_ The scope name for the NFT. - * @param name_ The ERC-721 name for the NFT. - * @param symbol_ The ERC-721 symbol for the NFT. - * @param owner_ The address that will be set as the owner. - * @param manager_ The address that will be set as the manager (PatchworkProtocol). - */ - constructor( - string memory scopeName_, - string memory name_, - string memory symbol_, - address owner_, - address manager_ - ) ERC721(name_, symbol_) { - _scopeName = scopeName_; - _owner = owner_; - _manager = manager_; - } - - /** - @dev See {IPatchworkNFT-getScopeName} - */ - function getScopeName() public view virtual returns (string memory) { - return _scopeName; - } - - /** - @dev See {IPatchworkNFT-storePackedMetadataSlot} - */ - function storePackedMetadataSlot(uint256 tokenId, uint256 slot, uint256 data) public virtual mustHaveTokenWriteAuth(tokenId) { - _metadataStorage[tokenId][slot] = data; - } - - /** - @dev See {IPatchworkNFT-loadPackedMetadataSlot} - */ - function loadPackedMetadataSlot(uint256 tokenId, uint256 slot) public virtual view returns (uint256) { - return _metadataStorage[tokenId][slot]; - } - - // Does msg.sender have permission to write to our top level storage? - function _checkWriteAuth() internal virtual view returns (bool allow) { - return (msg.sender == _owner); - } - - // Does msg.sender have permission to write to this token's data? - function _checkTokenWriteAuth(uint256 /*tokenId*/) internal virtual view returns (bool allow) { - return (msg.sender == _owner || msg.sender == _manager); - } - - /** - @dev See {IPatchworkNFT-setPermissions} - */ - function setPermissions(address to, uint256 permissions) public virtual mustHaveWriteAuth { - _permissionsAllow[to] = permissions; - emit PermissionChange(to, permissions); - } - - /** - @dev See {IERC165-supportsInterface} - */ - function supportsInterface(bytes4 interfaceID) public view virtual override(ERC721, IERC165) returns (bool) { - return interfaceID == type(IPatchworkNFT).interfaceId || - interfaceID == type(IERC5192).interfaceId || - interfaceID == type(IERC4906).interfaceId || - ERC721.supportsInterface(interfaceID); - } - - /** - @dev See {IERC721-transferFrom}. - */ - function transferFrom(address from, address to, uint256 tokenId) public virtual override(ERC721, IERC721) { - PatchworkProtocol(_manager).applyTransfer(from, to, tokenId); - super.transferFrom(from, to, tokenId); - } - - /** - @dev See {IERC721-safeTransferFrom}. - */ - function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override(ERC721, IERC721) { - PatchworkProtocol(_manager).applyTransfer(from, to, tokenId); - super.safeTransferFrom(from, to, tokenId); - } - - /** - @dev See {IERC721-safeTransferFrom}. - */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override(ERC721, IERC721) { - PatchworkProtocol(_manager).applyTransfer(from, to, tokenId); - super.safeTransferFrom(from, to, tokenId, data); - } - - /** - @notice transfers a token with a known freeze nonce - @dev reverts if the token is not frozen or if the current freeze nonce does not match the provided nonce - @dev See {IERC721-transferFrom}. - */ - function transferFromWithFreezeNonce(address from, address to, uint256 tokenId, uint256 nonce) public mustBeFrozenWithNonce(tokenId, nonce) { - transferFrom(from, to, tokenId); - } - - /** - @notice transfers a token with a known freeze nonce - @dev reverts if the token is not frozen or if the current freeze nonce does not match the provided nonce - @dev See {IERC721-safeTransferFrom}. - */ - function safeTransferFromWithFreezeNonce(address from, address to, uint256 tokenId, uint256 nonce) public mustBeFrozenWithNonce(tokenId, nonce) { - safeTransferFrom(from, to, tokenId); - } - - /** - @notice transfers a token with a known freeze nonce - @dev reverts if the token is not frozen or if the current freeze nonce does not match the provided nonce - @dev See {IERC721-safeTransferFrom}. - */ - function safeTransferFromWithFreezeNonce(address from, address to, uint256 tokenId, bytes memory data, uint256 nonce) public mustBeFrozenWithNonce(tokenId, nonce) { - safeTransferFrom(from, to, tokenId, data); - } - - /** - @dev See {IPatchworkNFT-getFreezeNonce} - */ - function getFreezeNonce(uint256 tokenId) public view virtual returns (uint256 nonce) { - return _freezeNonces[tokenId]; - } - - /** - @dev See {IPatchworkNFT-setFrozen} - */ - function setFrozen(uint256 tokenId, bool frozen_) public virtual mustBeTokenOwner(tokenId) { - bool _frozen = _freezes[tokenId]; - if (_frozen != frozen_) { - if (frozen_) { - _freezes[tokenId] = true; - emit Frozen(tokenId); - } else { - _freezeNonces[tokenId]++; - _freezes[tokenId] = false; - emit Thawed(tokenId); - } - } - } - - /** - @dev See {IPatchworkNFT-frozen} - */ - function frozen(uint256 tokenId) public view virtual returns (bool) { - return _freezes[tokenId]; - } - - /** - @dev See {IPatchworkNFT-locked} - */ - function locked(uint256 tokenId) public view virtual returns (bool) { - return _locks[tokenId]; - } - - /** - @dev See {IPatchworkNFT-setLocked} - */ - function setLocked(uint256 tokenId, bool locked_) public virtual mustBeTokenOwner(tokenId) { - bool _locked = _locks[tokenId]; - if (_locked != locked_) { - _locks[tokenId] = locked_; - if (locked_) { - emit Locked(tokenId); - } else { - emit Unlocked(tokenId); - } - } - } - - modifier mustHaveWriteAuth { - if (!_checkWriteAuth()) { - revert PatchworkProtocol.NotAuthorized(msg.sender); - } - _; - } - - modifier mustHaveTokenWriteAuth(uint256 tokenId) { - if (!_checkTokenWriteAuth(tokenId)) { - revert PatchworkProtocol.NotAuthorized(msg.sender); - } - _; - } - - modifier mustBeTokenOwner(uint256 tokenId) { - if (msg.sender != ownerOf(tokenId)) { - revert PatchworkProtocol.NotAuthorized(msg.sender); - } - _; - } - - modifier mustBeFrozenWithNonce(uint256 tokenId, uint256 nonce) { - if (!frozen(tokenId)) { - revert PatchworkProtocol.NotFrozen(address(this), tokenId); - } - if (getFreezeNonce(tokenId) != nonce) { - revert PatchworkProtocol.IncorrectNonce(address(this), tokenId, nonce); - } - _; - } -} - -/** -@title PatchworkPatch -@dev Base implementation of IPatchworkPatch -@dev It is soul-bound to another ERC-721 and cannot be transferred or reassigned. -@dev It extends the functionalities of PatchworkNFT and implements the IPatchworkPatch interface. -*/ -abstract contract PatchworkPatch is PatchworkNFT, IPatchworkPatch { - - /// @dev Mapping from token ID to the address of the NFT that this patch is applied to. - mapping(uint256 => address) internal _patchedAddresses; - - /// @dev Mapping from token ID to the token ID of the NFT that this patch is applied to. - mapping(uint256 => uint256) internal _patchedTokenIds; - - /** - @dev See {IERC165-supportsInterface} - */ - function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { - return interfaceID == type(IPatchworkPatch).interfaceId || - super.supportsInterface(interfaceID); - } - - /** - @dev See {IPatchworkNFT-getScopeName} - */ - function getScopeName() public view virtual override(PatchworkNFT, IPatchworkPatch) returns (string memory) { - return _scopeName; - } - - /** - @dev will return the current owner of the patched address+tokenId - @dev See {IERC721-ownerOf} - */ - function ownerOf(uint256 tokenId) public view virtual override(ERC721, IERC721) returns (address) { - return IERC721(_patchedAddresses[tokenId]).ownerOf(_patchedTokenIds[tokenId]); - } - - /** - @notice stores a patch - @param tokenId the tokenId of the patch - @param originalNFTAddress the address of the original ERC-721 we are patching - @param originalNFTTokenId the tokenId of the original ERC-721 we are patching - */ - function _storePatch(uint256 tokenId, address originalNFTAddress, uint256 originalNFTTokenId) internal virtual { - _patchedAddresses[tokenId] = originalNFTAddress; - _patchedTokenIds[tokenId] = originalNFTTokenId; - } - - /** - @dev See {IPatchworkPatch-updateOwnership} - */ - function updateOwnership(uint256 tokenId) public virtual { - address patchedAddr = _patchedAddresses[tokenId]; - if (patchedAddr != address(0)) { - address owner_ = ownerOf(tokenId); - address curOwner = super.ownerOf(tokenId); - if (owner_ != curOwner) { - // Parent ownership has changed, update our ownership to reflect this - ERC721._transfer(curOwner, owner_, tokenId); - } - } - } - - /** - @dev See {IPatchworkPatch-unpatchedOwnerOf} - */ - function unpatchedOwnerOf(uint256 tokenId) public virtual view returns (address) { - return super.ownerOf(tokenId); - } - - /** - @dev always false because a patch cannot be locked as the ownership is inferred - @dev See {IPatchworkNFT-locked} - */ - function locked(uint256 /* tokenId */) public pure virtual override returns (bool) { - return false; - } - - /** - @dev always reverts because a patch cannot be locked as the ownership is inferred - @dev See {IPatchworkNFT-setLocked} - */ - function setLocked(uint256 /* tokenId */, bool /* locked_ */) public view virtual override { - revert PatchworkProtocol.CannotLockSoulboundPatch(address(this)); - } - - /** - @dev See {IPatchworkPatch-patchworkCompatible_} - */ - function patchworkCompatible_() external pure returns (bytes1) {} -} - -/** -@title PatchworkFragment -@dev base implementation of a Fragment is IPatchworkAssignableNFT -*/ -abstract contract PatchworkFragment is PatchworkNFT, IPatchworkAssignableNFT { - - /// Represents an assignment of a token from an external NFT contract to a token in this contract. - struct Assignment { - address tokenAddr; /// The address of the external NFT contract. - uint256 tokenId; /// The ID of the token in the external NFT contract. - } - - /// A mapping from token IDs in this contract to their assignments. - mapping(uint256 => Assignment) internal _assignments; - - /** - @dev See {IPatchworkNFT-getScopeName} - */ - function getScopeName() public view virtual override (IPatchworkAssignableNFT, PatchworkNFT) returns (string memory) { - return _scopeName; - } - - /** - @dev See {IERC165-supportsInterface} - */ - function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { - return interfaceID == type(IPatchworkAssignableNFT).interfaceId || - super.supportsInterface(interfaceID); - } - - /** - @dev See {IPatchworkAssignableNFT-assign} - */ - function assign(uint256 ourTokenId, address to, uint256 tokenId) public virtual mustHaveTokenWriteAuth(tokenId) { - // One time use policy - Assignment storage a = _assignments[ourTokenId]; - if (a.tokenAddr != address(0)) { - revert PatchworkProtocol.FragmentAlreadyAssigned(address(this), ourTokenId); - } - a.tokenAddr = to; - a.tokenId = tokenId; - emit Locked(ourTokenId); - } - - /** - @dev See {IPatchworkAssignableNFT-unassign} - */ - function unassign(uint256 tokenId) public virtual mustHaveTokenWriteAuth(tokenId) { - updateOwnership(tokenId); - delete _assignments[tokenId]; - emit Unlocked(tokenId); - } - - /** - @dev See {IPatchworkAssignableNFT-updateOwnership} - */ - function updateOwnership(uint256 tokenId) public virtual { - Assignment storage assignment = _assignments[tokenId]; - if (assignment.tokenAddr != address(0)) { - address owner_ = ownerOf(tokenId); - address curOwner = super.ownerOf(tokenId); - if (owner_ != curOwner) { - // Parent ownership has changed, update our ownership to reflect this - ERC721._transfer(curOwner, owner_, tokenId); - } - } - } - - /** - @dev owned by the assignment's owner - @dev See {IERC721-ownerOf} - */ - function ownerOf(uint256 tokenId) public view virtual override(ERC721, IERC721) returns (address) { - // If assigned, it's owned by the assignment, otherwise normal owner - Assignment storage assignment = _assignments[tokenId]; - if (assignment.tokenAddr != address(0)) { - return IERC721(assignment.tokenAddr).ownerOf(assignment.tokenId); - } - return super.ownerOf(tokenId); - } - - /** - @dev See {IPatchworkAssignableNFT-unassignedOwnerOf} - */ - function unassignedOwnerOf(uint256 tokenId) public virtual view returns (address) { - return super.ownerOf(tokenId); - } - - /** - @dev See {IPatchworkAssignableNFT-getAssignedTo} - */ - function getAssignedTo(uint256 ourTokenId) public virtual view returns (address, uint256) { - Assignment storage a = _assignments[ourTokenId]; - return (a.tokenAddr, a.tokenId); - } - - /** - @dev See {IPatchworkAssignableNFT-onAssignedTransfer} - */ - function onAssignedTransfer(address from, address to, uint256 tokenId) public virtual { - require(msg.sender == _manager); - emit Transfer(from, to, tokenId); - } - - /** - @dev See {IPatchworkNFT-locked} - */ - function locked(uint256 tokenId) public view virtual override returns (bool) { - // Locked when assigned (implicit) or if explicitly locked - return _assignments[tokenId].tokenAddr != address(0) || super.locked(tokenId); - } - - /** - @dev See {IPatchworkNFT-setLocked} - */ - function setLocked(uint256 tokenId, bool locked_) public virtual override { - if (msg.sender != ownerOf(tokenId)) { - revert PatchworkProtocol.NotAuthorized(msg.sender); - } - require(_assignments[tokenId].tokenAddr == address(0), "cannot setLocked assigned fragment"); - super.setLocked(tokenId, locked_); - } - - /** - @dev See {IPatchworkNFT-patchworkCompatible_} - */ - function patchworkCompatible_() external pure returns (bytes2) {} -} - -/** -@title PatchworkLiteRef -@dev base implementation of IPatchworkLiteRef -*/ -abstract contract PatchworkLiteRef is IPatchworkLiteRef, ERC165 { - - /// A mapping from reference IDs to their associated addresses. - mapping(uint8 => address) internal _referenceAddresses; - - /// A reverse mapping from addresses to their corresponding reference IDs. - mapping(address => uint8) internal _referenceAddressIds; - - /// A mapping indicating which reference IDs have been redacted. - mapping(uint8 => bool) internal _redactedReferenceIds; - - /// The ID that will be used for the next reference added. - uint8 internal _nextReferenceId; - - /** - @dev Constructor for the PatchworkLiteRef contract. Initializes the next reference ID to 1 to differentiate unregistered references. - */ - constructor() { - _nextReferenceId = 1; // Start at 1 so we can identify if we already have one registered - } - - /** - @notice implements a permission check for functions of this abstract class to use - @return allow true if write is allowed, false if not - */ - function _checkWriteAuth() internal virtual returns (bool allow); - - /** - @dev See {IERC165-supportsInterface} - */ - function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { - return interfaceID == type(IPatchworkLiteRef).interfaceId || - ERC165.supportsInterface(interfaceID); - } - - /** - @dev See {IPatchworkLiteRef-registerReferenceAddress} - */ - function registerReferenceAddress(address addr) public virtual _mustHaveWriteAuth returns (uint8 id) { - uint8 refId = _nextReferenceId; - if (_nextReferenceId == 255) { - revert PatchworkProtocol.OutOfIDs(); - } - _nextReferenceId++; - if (_referenceAddressIds[addr] != 0) { - revert PatchworkProtocol.FragmentAlreadyRegistered(addr); - } - _referenceAddresses[refId] = addr; - _referenceAddressIds[addr] = refId; - return refId; - } - - /** - @dev See {IPatchworkLiteRef-redactReferenceAddress} - */ - function redactReferenceAddress(uint8 id) public virtual _mustHaveWriteAuth { - _redactedReferenceIds[id] = true; - emit Redact(address(this), _referenceAddresses[id]); - } - - /** - @dev See {IPatchworkLiteRef-unredactReferenceAddress} - */ - function unredactReferenceAddress(uint8 id) public virtual _mustHaveWriteAuth { - _redactedReferenceIds[id] = false; - emit Unredact(address(this), _referenceAddresses[id]); - } - - /** - @dev See {IPatchworkLiteRef-getLiteReference} - */ - function getLiteReference(address addr, uint256 tokenId) public virtual view returns (uint64 referenceAddress, bool redacted) { - uint8 refId = _referenceAddressIds[addr]; - if (refId == 0) { - return (0, false); - } - if (tokenId > type(uint56).max) { - revert PatchworkProtocol.UnsupportedTokenId(tokenId); - } - return (uint64(uint256(refId) << 56 | tokenId), _redactedReferenceIds[refId]); - } - - /** - @dev See {IPatchworkLiteRef-getReferenceAddressAndTokenId} - */ - function getReferenceAddressAndTokenId(uint64 referenceAddress) public virtual view returns (address addr, uint256 tokenId) { - // <8 bits of refId, 56 bits of tokenId> - uint8 refId = uint8(referenceAddress >> 56); - tokenId = referenceAddress & 0x00FFFFFFFFFFFFFF; // 64 bit mask - return (_referenceAddresses[refId], tokenId); - } - - modifier _mustHaveWriteAuth { - if (!_checkWriteAuth()) { - revert PatchworkProtocol.NotAuthorized(msg.sender); - } - _; - } -} \ No newline at end of file diff --git a/src/PatchworkNFTInterface.sol b/src/PatchworkNFTInterface.sol deleted file mode 100644 index d95ef1d..0000000 --- a/src/PatchworkNFTInterface.sol +++ /dev/null @@ -1,370 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import "forge-std/console.sol"; -import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "./IERC5192.sol"; - -/** -@title Patchwork Protocol NFT Interface Metadata -@author Runic Labs, Inc -@notice Metadata for IPatchworkNFT and related contract interfaces -*/ -interface PatchworkNFTInterfaceMeta { - /** - @notice Enumeration of possible field data types. - @dev This defines the various basic data types for the fields. - */ - enum FieldType { - BOOLEAN, ///< A Boolean type (true or false). - INT8, ///< An 8-bit signed integer. - INT16, ///< A 16-bit signed integer. - INT32, ///< A 32-bit signed integer. - INT64, ///< A 64-bit signed integer. - INT128, ///< A 128-bit signed integer. - INT256, ///< A 256-bit signed integer. - UINT8, ///< An 8-bit unsigned integer. - UINT16, ///< A 16-bit unsigned integer. - UINT32, ///< A 32-bit unsigned integer. - UINT64, ///< A 64-bit unsigned integer. - UINT128, ///< A 128-bit unsigned integer. - UINT256, ///< A 256-bit unsigned integer. - CHAR8, ///< An 8-character string. - CHAR16, ///< A 16-character string. - CHAR32, ///< A 32-character string. - CHAR64, ///< A 64-character string. - LITEREF ///< A Literef reference to a patchwork fragment - } - - /** - @notice Struct defining the metadata schema. - @dev This defines the overall structure of the metadata and contains entries describing each data field. - */ - struct MetadataSchema { - uint256 version; ///< Version of the metadata schema. - MetadataSchemaEntry[] entries; ///< Array of entries in the schema. - } - - /** - @notice Struct defining individual entries within the metadata schema. - @dev Represents each data field in the schema, detailing its properties and type. - */ - struct MetadataSchemaEntry { - uint256 id; ///< Index or unique identifier of the entry. - uint256 permissionId; ///< Permission identifier associated with the entry. - FieldType fieldType; ///< Type of field data (from the FieldType enum). - uint256 arrayLength; ///< Length of array for the field (0 means it's a single field). - FieldVisibility visibility; ///< Visibility level of the field. - uint256 slot; ///< Starting storage slot, may span multiple slots based on width. - uint256 offset; ///< Offset in bits within the storage slot. - string key; ///< Key or name associated with the field. - } - - /** - @notice Enumeration of field visibility options. - @dev Specifies whether a field is publicly accessible or private. - */ - enum FieldVisibility { - PUBLIC, ///< Field is publicly accessible. - PRIVATE ///< Field is private - } -} - -/** -@title Patchwork Protocol NFT Interface -@author Runic Labs, Inc -@notice Interface for contracts supporting Patchwork metadata standard -*/ -interface IPatchworkNFT is PatchworkNFTInterfaceMeta, IERC5192 { - /** - @notice Emitted when the freeze status is changed to frozen. - @param tokenId The identifier for a token. - */ - event Frozen(uint256 indexed tokenId); - - /** - @notice Emitted when the locking status is changed to not frozen. - @param tokenId The identifier for a token. - */ - event Thawed(uint256 indexed tokenId); - - /** - @notice Emitted when the permissions are changed for an NFT - @param to The address the permissions are assigned to - @param permissions The permissions - */ - event PermissionChange(address indexed to, uint256 permissions); - - /** - @notice Get the scope this NFT claims to belong to - @return string the name of the scope - */ - function getScopeName() external returns (string memory); - - /** - @notice Returns the URI of the schema - @return string the URI of the schema - */ - function schemaURI() external returns (string memory); - - /** - @notice Returns the metadata schema - @return MetadataSchema the metadata schema - */ - function schema() external returns (MetadataSchema memory); - - /** - @notice Returns the URI of the image associated with the given token ID - @param tokenId ID of the token - @return string the image URI - */ - function imageURI(uint256 tokenId) external returns (string memory); - - /** - @notice Sets permissions for a given address - @param to Address to set permissions for - @param permissions Permissions value - */ - function setPermissions(address to, uint256 permissions) external; - - /** - @notice Stores packed metadata for a given token ID and slot - @param tokenId ID of the token - @param slot Slot to store metadata - @param data Metadata to store - */ - function storePackedMetadataSlot(uint256 tokenId, uint256 slot, uint256 data) external; - - /** - @notice Loads packed metadata for a given token ID and slot - @param tokenId ID of the token - @param slot Slot to load metadata from - @return uint256 the raw slot data as a uint256 - */ - function loadPackedMetadataSlot(uint256 tokenId, uint256 slot) external returns (uint256); - - /** - @notice Returns the freeze nonce for a given token ID - @param tokenId ID of the token - @return nonce the nonce - */ - function getFreezeNonce(uint256 tokenId) external returns (uint256 nonce); - - /** - @notice Sets the freeze status of a token - @param tokenId ID of the token - @param frozen Freeze status to set - */ - function setFrozen(uint256 tokenId, bool frozen) external; - - /** - @notice Gets the freeze status of a token (ERC-5192) - @param tokenId ID of the token - @return bool true if frozen, false if not - */ - function frozen(uint256 tokenId) external view returns (bool); - - /** - @notice Sets the lock status of a token - @param tokenId ID of the token - @param locked Lock status to set - */ - function setLocked(uint256 tokenId, bool locked) external; -} - -/** -@title Patchwork Protocol Patch Interface -@author Runic Labs, Inc -@notice Interface for contracts supporting Patchwork patch standard -*/ -interface IPatchworkPatch { - /** - @notice Get the scope this NFT claims to belong to - @return string the name of the scope - */ - function getScopeName() external returns (string memory); - - /** - @notice Creates a new token for the owner, representing a patch - @param owner Address of the owner of the token - @param originalNFTAddress Address of the original NFT - @param originalNFTTokenId ID of the original NFT token - @return tokenId ID of the newly minted token - */ - function mintPatch(address owner, address originalNFTAddress, uint256 originalNFTTokenId) external returns (uint256 tokenId); - - /** - @notice Updates the real underlying ownership of a token in storage (if different from current) - @param tokenId ID of the token - */ - function updateOwnership(uint256 tokenId) external; - - /** - @notice Returns the underlying stored owner of a token ignoring real patched NFT ownership - @param tokenId ID of the token - @return address Address of the owner - */ - function unpatchedOwnerOf(uint256 tokenId) external returns (address); - - /** - @notice A deliberately incompatible function to block implementing both assignable and patch - @return bytes1 Always returns 0x00 - */ - function patchworkCompatible_() external pure returns (bytes1); -} - -/** -@title Patchwork Protocol Assignable NFT Interface -@author Runic Labs, Inc -@notice Interface for contracts supporting Patchwork assignment -*/ -interface IPatchworkAssignableNFT { - /** - @notice Get the scope this NFT claims to belong to - @return string the name of the scope - */ - function getScopeName() external returns (string memory); - - /** - @notice Assigns a token to another - @param ourTokenId ID of our token - @param to Address to assign to - @param tokenId ID of the token to assign - */ - function assign(uint256 ourTokenId, address to, uint256 tokenId) external; - - /** - @notice Unassigns a token - @param ourTokenId ID of our token - */ - function unassign(uint256 ourTokenId) external; - - /** - @notice Returns the address and token ID that our token is assigned to - @param ourTokenId ID of our token - @return address the address this is assigned to - @return uint256 the tokenId this is assigned to - */ - function getAssignedTo(uint256 ourTokenId) external view returns (address, uint256); - - /** - @notice Returns the underlying stored owner of a token ignoring current assignment - @param ourTokenId ID of our token - @return address address of the owner - */ - function unassignedOwnerOf(uint256 ourTokenId) external view returns (address); - - /** - @notice Sends events for a token when the assigned-to token has been transferred - @param from Sender address - @param to Recipient address - @param tokenId ID of the token - */ - function onAssignedTransfer(address from, address to, uint256 tokenId) external; - - /** - @notice Updates the real underlying ownership of a token in storage (if different from current) - @param tokenId ID of the token - */ - function updateOwnership(uint256 tokenId) external; - - /** - @notice A deliberately incompatible function to block implementing both assignable and patch - @return bytes2 Always returns 0x0000 - */ - function patchworkCompatible_() external pure returns (bytes2); -} - -/** -@title Patchwork Protocol LiteRef NFT Interface -@author Runic Labs, Inc -@notice Interface for contracts that have Lite Reference ID support -*/ -interface IPatchworkLiteRef { - /** - @notice Emitted when a contract redacts a fragment - @param target the contract which issued the redaction - @param fragment the fragment that was redacted - */ - event Redact(address indexed target, address indexed fragment); - - /** - @notice Emitted when a contract unredacts a fragment - @param target the contract which revoked the redaction - @param fragment the fragment that was unredacted - */ - event Unredact(address indexed target, address indexed fragment); - - /** - @notice Registers a reference address - @param addr Address to register - @return id ID assigned to the address - */ - function registerReferenceAddress(address addr) external returns (uint8 id); - - /** - @notice Redacts a reference address - @param id ID of the address to redact - */ - function redactReferenceAddress(uint8 id) external; - - /** - @notice Unredacts a reference address - @param id ID of the address to unredact - */ - function unredactReferenceAddress(uint8 id) external; - - /** - @notice Returns a lite reference for a given address and token ID - @param addr Address to get reference for - @param tokenId ID of the token - @return liteRef Lite reference - @return redacted Redacted status - */ - function getLiteReference(address addr, uint256 tokenId) external view returns (uint64 liteRef, bool redacted); - - /** - @notice Returns an address and token ID for a given lite reference - @param liteRef Lite reference to get address and token ID for - @return addr Address - @return tokenId Token ID - */ - function getReferenceAddressAndTokenId(uint64 liteRef) external view returns (address addr, uint256 tokenId); - - /** - @notice Adds a reference to a token - @param tokenId ID of the token - @param referenceAddress Reference address to add - */ - function addReference(uint256 tokenId, uint64 referenceAddress) external; - - /** - @notice Adds multiple references to a token - @param tokenId ID of the token - @param liteRefs Array of lite references to add - */ - function batchAddReferences(uint256 tokenId, uint64[] calldata liteRefs) external; - - /** - @notice Removes a reference from a token - @param tokenId ID of the token - @param liteRef Lite reference to remove - */ - function removeReference(uint256 tokenId, uint64 liteRef) external; - - /** - @notice Loads a reference address and token ID at a given index - @param idx Index to load from - @return addr Address - @return tokenId Token ID - */ - function loadReferenceAddressAndTokenId(uint256 idx) external view returns (address addr, uint256 tokenId); - - /** - @notice Loads all references for a given token ID - @param tokenId ID of the token - @return addresses Array of addresses - @return tokenIds Array of token IDs - */ - function loadAllReferences(uint256 tokenId) external view returns (address[] memory addresses, uint256[] memory tokenIds); -} diff --git a/src/PatchworkPatch.sol b/src/PatchworkPatch.sol new file mode 100644 index 0000000..eaf3125 --- /dev/null +++ b/src/PatchworkPatch.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./Patchwork721.sol"; +import "./interfaces/IPatchworkPatch.sol"; + +/** +@title PatchworkPatch +@dev Base implementation of IPatchworkPatch +@dev It is soul-bound to another ERC-721 and cannot be transferred or reassigned. +@dev It extends the functionalities of Patchwork721 and implements the IPatchworkPatch interface. +*/ +abstract contract PatchworkPatch is Patchwork721, IPatchworkPatch { + + /// @dev Mapping from token ID to the canonical address and tokenId of the NFT that this patch is applied to. + mapping(uint256 => PatchTarget) internal _targetsById; + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkPatch).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @dev will return the current owner of the patched address+tokenId + @dev See {IERC721-ownerOf} + */ + function ownerOf(uint256 tokenId) public view virtual override(ERC721, IERC721) returns (address) { + // Default is inherited ownership + PatchTarget storage target = _targetsById[tokenId]; + return IERC721(target.addr).ownerOf(target.tokenId); + } + + /** + @notice stores a patch + @param tokenId the tokenId of the patch + @param target the target 721 being patched + */ + function _storePatch(uint256 tokenId, PatchTarget memory target) internal virtual { + _targetsById[tokenId] = target; + } + + /** + @dev See {IPatchworkPatch-updateOwnership} + */ + function updateOwnership(uint256 tokenId) public virtual { + address patchedAddr = _targetsById[tokenId].addr; + if (patchedAddr != address(0)) { + address owner_ = ownerOf(tokenId); + address curOwner = super.ownerOf(tokenId); + if (owner_ != curOwner) { + // Parent ownership has changed, update our ownership to reflect this + ERC721._transfer(curOwner, owner_, tokenId); + } + } + } + + /** + @dev See {IPatchworkPatch-ownerOfPatch} + */ + function ownerOfPatch(uint256 tokenId) public virtual view returns (address) { + return ERC721.ownerOf(tokenId); + } + + /** + @dev always false because a patch cannot be locked as the ownership is inferred + @dev See {IPatchwork721-locked} + */ + function locked(uint256 /* tokenId */) public pure virtual override returns (bool) { + return false; + } + + /** + @dev always reverts because a patch cannot be locked as the ownership is inferred + @dev See {IPatchwork721-setLocked} + */ + function setLocked(uint256 /* tokenId */, bool /* locked_ */) public view virtual override { + revert IPatchworkProtocol.CannotLockSoulboundPatch(address(this)); + } + + /** + @dev See {ERC721-_burn} + */ + function _burnPatch(uint256 tokenId) internal virtual { + PatchTarget storage target = _targetsById[tokenId]; + IPatchworkProtocol(_manager).patchBurned(target.addr, target.tokenId, address(this)); + delete _targetsById[tokenId]; + super._burn(tokenId); + } +} + +abstract contract PatchworkReversiblePatch is PatchworkPatch, IPatchworkReversiblePatch { + /// @dev Mapping of hash of original address + token ID for reverse lookups + mapping(bytes32 => uint256) internal _idsByTargetHash; // hash of patched addr+tokenid to tokenId + + /** + @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPatchworkReversiblePatch).interfaceId || + super.supportsInterface(interfaceID); + } + + /** + @dev See {IPatchworkPatch-getTokenIdByTarget} + */ + function getTokenIdByTarget(PatchTarget memory target) public view virtual returns (uint256 tokenId) { + return _idsByTargetHash[keccak256(abi.encode(target))]; + } + + /** + @notice stores a patch + @param tokenId the tokenId of the patch + @param target the target 721 being patched + */ + function _storePatch(uint256 tokenId, PatchTarget memory target) internal virtual override { + super._storePatch(tokenId, target); + _idsByTargetHash[keccak256(abi.encode(target))] = tokenId; + } + + /** + @dev See {ERC721-_burn} + */ + function _burnPatch(uint256 tokenId) internal virtual override { + PatchTarget storage target = _targetsById[tokenId]; + delete _idsByTargetHash[keccak256(abi.encode(target))]; + super._burnPatch(tokenId); + } +} \ No newline at end of file diff --git a/src/PatchworkProtocol.sol b/src/PatchworkProtocol.sol index d851290..0c6b833 100644 --- a/src/PatchworkProtocol.sol +++ b/src/PatchworkProtocol.sol @@ -1,421 +1,459 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "./PatchworkNFTInterface.sol"; +/** + + ____ __ __ __ + / __ \____ _/ /______/ /_ _ ______ _____/ /__ + / /_/ / __ `/ __/ ___/ __ \ | /| / / __ \/ ___/ //_/ + / ____/ /_/ / /_/ /__/ / / / |/ |/ / /_/ / / / ,< +/_/ ___\__,_/\__/\___/_/ /_/|__/|__/\____/_/ /_/|_| + / __ \_________ / /_____ _________ / / + / /_/ / ___/ __ \/ __/ __ \/ ___/ __ \/ / + / ____/ / / /_/ / /_/ /_/ / /__/ /_/ / / +/_/ /_/ \____/\__/\____/\___/\____/_/ + +*/ + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "./PatchworkProtocolCommon.sol"; +import "./interfaces/IPatchwork721.sol"; +import "./interfaces/IPatchworkSingleAssignable.sol"; +import "./interfaces/IPatchworkMultiAssignable.sol"; +import "./interfaces/IPatchworkLiteRef.sol"; +import "./interfaces/IPatchworkPatch.sol"; +import "./interfaces/IPatchwork1155Patch.sol"; +import "./interfaces/IPatchworkAccountPatch.sol"; +import "./interfaces/IPatchworkProtocol.sol"; +import "./interfaces/IPatchworkMintable.sol"; +import "./interfaces/IPatchworkScoped.sol"; /** @title Patchwork Protocol @author Runic Labs, Inc -@notice Manages data integrity of relational NFTs implemented with Patchwork interfaces */ -contract PatchworkProtocol { +contract PatchworkProtocol is IPatchworkProtocol, PatchworkProtocolCommon { + + /// How much time must elapse before a fee change can be committed (1209600 = 2 weeks) + uint256 public constant FEE_CHANGE_TIMELOCK = 1209600; - /** - @notice The address is not authorized to perform this action - @param addr The address attempting to perform the action - */ - error NotAuthorized(address addr); + /// How much time must elapse before a contract upgrade can be committed (1209600 = 2 weeks) + uint256 public constant CONTRACT_UPGRADE_TIMELOCK = 1209600; - /** - @notice The scope with the provided name already exists - @param scopeName Name of the scope - */ - error ScopeExists(string scopeName); + /// The denominator for fee basis points + uint256 private constant _FEE_BASIS_DENOM = 10000; - /** - @notice The scope with the provided name does not exist - @param scopeName Name of the scope - */ - error ScopeDoesNotExist(string scopeName); - - /** - @notice Transfer of the scope to the provided address is not allowed - @param to Address not allowed for scope transfer - */ - error ScopeTransferNotAllowed(address to); + /// The maximum basis points patchwork can ever be configured to + uint256 private constant _PROTOCOL_FEE_CEILING = 3000; - /** - @notice The token with the provided ID at the given address is frozen - @param addr Address of the token owner - @param tokenId ID of the frozen token - */ - error Frozen(address addr, uint256 tokenId); - - /** - @notice The token with the provided ID at the given address is locked - @param addr Address of the token owner - @param tokenId ID of the locked token - */ - error Locked(address addr, uint256 tokenId); - - /** - @notice The address is not whitelisted for the given scope - @param scopeName Name of the scope - @param addr Address that isn't whitelisted - */ - error NotWhitelisted(string scopeName, address addr); - - /** - @notice The token at the given address has already been patched - @param addr Address of the token owner - @param tokenId ID of the patched token - @param patchAddress Address of the patch applied - */ - error AlreadyPatched(address addr, uint256 tokenId, address patchAddress); - - /** - @notice The provided input lengths are not compatible or valid - @dev for any multi array inputs, they must be the same length - */ - error BadInputLengths(); - - /** - @notice The fragment at the given address is unregistered - @param addr Address of the unregistered fragment - */ - error FragmentUnregistered(address addr); + /// Constructor + /// @param owner_ The address of the initial owner + constructor(address owner_, address assignerDelegate_) PatchworkProtocolCommon(owner_) { + _assignerDelegate = assignerDelegate_; + } /** - @notice The fragment at the given address has been redacted - @param addr Address of the redacted fragment + @dev See {IPatchworkProtocol-claimScope} */ - error FragmentRedacted(address addr); + function claimScope(string calldata scopeName) public { + if (bytes(scopeName).length == 0) { + revert NotAuthorized(msg.sender); + } + Scope storage s = _scopes[scopeName]; + if (s.owner != address(0)) { + revert ScopeExists(scopeName); + } + s.owner = msg.sender; + s.requireWhitelist = true; // better security by default + emit ScopeClaim(scopeName, msg.sender); + } /** - @notice The fragment with the provided ID at the given address is already assigned - @param addr Address of the fragment - @param tokenId ID of the assigned fragment + @dev See {IPatchworkProtocol-transferScopeOwnership} */ - error FragmentAlreadyAssigned(address addr, uint256 tokenId); + function transferScopeOwnership(string calldata scopeName, address newOwner) public { + Scope storage s = _mustHaveScope(scopeName); + _mustBeOwner(s); + if (newOwner == address(0)) { + revert ScopeTransferNotAllowed(address(0)); + } + s.ownerElect = newOwner; + emit ScopeTransferElect(scopeName, s.owner, s.ownerElect); + } /** - @notice The fragment with the provided ID at the given address is already assigned in the scope - @param scopeName Name of the scope - @param addr Address of the fragment - @param tokenId ID of the fragment + @dev See {IPatchworkProtocol-cancelScopeTransfer} */ - error FragmentAlreadyAssignedInScope(string scopeName, address addr, uint256 tokenId); + function cancelScopeTransfer(string calldata scopeName) public { + Scope storage s = _mustHaveScope(scopeName); + _mustBeOwner(s); + emit ScopeTransferCancel(scopeName, s.owner, s.ownerElect); + s.ownerElect = address(0); + } /** - @notice The reference was not found in the scope for the given fragment and target - @param scopeName Name of the scope - @param target Address of the target token - @param fragment Address of the fragment - @param tokenId ID of the fragment + @dev See {IPatchworkProtocol-acceptScopeTransfer} */ - error RefNotFoundInScope(string scopeName, address target, address fragment, uint256 tokenId); + function acceptScopeTransfer(string calldata scopeName) public { + Scope storage s = _mustHaveScope(scopeName); + if (s.ownerElect == msg.sender) { + address oldOwner = s.owner; + s.owner = msg.sender; + s.ownerElect = address(0); + emit ScopeTransfer(scopeName, oldOwner, msg.sender); + } else { + revert NotAuthorized(msg.sender); + } + } /** - @notice The fragment with the provided ID at the given address is not assigned - @param addr Address of the fragment - @param tokenId ID of the fragment + @dev See {IPatchworkProtocol-getScopeOwnerElect} */ - error FragmentNotAssigned(address addr, uint256 tokenId); + function getScopeOwnerElect(string calldata scopeName) public view returns (address ownerElect) { + return _scopes[scopeName].ownerElect; + } /** - @notice The fragment at the given address is already registered - @param addr Address of the registered fragment + @dev See {IPatchworkProtocol-getScopeOwner} */ - error FragmentAlreadyRegistered(address addr); + function getScopeOwner(string calldata scopeName) public view returns (address owner) { + return _scopes[scopeName].owner; + } /** - @notice Ran out of available IDs for allocation - @dev Max 255 IDs per NFT + @dev See {IPatchworkProtocol-addOperator} */ - error OutOfIDs(); + function addOperator(string calldata scopeName, address op) public { + Scope storage s = _mustHaveScope(scopeName); + _mustBeOwner(s); + s.operators[op] = true; + emit ScopeAddOperator(scopeName, msg.sender, op); + } /** - @notice The provided token ID is unsupported - @dev TokenIds may only be 56 bits long - @param tokenId The unsupported token ID + @dev See {IPatchworkProtocol-removeOperator} */ - error UnsupportedTokenId(uint256 tokenId); + function removeOperator(string calldata scopeName, address op) public { + Scope storage s = _mustHaveScope(scopeName); + _mustBeOwner(s); + s.operators[op] = false; + emit ScopeRemoveOperator(scopeName, msg.sender, op); + } /** - @notice Cannot lock the soulbound patch at the given address - @param addr Address of the soulbound patch + @dev See {IPatchworkProtocol-setScopeRules} */ - error CannotLockSoulboundPatch(address addr); + function setScopeRules(string calldata scopeName, bool allowUserPatch, bool allowUserAssign, bool requireWhitelist) public { + Scope storage s = _mustHaveScope(scopeName); + _mustBeOwner(s); + s.allowUserPatch = allowUserPatch; + s.allowUserAssign = allowUserAssign; + s.requireWhitelist = requireWhitelist; + emit ScopeRuleChange(scopeName, msg.sender, allowUserPatch, allowUserAssign, requireWhitelist); + } /** - @notice The token with the provided ID at the given address is not frozen - @param addr Address of the token owner - @param tokenId ID of the token + @dev See {IPatchworkProtocol-setMintConfiguration} */ - error NotFrozen(address addr, uint256 tokenId); + function setMintConfiguration(address addr, MintConfig memory config) public { + if (!IERC165(addr).supportsInterface(type(IPatchworkMintable).interfaceId)) { + revert UnsupportedContract(); + } + string memory scopeName = _getScopeName(addr); + Scope storage scope = _mustHaveScope(scopeName); + _mustBeWhitelisted(scopeName, scope, addr); + _mustBeOwnerOrOperator(scope); + scope.mintConfigurations[addr] = config; + emit MintConfigure(scopeName, msg.sender, addr, config); + } /** - @notice The nonce for the token with the provided ID at the given address is incorrect - @dev It may be incorrect or a newer nonce may be present - @param addr Address of the token owner - @param tokenId ID of the token - @param nonce The incorrect nonce + @dev See {IPatchworkProtocol-getMintConfiguration} */ - error IncorrectNonce(address addr, uint256 tokenId, uint256 nonce); + function getMintConfiguration(address addr) public view returns (MintConfig memory config) { + if (!IERC165(addr).supportsInterface(type(IPatchworkMintable).interfaceId)) { + revert UnsupportedContract(); + } + Scope storage scope = _mustHaveScope(_getScopeNameViewOnly(addr)); + return scope.mintConfigurations[addr]; + } /** - @notice Self assignment of the token with the provided ID at the given address is not allowed - @param addr Address of the token owner - @param tokenId ID of the token + @dev See {IPatchworkProtocol-setPatchFee} */ - error SelfAssignmentNotAllowed(address addr, uint256 tokenId); + function setPatchFee(address addr, uint256 baseFee) public { + if (!IERC165(addr).supportsInterface(type(IPatchworkScoped).interfaceId)) { + revert UnsupportedContract(); + } + string memory scopeName = _getScopeName(addr); + Scope storage scope = _mustHaveScope(scopeName); + _mustBeWhitelisted(scopeName, scope, addr); + _mustBeOwnerOrOperator(scope); + scope.patchFees[addr] = baseFee; + emit PatchFeeChange(scopeName, addr, baseFee); + } /** - @notice Transfer of the soulbound token with the provided ID at the given address is not allowed - @param addr Address of the token owner - @param tokenId ID of the token + @dev See {IPatchworkProtocol-getPatchFee} */ - error SoulboundTransferNotAllowed(address addr, uint256 tokenId); + function getPatchFee(address addr) public view returns (uint256 baseFee) { + if (!IERC165(addr).supportsInterface(type(IPatchworkScoped).interfaceId)) { + revert UnsupportedContract(); + } + Scope storage scope = _mustHaveScope(_getScopeNameViewOnly(addr)); + return scope.patchFees[addr]; + } /** - @notice Transfer of the token with the provided ID at the given address is blocked by an assignment - @param addr Address of the token owner - @param tokenId ID of the token + @dev See {IPatchworkProtocol-setAssignFee} */ - error TransferBlockedByAssignment(address addr, uint256 tokenId); + function setAssignFee(address fragmentAddress, uint256 baseFee) public { + if (!IERC165(fragmentAddress).supportsInterface(type(IPatchworkScoped).interfaceId)) { + revert UnsupportedContract(); + } + string memory scopeName = _getScopeName(fragmentAddress); + Scope storage scope = _mustHaveScope(scopeName); + _mustBeWhitelisted(scopeName, scope, fragmentAddress); + _mustBeOwnerOrOperator(scope); + scope.assignFees[fragmentAddress] = baseFee; + emit AssignFeeChange(scopeName, fragmentAddress, baseFee); + } /** - @notice The token at the given address is not IPatchworkAssignable - @param addr Address of the non-assignable token + @dev See {IPatchworkProtocol-getAssignFee} */ - error NotPatchworkAssignable(address addr); + function getAssignFee(address fragmentAddress) public view returns (uint256 baseFee) { + if (!IERC165(fragmentAddress).supportsInterface(type(IPatchworkScoped).interfaceId)) { + revert UnsupportedContract(); + } + Scope storage scope = _mustHaveScope(_getScopeNameViewOnly(fragmentAddress)); + return scope.assignFees[fragmentAddress]; + } /** - @notice A data integrity error has been detected - @dev Addr+TokenId is expected where addr2+tokenId2 is present - @param addr Address of the first token - @param tokenId ID of the first token - @param addr2 Address of the second token - @param tokenId2 ID of the second token + @dev See {IPatchworkProtocol-addBanker} */ - error DataIntegrityError(address addr, uint256 tokenId, address addr2, uint256 tokenId2); + function addBanker(string memory scopeName, address addr) public { + Scope storage scope = _mustHaveScope(scopeName); + _mustBeOwnerOrOperator(scope); + scope.bankers[addr] = true; + emit ScopeBankerAdd(scopeName, msg.sender, addr); + } /** - @notice Represents a defined scope within the system - @dev Contains details about the scope ownership, permissions, and mappings for references and assignments + @dev See {IPatchworkProtocol-removeBanker} */ - struct Scope { - /** - @notice Owner of this scope - @dev Address of the account or contract that owns this scope - */ - address owner; - - /** - @notice Indicates whether a user is allowed to patch within this scope - @dev True if a user can patch, false otherwise. If false, only operators and the scope owner can perform patching. - */ - bool allowUserPatch; - - /** - @notice Indicates whether a user is allowed to assign within this scope - @dev True if a user can assign, false otherwise. If false, only operators and the scope owner can perform assignments. - */ - bool allowUserAssign; - - /** - @notice Indicates if a whitelist is required for operations within this scope - @dev True if whitelist is required, false otherwise - */ - bool requireWhitelist; - - /** - @notice Mapped list of operator addresses for this scope - @dev Address of the operator mapped to a boolean indicating if they are an operator - */ - mapping(address => bool) operators; - - /** - @notice Mapped list of lightweight references within this scope - // TODO: A unique hash of liteRefAddr + reference will be needed for uniqueness - */ - mapping(uint64 => bool) liteRefs; - - /** - @notice Mapped whitelist of addresses that belong to this scope - @dev Address mapped to a boolean indicating if it's whitelisted - */ - mapping(address => bool) whitelist; - - /** - @notice Mapped list of unique patches associated with this scope - @dev Hash of the patch mapped to a boolean indicating its uniqueness - */ - mapping(bytes32 => bool) uniquePatches; + function removeBanker(string memory scopeName, address addr) public { + Scope storage scope = _mustHaveScope(scopeName); + _mustBeOwnerOrOperator(scope); + delete scope.bankers[addr]; + emit ScopeBankerRemove(scopeName, msg.sender, addr); } - mapping(string => Scope) private _scopes; - /** - @notice Emitted when a fragment is assigned - @param owner The owner of the target and fragment - @param fragmentAddress The address of the fragment's contract - @param fragmentTokenId The tokenId of the fragment - @param targetAddress The address of the target's contract - @param targetTokenId The tokenId of the target + @dev See {IPatchworkProtocol-withdraw} */ - event Assign(address indexed owner, address fragmentAddress, uint256 fragmentTokenId, address indexed targetAddress, uint256 indexed targetTokenId); + function withdraw(string memory scopeName, uint256 amount) public nonReentrant { + Scope storage scope = _mustHaveScope(scopeName); + if (msg.sender != scope.owner && !scope.bankers[msg.sender]) { + revert NotAuthorized(msg.sender); + } + if (amount > scope.balance) { + revert InsufficientFunds(); + } + // modify state before calling to send + scope.balance -= amount; + // transfer funds + (bool sent,) = msg.sender.call{value: amount}(""); + if (!sent) { + revert FailedToSend(); + } + emit ScopeWithdraw(scopeName, msg.sender, amount); + } /** - @notice Emitted when a fragment is unassigned - @param owner The owner of the fragment - @param fragmentAddress The address of the fragment's contract - @param fragmentTokenId The tokenId of the fragment - @param targetAddress The address of the target's contract - @param targetTokenId The tokenId of the target + @dev See {IPatchworkProtocol-balanceOf} */ - event Unassign(address indexed owner, address fragmentAddress, uint256 fragmentTokenId, address indexed targetAddress, uint256 indexed targetTokenId); + function balanceOf(string memory scopeName) public view returns (uint256 balance) { + Scope storage scope = _mustHaveScope(scopeName); + return scope.balance; + } /** - @notice Emitted when a patch is minted - @param owner The owner of the patch - @param originalAddress The address of the original NFT's contract - @param originalTokenId The tokenId of the original NFT - @param patchAddress The address of the patch's contract - @param patchTokenId The tokenId of the patch + @dev See {IPatchworkProtocol-mint} */ - event Patch(address indexed owner, address originalAddress, uint256 originalTokenId, address indexed patchAddress, uint256 indexed patchTokenId); - + function mint(address to, address mintable, bytes calldata data) external payable returns (uint256 tokenId) { + (MintConfig memory config, string memory scopeName, Scope storage scope) = _setupMint(mintable); + if (msg.value != config.flatFee) { + revert IncorrectFeeAmount(); + } + (uint256 scopeFee, uint256 protocolFee) = _handleMintFee(scopeName, scope); + tokenId = IPatchworkMintable(mintable).mint(to, data); + emit Mint(msg.sender, scopeName, to, mintable, data, scopeFee, protocolFee); + } + /** - @notice Emitted when a new scope is claimed - @param scopeName The name of the claimed scope - @param owner The owner of the scope + @dev See {IPatchworkProtocol-mintBatch} */ - event ScopeClaim(string scopeName, address indexed owner); + function mintBatch(address to, address mintable, bytes calldata data, uint256 quantity) external payable returns (uint256[] memory tokenIds) { + (MintConfig memory config, string memory scopeName, Scope storage scope) = _setupMint(mintable); + uint256 totalFee = config.flatFee * quantity; + if (msg.value != totalFee) { + revert IncorrectFeeAmount(); + } + (uint256 scopeFee, uint256 protocolFee) = _handleMintFee(scopeName, scope); + tokenIds = IPatchworkMintable(mintable).mintBatch(to, data, quantity); + emit MintBatch(msg.sender, scopeName, to, mintable, data, quantity, scopeFee, protocolFee); + } - /** - @notice Emitted when a scope is transferred - @param scopeName The name of the transferred scope - @param from The address transferring the scope - @param to The recipient of the scope - */ - event ScopeTransfer(string scopeName, address indexed from, address indexed to); + /// Common to mints + function _setupMint(address mintable) internal view returns (MintConfig memory config, string memory scopeName, Scope storage scope) { + if (!IERC165(mintable).supportsInterface(type(IPatchworkMintable).interfaceId)) { + revert UnsupportedContract(); + } + scopeName = _getScopeNameViewOnly(mintable); + scope = _mustHaveScope(scopeName); + _mustBeWhitelisted(scopeName, scope, mintable); + config = scope.mintConfigurations[mintable]; + if (!config.active) { + revert MintNotActive(); + } + } - /** - @notice Emitted when a scope has an operator added - @param scopeName The name of the scope - @param actor The address responsible for the action - @param operator The new operator's address - */ - event ScopeAddOperator(string scopeName, address indexed actor, address indexed operator); + /// Common to mints + function _handleMintFee(string memory scopeName, Scope storage scope) internal returns (uint256 scopeFee, uint256 protocolFee) { + // Account for 100% of the message value + if (msg.value != 0) { + uint256 mintBp; + FeeConfigOverride storage feeOverride = _scopeFeeOverrides[scopeName]; + if (feeOverride.active) { + mintBp = feeOverride.mintBp; + } else { + mintBp = _protocolFeeConfig.mintBp; + } + protocolFee = msg.value * mintBp / _FEE_BASIS_DENOM; + scopeFee = msg.value - protocolFee; + _protocolBalance += protocolFee; + scope.balance += scopeFee; + } + } /** - @notice Emitted when a scope has an operator removed - @param scopeName The name of the scope - @param actor The address responsible for the action - @param operator The operator's address being removed + @dev See {IPatchworkProtocol-proposeProtocolFeeConfig} */ - event ScopeRemoveOperator(string scopeName, address indexed actor, address indexed operator); + function proposeProtocolFeeConfig(FeeConfig memory config) public onlyProtoOwnerBanker { + if (config.assignBp > _PROTOCOL_FEE_CEILING || config.mintBp > _PROTOCOL_FEE_CEILING || config.patchBp > _PROTOCOL_FEE_CEILING) { + revert InvalidFeeValue(); + } + _proposedFeeConfigs[""] = ProposedFeeConfig(config, block.timestamp, true); + emit ProtocolFeeConfigPropose(config); + } /** - @notice Emitted when a scope's rules are changed - @param scopeName The name of the scope - @param actor The address responsible for the action - @param allowUserPatch Indicates whether user patches are allowed - @param allowUserAssign Indicates whether user assignments are allowed - @param requireWhitelist Indicates whether a whitelist is required + @dev See {IPatchworkProtocol-commitProtocolFeeConfig} */ - event ScopeRuleChange(string scopeName, address indexed actor, bool allowUserPatch, bool allowUserAssign, bool requireWhitelist); + function commitProtocolFeeConfig() public onlyProtoOwnerBanker { + (FeeConfig memory config, /* bool active */) = _preCommitFeeChange(""); + _protocolFeeConfig = config; + emit ProtocolFeeConfigCommit(_protocolFeeConfig); + } /** - @notice Emitted when a scope has an address added to the whitelist - @param scopeName The name of the scope - @param actor The address responsible for the action - @param addr The address being added to the whitelist + @dev See {IPatchworkProtocol-getProtocolFeeConfig} */ - event ScopeWhitelistAdd(string scopeName, address indexed actor, address indexed addr); + function getProtocolFeeConfig() public view returns (FeeConfig memory config) { + return _protocolFeeConfig; + } /** - @notice Emitted when a scope has an address removed from the whitelist - @param scopeName The name of the scope - @param actor The address responsible for the action - @param addr The address being removed from the whitelist + @dev See {IPatchworkProtocol-proposeScopeFeeOverride} */ - event ScopeWhitelistRemove(string scopeName, address indexed actor, address indexed addr); + function proposeScopeFeeOverride(string memory scopeName, FeeConfigOverride memory config) public onlyProtoOwnerBanker { + if (config.assignBp > _PROTOCOL_FEE_CEILING || config.mintBp > _PROTOCOL_FEE_CEILING || config.patchBp > _PROTOCOL_FEE_CEILING) { + revert InvalidFeeValue(); + } + _proposedFeeConfigs[scopeName] = ProposedFeeConfig( + FeeConfig(config.mintBp, config.patchBp, config.assignBp), block.timestamp, config.active); + emit ScopeFeeOverridePropose(scopeName, config); + } /** - @notice Claim a scope - @param scopeName the name of the scope + @dev See {IPatchworkProtocol-commitScopeFeeOverride} */ - function claimScope(string calldata scopeName) public { - Scope storage s = _scopes[scopeName]; - if (s.owner != address(0)) { - revert ScopeExists(scopeName); + function commitScopeFeeOverride(string memory scopeName) public onlyProtoOwnerBanker { + (FeeConfig memory config, bool active) = _preCommitFeeChange(scopeName); + FeeConfigOverride memory feeOverride = FeeConfigOverride(config.mintBp, config.patchBp, config.assignBp, active); + if (!active) { + delete _scopeFeeOverrides[scopeName]; + } else { + _scopeFeeOverrides[scopeName] = feeOverride; } - s.owner = msg.sender; - // s.requireWhitelist = true; // better security by default - enable in future PR - emit ScopeClaim(scopeName, msg.sender); + emit ScopeFeeOverrideCommit(scopeName, feeOverride); } /** - @notice Transfer ownership of a scope - @param scopeName Name of the scope - @param newOwner Address of the new owner + @dev commits a fee change if a proposal exists and timelock is satisfied + @param scopeName "" for protocol or the scope name + @return config The proposed config + @return active The proposed active state (only applies to fee overrides) */ - function transferScopeOwnership(string calldata scopeName, address newOwner) public { - Scope storage s = _mustHaveScope(scopeName); - _mustBeOwner(s); - if (newOwner == address(0)) { - revert ScopeTransferNotAllowed(address(0)); + function _preCommitFeeChange(string memory scopeName) private returns (FeeConfig memory config, bool active) { + ProposedFeeConfig storage proposal = _proposedFeeConfigs[scopeName]; + if (proposal.timestamp == 0) { + revert NoProposedFeeSet(); + } + if (block.timestamp < proposal.timestamp + FEE_CHANGE_TIMELOCK) { + revert TimelockNotElapsed(); } - s.owner = newOwner; - emit ScopeTransfer(scopeName, msg.sender, newOwner); + config = proposal.config; + active = proposal.active; + delete _proposedFeeConfigs[scopeName]; } /** - @notice Get owner of a scope - @param scopeName Name of the scope - @return owner Address of the scope owner + @dev See {IPatchworkProtocol-getScopeFeeOverride} */ - function getScopeOwner(string calldata scopeName) public view returns (address owner) { - return _scopes[scopeName].owner; + function getScopeFeeOverride(string memory scopeName) public view returns (FeeConfigOverride memory config) { + return _scopeFeeOverrides[scopeName]; } /** - @notice Add an operator to a scope - @param scopeName Name of the scope - @param op Address of the operator + @dev See {IPatchworkProtocol-addProtocolBanker} */ - function addOperator(string calldata scopeName, address op) public { - Scope storage s = _mustHaveScope(scopeName); - _mustBeOwner(s); - s.operators[op] = true; - emit ScopeAddOperator(scopeName, msg.sender, op); + function addProtocolBanker(address addr) external onlyOwner { + _protocolBankers[addr] = true; + emit ProtocolBankerAdd(msg.sender, addr); } /** - @notice Remove an operator from a scope - @param scopeName Name of the scope - @param op Address of the operator + @dev See {IPatchworkProtocol-removeProtocolBanker} */ - function removeOperator(string calldata scopeName, address op) public { - Scope storage s = _mustHaveScope(scopeName); - _mustBeOwner(s); - s.operators[op] = false; - emit ScopeRemoveOperator(scopeName, msg.sender, op); + function removeProtocolBanker(address addr) external onlyOwner { + delete _protocolBankers[addr]; + emit ProtocolBankerRemove(msg.sender, addr); } /** - @notice Set rules for a scope - @param scopeName Name of the scope - @param allowUserPatch Boolean indicating whether user patches are allowed - @param allowUserAssign Boolean indicating whether user assignments are allowed - @param requireWhitelist Boolean indicating whether whitelist is required + @dev See {IPatchworkProtocol-withdrawFromProtocol} */ - function setScopeRules(string calldata scopeName, bool allowUserPatch, bool allowUserAssign, bool requireWhitelist) public { - Scope storage s = _mustHaveScope(scopeName); - _mustBeOwner(s); - s.allowUserPatch = allowUserPatch; - s.allowUserAssign = allowUserAssign; - s.requireWhitelist = requireWhitelist; - emit ScopeRuleChange(scopeName, msg.sender, allowUserPatch, allowUserAssign, requireWhitelist); + function withdrawFromProtocol(uint256 amount) external nonReentrant onlyProtoOwnerBanker { + if (amount > _protocolBalance) { + revert InsufficientFunds(); + } + _protocolBalance -= amount; + (bool sent,) = msg.sender.call{value: amount}(""); + if (!sent) { + revert FailedToSend(); + } + emit ProtocolWithdraw(msg.sender, amount); + } + + function balanceOfProtocol() public view returns (uint256 balance) { + return _protocolBalance; } /** - @notice Add an address to a scope's whitelist - @param scopeName Name of the scope - @param addr Address to be whitelisted + @dev See {IPatchworkProtocol-addWhitelist} */ function addWhitelist(string calldata scopeName, address addr) public { Scope storage s = _mustHaveScope(scopeName); @@ -425,9 +463,7 @@ contract PatchworkProtocol { } /** - @notice Remove an address from a scope's whitelist - @param scopeName Name of the scope - @param addr Address to be removed from the whitelist + @dev See {IPatchworkProtocol-removeWhitelist} */ function removeWhitelist(string calldata scopeName, address addr) public { Scope storage s = _mustHaveScope(scopeName); @@ -437,196 +473,243 @@ contract PatchworkProtocol { } /** - @notice Create a new patch - @param originalNFTAddress Address of the original NFT - @param originalNFTTokenId Token ID of the original NFT - @param patchAddress Address of the IPatchworkPatch to mint - @return tokenId Token ID of the newly created patch + @dev See {IPatchworkProtocol-patch} */ - function createPatch(address originalNFTAddress, uint originalNFTTokenId, address patchAddress) public returns (uint256 tokenId) { - IPatchworkPatch patch = IPatchworkPatch(patchAddress); - string memory scopeName = patch.getScopeName(); - // mint a Patch that is soulbound to the originalNFT using the contract address at patchAddress which must support Patchwork metadata + function patch(address owner, address originalAddress, uint originalTokenId, address patchAddress) external payable returns (uint256 tokenId) { + if (!IERC165(patchAddress).supportsInterface(type(IPatchworkPatch).interfaceId)) { + revert UnsupportedContract(); + } + IPatchworkPatch patch_ = IPatchworkPatch(patchAddress); + string memory scopeName = _getScopeName(patchAddress); Scope storage scope = _mustHaveScope(scopeName); _mustBeWhitelisted(scopeName, scope, patchAddress); - address tokenOwner = IERC721(originalNFTAddress).ownerOf(originalNFTTokenId); if (scope.owner == msg.sender || scope.operators[msg.sender]) { // continue - } else if (scope.allowUserPatch && msg.sender == tokenOwner) { + } else if (scope.allowUserPatch) { // continue } else { revert NotAuthorized(msg.sender); } - // limit this to one unique patch (originalNFTAddress+TokenID+patchAddress) - bytes32 _hash = keccak256(abi.encodePacked(originalNFTAddress, originalNFTTokenId, patchAddress)); - if (scope.uniquePatches[_hash]) { - revert AlreadyPatched(originalNFTAddress, originalNFTTokenId, patchAddress); + (uint256 scopeFee, uint256 protocolFee) = _handlePatchFee(scopeName, scope, patchAddress); + // limit this to one unique patch (originalAddress+TokenID+patchAddress) + bytes32 _hash = keccak256(abi.encodePacked(originalAddress, originalTokenId, patchAddress)); + if (_uniquePatches[_hash]) { + revert AlreadyPatched(originalAddress, originalTokenId, patchAddress); } - scope.uniquePatches[_hash] = true; - tokenId = patch.mintPatch(tokenOwner, originalNFTAddress, originalNFTTokenId); - emit Patch(tokenOwner, originalNFTAddress, originalNFTTokenId, patchAddress, tokenId); + _uniquePatches[_hash] = true; + tokenId = patch_.mintPatch(owner, IPatchworkPatch.PatchTarget(originalAddress, originalTokenId)); + emit Patch(owner, originalAddress, originalTokenId, patchAddress, tokenId, scopeFee, protocolFee); return tokenId; } /** - @notice Assigns an NFT relation to have an IPatchworkLiteRef form a LiteRef to a IPatchworkAssignableNFT - @param fragment The IPatchworkAssignableNFT address to assign - @param fragmentTokenId The IPatchworkAssignableNFT Token ID to assign - @param target The IPatchworkLiteRef address to hold the reference to the fragment - @param targetTokenId The IPatchworkLiteRef Token ID to hold the reference to the fragment + @dev See {IPatchworkProtocol-patchBurned} */ - function assignNFT(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) public mustNotBeFrozen(target, targetTokenId) { - address targetOwner = IERC721(target).ownerOf(targetTokenId); - uint64 ref = _doAssign(fragment, fragmentTokenId, target, targetTokenId, targetOwner); - // call addReference on the target - IPatchworkLiteRef(target).addReference(targetTokenId, ref); + function patchBurned(address originalAddress, uint originalTokenId, address patchAddress) external onlyFrom(patchAddress) { + bytes32 _hash = keccak256(abi.encodePacked(originalAddress, originalTokenId, patchAddress)); + delete _uniquePatches[_hash]; } /** - @notice Assign multiple NFT fragments to a target NFT in batch - @param fragments The array of addresses of the fragment IPatchworkAssignableNFTs - @param tokenIds The array of token IDs of the fragment IPatchworkAssignableNFTs - @param target The address of the target IPatchworkLiteRef NFT - @param targetTokenId The token ID of the target IPatchworkLiteRef NFT + @dev See {IPatchworkProtocol-patch1155} */ - function batchAssignNFT(address[] calldata fragments, uint[] calldata tokenIds, address target, uint targetTokenId) public mustNotBeFrozen(target, targetTokenId) { - if (fragments.length != tokenIds.length) { - revert BadInputLengths(); - } - address targetOwner = IERC721(target).ownerOf(targetTokenId); - uint64[] memory refs = new uint64[](fragments.length); - for (uint i = 0; i < fragments.length; i++) { - address fragment = fragments[i]; - uint256 fragmentTokenId = tokenIds[i]; - refs[i] = _doAssign(fragment, fragmentTokenId, target, targetTokenId, targetOwner); + function patch1155(address to, address originalAddress, uint originalTokenId, address originalAccount, address patchAddress) external payable returns (uint256 tokenId) { + if (!IERC165(patchAddress).supportsInterface(type(IPatchwork1155Patch).interfaceId)) { + revert UnsupportedContract(); } - IPatchworkLiteRef(target).batchAddReferences(targetTokenId, refs); - } - - /** - @notice Performs assignment of an IPatchworkAssignableNFT to an IPatchworkLiteRef - @param fragment the IPatchworkAssignableNFT's address - @param fragmentTokenId the IPatchworkAssignableNFT's tokenId - @param target the IPatchworkLiteRef target's address - @param targetTokenId the IPatchworkLiteRef target's tokenId - @param targetOwner the owner address of the target - @return uint64 literef of assignable in target - */ - function _doAssign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, address targetOwner) private mustNotBeFrozen(fragment, fragmentTokenId) returns (uint64) { - if (fragment == target && fragmentTokenId == targetTokenId) { - revert SelfAssignmentNotAllowed(fragment, fragmentTokenId); - } - IPatchworkAssignableNFT assignableNFT = IPatchworkAssignableNFT(fragment); - if (_isLocked(fragment, fragmentTokenId)) { - revert Locked(fragment, fragmentTokenId); - } - // Use the fragment's scope for permissions, target already has to have fragment registered to be assignable - string memory scopeName = assignableNFT.getScopeName(); + IPatchwork1155Patch patch_ = IPatchwork1155Patch(patchAddress); + string memory scopeName = _getScopeName(patchAddress); Scope storage scope = _mustHaveScope(scopeName); - _mustBeWhitelisted(scopeName, scope, fragment); + _mustBeWhitelisted(scopeName, scope, patchAddress); if (scope.owner == msg.sender || scope.operators[msg.sender]) { - // Fragment and target must be same owner - if (IERC721(fragment).ownerOf(fragmentTokenId) != targetOwner) { - revert NotAuthorized(msg.sender); - } - } else if (scope.allowUserAssign) { - // If allowUserAssign is set for this scope, the sender must own both fragment and target - if (IERC721(fragment).ownerOf(fragmentTokenId) != msg.sender) { - revert NotAuthorized(msg.sender); - } - if (targetOwner != msg.sender) { - revert NotAuthorized(msg.sender); - } + // continue + } else if (scope.allowUserPatch) { // continue } else { revert NotAuthorized(msg.sender); } - // reduce stack to stay under limit - uint64 ref; - { - (uint64 _ref, bool redacted) = IPatchworkLiteRef(target).getLiteReference(fragment, fragmentTokenId); - ref = _ref; - if (ref == 0) { - revert FragmentUnregistered(address(fragment)); - } - if (redacted) { - revert FragmentRedacted(address(fragment)); - } - if (scope.liteRefs[ref]) { - revert FragmentAlreadyAssignedInScope(scopeName, address(fragment), fragmentTokenId); - } + (uint256 scopeFee, uint256 protocolFee) = _handlePatchFee(scopeName, scope, patchAddress); + // limit this to one unique patch (originalAddress+TokenID+patchAddress) + bytes32 _hash = keccak256(abi.encodePacked(originalAddress, originalTokenId, originalAccount, patchAddress)); + if (_uniquePatches[_hash]) { + revert ERC1155AlreadyPatched(originalAddress, originalTokenId, originalAccount, patchAddress); } - // call assign on the fragment - assignableNFT.assign(fragmentTokenId, target, targetTokenId); - // add to our storage of scope->target assignments - scope.liteRefs[ref] = true; - emit Assign(targetOwner, fragment, fragmentTokenId, target, targetTokenId); - return ref; + _uniquePatches[_hash] = true; + tokenId = patch_.mintPatch(to, IPatchwork1155Patch.PatchTarget(originalAddress, originalTokenId, originalAccount)); + emit ERC1155Patch(to, originalAddress, originalTokenId, originalAccount, patchAddress, tokenId, scopeFee, protocolFee); + return tokenId; } /** - @notice Unassign a NFT fragment from a target NFT - @param fragment The IPatchworkAssignableNFT address of the fragment NFT - @param fragmentTokenId The IPatchworkAssignableNFT token ID of the fragment NFT + @dev See {IPatchworkProtocol-patchBurned1155} + */ + function patchBurned1155(address originalAddress, uint originalTokenId, address originalAccount, address patchAddress) external onlyFrom(patchAddress) { + bytes32 _hash = keccak256(abi.encodePacked(originalAddress, originalTokenId, originalAccount, patchAddress)); + delete _uniquePatches[_hash]; + } + + /** + @dev See {IPatchworkProtocol-patchAccount} */ - function unassignNFT(address fragment, uint fragmentTokenId) public mustNotBeFrozen(fragment, fragmentTokenId) { - IPatchworkAssignableNFT assignableNFT = IPatchworkAssignableNFT(fragment); - string memory scopeName = assignableNFT.getScopeName(); + function patchAccount(address owner, address originalAddress, address patchAddress) external payable returns (uint256 tokenId) { + if (!IERC165(patchAddress).supportsInterface(type(IPatchworkAccountPatch).interfaceId)) { + revert UnsupportedContract(); + } + IPatchworkAccountPatch patch_ = IPatchworkAccountPatch(patchAddress); + string memory scopeName = _getScopeName(patchAddress); Scope storage scope = _mustHaveScope(scopeName); + _mustBeWhitelisted(scopeName, scope, patchAddress); if (scope.owner == msg.sender || scope.operators[msg.sender]) { // continue - } else if (scope.allowUserAssign) { - // If allowUserAssign is set for this scope, the sender must own both fragment - if (IERC721(fragment).ownerOf(fragmentTokenId) != msg.sender) { - revert NotAuthorized(msg.sender); - } + } else if (scope.allowUserPatch) { // This allows any user to patch any address // continue } else { revert NotAuthorized(msg.sender); } - (address target, uint256 targetTokenId) = IPatchworkAssignableNFT(fragment).getAssignedTo(fragmentTokenId); - if (target == address(0)) { - revert FragmentNotAssigned(fragment, fragmentTokenId); + (uint256 scopeFee, uint256 protocolFee) = _handlePatchFee(scopeName, scope, patchAddress); + // limit this to one unique patch (originalAddress+patchAddress) + bytes32 _hash = keccak256(abi.encodePacked(originalAddress, patchAddress)); + if (_uniquePatches[_hash]) { + revert AccountAlreadyPatched(originalAddress, patchAddress); } - assignableNFT.unassign(fragmentTokenId); - (uint64 ref, ) = IPatchworkLiteRef(target).getLiteReference(fragment, fragmentTokenId); - if (ref == 0) { - revert FragmentUnregistered(address(fragment)); + _uniquePatches[_hash] = true; + tokenId = patch_.mintPatch(owner, originalAddress); + emit AccountPatch(owner, originalAddress, patchAddress, tokenId, scopeFee, protocolFee); + return tokenId; + } + + /** + @dev See {IPatchworkProtocol-patchBurnedAccount} + */ + function patchBurnedAccount(address originalAddress, address patchAddress) external onlyFrom(patchAddress) { + bytes32 _hash = keccak256(abi.encodePacked(originalAddress, patchAddress)); + delete _uniquePatches[_hash]; + } + + /// common to patches + function _handlePatchFee(string memory scopeName, Scope storage scope, address patchAddress) private returns (uint256 scopeFee, uint256 protocolFee) { + uint256 patchFee = scope.patchFees[patchAddress]; + if (msg.value != patchFee) { + revert IncorrectFeeAmount(); } - if (!scope.liteRefs[ref]) { - revert RefNotFoundInScope(scopeName, target, fragment, fragmentTokenId); + if (msg.value > 0) { + uint256 patchBp; + FeeConfigOverride storage feeOverride = _scopeFeeOverrides[scopeName]; + if (feeOverride.active) { + patchBp = feeOverride.patchBp; + } else { + patchBp = _protocolFeeConfig.patchBp; + } + protocolFee = msg.value * patchBp / _FEE_BASIS_DENOM; + scopeFee = msg.value - protocolFee; + _protocolBalance += protocolFee; + scope.balance += scopeFee; + } + } + + function _delegatecall(address delegate, bytes memory data) internal returns (bytes memory) { + (bool success, bytes memory returndata) = delegate.delegatecall(data); + if (!success) { + if (returndata.length == 0) revert(); + assembly { + revert(add(32, returndata), mload(returndata)) + } } - scope.liteRefs[ref] = false; - IPatchworkLiteRef(target).removeReference(targetTokenId, ref); - emit Unassign(IERC721(target).ownerOf(targetTokenId), fragment, fragmentTokenId, target, targetTokenId); + return returndata; } /** - @notice Apply transfer rules and actions of a specific token from one address to another - @param from The address of the sender - @param to The address of the receiver - @param tokenId The ID of the token to be transferred + @dev See {IPatchworkProtocol-assign} + */ + function assign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) public payable { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("assign(address,uint256,address,uint256)", fragment, fragmentTokenId, target, targetTokenId)); + } + + /** + @dev See {IPatchworkProtocol-assign} + */ + function assign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) public payable { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("assign(address,uint256,address,uint256,uint256)", fragment, fragmentTokenId, target, targetTokenId, targetMetadataId)); + } + + /** + @dev See {IPatchworkProtocol-assignBatch} + */ + function assignBatch(address[] calldata fragments, uint256[] calldata tokenIds, address target, uint256 targetTokenId) public payable { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("assignBatch(address[],uint256[],address,uint256)", fragments, tokenIds, target, targetTokenId)); + } + + /** + @dev See {IPatchworkProtocol-assignBatch} + */ + function assignBatch(address[] calldata fragments, uint256[] calldata tokenIds, address target, uint256 targetTokenId, uint256 targetMetadataId) public payable { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("assignBatch(address[],uint256[],address,uint256,uint256)", fragments, tokenIds, target, targetTokenId, targetMetadataId)); + } + + /** + @dev See {IPatchworkProtocol-unassign} + */ + function unassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) public { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("unassign(address,uint256,address,uint256)", fragment, fragmentTokenId, target, targetTokenId)); + } + + /** + @dev See {IPatchworkProtocol-unassign} + */ + function unassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) public { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("unassign(address,uint256,address,uint256,uint256)", fragment, fragmentTokenId, target, targetTokenId, targetMetadataId)); + } + + /** + @dev See {IPatchworkProtocol-unassignMulti} + */ + function unassignMulti(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) public { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("unassignMulti(address,uint256,address,uint256)", fragment, fragmentTokenId, target, targetTokenId)); + } + + /** + @dev See {IPatchworkProtocol-unassignMulti} + */ + function unassignMulti(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) public { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("unassignMulti(address,uint256,address,uint256,uint256)", fragment, fragmentTokenId, target, targetTokenId, targetMetadataId)); + } + + /** + @dev See {IPatchworkProtocol-unassignSingle} + */ + function unassignSingle(address fragment, uint256 fragmentTokenId) public { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("unassignSingle(address,uint256)", fragment, fragmentTokenId)); + } + + /** + @dev See {IPatchworkProtocol-unassignSingle} + */ + function unassignSingle(address fragment, uint256 fragmentTokenId, uint256 targetMetadataId) public { + _delegatecall(_assignerDelegate, abi.encodeWithSignature("unassignSingle(address,uint256,uint256)", fragment, fragmentTokenId, targetMetadataId)); + } + + /** + @dev See {IPatchworkProtocol-applyTransfer} */ function applyTransfer(address from, address to, uint256 tokenId) public { address nft = msg.sender; - if (IERC165(nft).supportsInterface(type(IPatchworkAssignableNFT).interfaceId)) { - IPatchworkAssignableNFT assignableNFT = IPatchworkAssignableNFT(nft); - (address addr,) = assignableNFT.getAssignedTo(tokenId); + if (IERC165(nft).supportsInterface(type(IPatchworkSingleAssignable).interfaceId)) { + IPatchworkSingleAssignable assignable = IPatchworkSingleAssignable(nft); + (address addr,) = assignable.getAssignedTo(tokenId); if (addr != address(0)) { revert TransferBlockedByAssignment(nft, tokenId); } } if (IERC165(nft).supportsInterface(type(IPatchworkPatch).interfaceId)) { - revert SoulboundTransferNotAllowed(nft, tokenId); + revert TransferNotAllowed(nft, tokenId); } - if (IERC165(nft).supportsInterface(type(IPatchworkNFT).interfaceId)) { - if (IPatchworkNFT(nft).locked(tokenId)) { + if (IERC165(nft).supportsInterface(type(IPatchwork721).interfaceId)) { + if (IPatchwork721(nft).locked(tokenId)) { revert Locked(nft, tokenId); } } if (IERC165(nft).supportsInterface(type(IPatchworkLiteRef).interfaceId)) { - IPatchworkLiteRef liteRefNFT = IPatchworkLiteRef(nft); - (address[] memory addresses, uint256[] memory tokenIds) = liteRefNFT.loadAllReferences(tokenId); + (address[] memory addresses, uint256[] memory tokenIds) = IPatchworkLiteRef(nft).loadAllStaticReferences(tokenId); for (uint i = 0; i < addresses.length; i++) { if (addresses[i] != address(0)) { _applyAssignedTransfer(addresses[i], from, to, tokenIds[i], nft, tokenId); @@ -635,20 +718,19 @@ contract PatchworkProtocol { } } - function _applyAssignedTransfer(address nft, address from, address to, uint256 tokenId, address assignedToNFT_, uint256 assignedToTokenId_) private { - if (!IERC165(nft).supportsInterface(type(IPatchworkAssignableNFT).interfaceId)) { + function _applyAssignedTransfer(address nft, address from, address to, uint256 tokenId, address assignedTo_, uint256 assignedToTokenId_) private { + if (!IERC165(nft).supportsInterface(type(IPatchworkSingleAssignable).interfaceId)) { revert NotPatchworkAssignable(nft); } - (address assignedToNFT, uint256 assignedToTokenId) = IPatchworkAssignableNFT(nft).getAssignedTo(tokenId); + (address assignedTo, uint256 assignedToTokenId) = IPatchworkSingleAssignable(nft).getAssignedTo(tokenId); // 2-way Check the assignment to prevent spoofing - if (assignedToNFT_ != assignedToNFT || assignedToTokenId_ != assignedToTokenId) { - revert DataIntegrityError(assignedToNFT_, assignedToTokenId_, assignedToNFT, assignedToTokenId); + if (assignedTo_ != assignedTo || assignedToTokenId_ != assignedToTokenId) { + revert DataIntegrityError(assignedTo_, assignedToTokenId_, assignedTo, assignedToTokenId); } - IPatchworkAssignableNFT(nft).onAssignedTransfer(from, to, tokenId); + IPatchworkSingleAssignable(nft).onAssignedTransfer(from, to, tokenId); if (IERC165(nft).supportsInterface(type(IPatchworkLiteRef).interfaceId)) { address nft_ = nft; // local variable prevents optimizer stack issue in v0.8.18 - IPatchworkLiteRef liteRefNFT = IPatchworkLiteRef(nft); - (address[] memory addresses, uint256[] memory tokenIds) = liteRefNFT.loadAllReferences(tokenId); + (address[] memory addresses, uint256[] memory tokenIds) = IPatchworkLiteRef(nft).loadAllStaticReferences(tokenId); for (uint i = 0; i < addresses.length; i++) { if (addresses[i] != address(0)) { _applyAssignedTransfer(addresses[i], from, to, tokenIds[i], nft_, tokenId); @@ -658,50 +740,50 @@ contract PatchworkProtocol { } /** - @notice Update the ownership tree of a specific Patchwork NFT - @param nft The address of the Patchwork NFT - @param tokenId The ID of the token whose ownership tree needs to be updated - */ - function updateOwnershipTree(address nft, uint256 tokenId) public { - if (IERC165(nft).supportsInterface(type(IPatchworkLiteRef).interfaceId)) { - IPatchworkLiteRef liteRefNFT = IPatchworkLiteRef(nft); - (address[] memory addresses, uint256[] memory tokenIds) = liteRefNFT.loadAllReferences(tokenId); + @dev See {IPatchworkProtocol-updateOwnershipTree} + */ + function updateOwnershipTree(address addr, uint256 tokenId) public { + if (IERC165(addr).supportsInterface(type(IPatchworkLiteRef).interfaceId)) { + (address[] memory addresses, uint256[] memory tokenIds) = IPatchworkLiteRef(addr).loadAllStaticReferences(tokenId); for (uint i = 0; i < addresses.length; i++) { if (addresses[i] != address(0)) { updateOwnershipTree(addresses[i], tokenIds[i]); } } } - if (IERC165(nft).supportsInterface(type(IPatchworkAssignableNFT).interfaceId)) { - IPatchworkAssignableNFT(nft).updateOwnership(tokenId); - } else if (IERC165(nft).supportsInterface(type(IPatchworkPatch).interfaceId)) { - IPatchworkPatch(nft).updateOwnership(tokenId); + if (IERC165(addr).supportsInterface(type(IPatchworkSingleAssignable).interfaceId)) { + IPatchworkSingleAssignable(addr).updateOwnership(tokenId); + } else if (IERC165(addr).supportsInterface(type(IPatchworkPatch).interfaceId)) { + IPatchworkPatch(addr).updateOwnership(tokenId); } } /** - @notice Requires that scopeName is present - @dev will revert with ScopeDoesNotExist if not present - @return scope the scope + @dev See {IPatchworkProtocol-proposeAssignerDelegate} */ - function _mustHaveScope(string memory scopeName) private view returns (Scope storage scope) { - scope = _scopes[scopeName]; - if (scope.owner == address(0)) { - revert ScopeDoesNotExist(scopeName); + function proposeAssignerDelegate(address addr) public onlyOwner { + if (addr == address(0)) { + // effectively a cancel + _proposedAssignerDelegate = ProposedAssignerDelegate(0, address(0)); + } else { + _proposedAssignerDelegate = ProposedAssignerDelegate(block.timestamp, addr); } + emit AssignerDelegatePropose(addr); } /** - @notice Requires that addr is whitelisted if whitelisting is enabled - @dev will revert with NotWhitelisted if whitelisting is enabled and address is not whitelisted - @param scopeName the name of the scope - @param scope the scope - @param addr the address to check + @dev See {IPatchworkProtocol-commitAssignerDelegate} */ - function _mustBeWhitelisted(string memory scopeName, Scope storage scope, address addr) private view { - if (scope.requireWhitelist && !scope.whitelist[addr]) { - revert NotWhitelisted(scopeName, addr); + function commitAssignerDelegate() public onlyOwner { + if (_proposedAssignerDelegate.timestamp == 0) { + revert NoDelegateProposed(); } + if (block.timestamp < _proposedAssignerDelegate.timestamp + CONTRACT_UPGRADE_TIMELOCK) { + revert TimelockNotElapsed(); + } + _assignerDelegate = _proposedAssignerDelegate.addr; + _proposedAssignerDelegate = ProposedAssignerDelegate(0, address(0)); + emit AssignerDelegateCommit(_assignerDelegate); } /** @@ -727,51 +809,31 @@ contract PatchworkProtocol { } /** - @notice Requires that nft is not frozen - @dev will revert with Frozen if nft is frozen - @param nft the address of nft - @param tokenId the tokenId of nft + @notice Memoized view-only wrapper for IPatchworkScoped.getScopeName() + @dev required to get optimized result from view-only functions, does not memoize result if not already memoized + @param addr Address to check + @return scopeName return value of IPatchworkScoped(addr).getScopeName() */ - modifier mustNotBeFrozen(address nft, uint256 tokenId) { - if (_isFrozen(nft, tokenId)) { - revert Frozen(nft, tokenId); + function _getScopeNameViewOnly(address addr) private view returns (string memory scopeName) { + scopeName = _scopeNameCache[addr]; + if (bytes(scopeName).length == 0) { + scopeName = IPatchworkScoped(addr).getScopeName(); } - _; } - /** - @notice Determines if nft is frozen using ownership hierarchy - @param nft the address of nft - @param tokenId the tokenId of nft - @return frozen if the nft or an owner up the tree is frozen - */ - function _isFrozen(address nft, uint256 tokenId) private view returns (bool frozen) { - if (IERC165(nft).supportsInterface(type(IPatchworkNFT).interfaceId)) { - if (IPatchworkNFT(nft).frozen(tokenId)) { - return true; - } - if (IERC165(nft).supportsInterface(type(IPatchworkAssignableNFT).interfaceId)) { - (address assignedAddr, uint256 assignedTokenId) = IPatchworkAssignableNFT(nft).getAssignedTo(tokenId); - if (assignedAddr != address(0)) { - return _isFrozen(assignedAddr, assignedTokenId); - } - } + /// Only protocol owner or protocol banker + modifier onlyProtoOwnerBanker() { + if (msg.sender != owner() && _protocolBankers[msg.sender] == false) { + revert NotAuthorized(msg.sender); } - return false; + _; } - /** - @notice Determines if nft is locked - @param nft the address of nft - @param tokenId the tokenId of nft - @return locked if the nft is locked - */ - function _isLocked(address nft, uint256 tokenId) private view returns (bool locked) { - if (IERC165(nft).supportsInterface(type(IPatchworkNFT).interfaceId)) { - if (IPatchworkNFT(nft).locked(tokenId)) { - return true; - } + /// Only msg.sender from addr + modifier onlyFrom(address addr) { + if (msg.sender != addr) { + revert NotAuthorized(msg.sender); } - return false; + _; } } \ No newline at end of file diff --git a/src/PatchworkProtocolAssigner.sol b/src/PatchworkProtocolAssigner.sol new file mode 100644 index 0000000..9d910ce --- /dev/null +++ b/src/PatchworkProtocolAssigner.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** + + ____ __ __ __ + / __ \____ _/ /______/ /_ _ ______ _____/ /__ + / /_/ / __ `/ __/ ___/ __ \ | /| / / __ \/ ___/ //_/ + / ____/ /_/ / /_/ /__/ / / / |/ |/ / /_/ / / / ,< +/_/ ___\__,_/\__/\___/_/ /_/|__/|__/\____/_/ /_/|_| + / __ \_________ / /_____ _________ / / + / /_/ / ___/ __ \/ __/ __ \/ ___/ __ \/ / + / ____/ / / /_/ / /_/ /_/ / /__/ /_/ / / +/_/ /_/ \____/\__/\____/\___/\____/_/ + +Assigner Module + +*/ + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "./PatchworkProtocolCommon.sol"; +import "./interfaces/IPatchwork721.sol"; +import "./interfaces/IPatchworkSingleAssignable.sol"; +import "./interfaces/IPatchworkMultiAssignable.sol"; +import "./interfaces/IPatchworkLiteRef.sol"; + +/** +@title Patchwork Protocol Assigner Module +@author Runic Labs, Inc +*/ +contract PatchworkProtocolAssigner is PatchworkProtocolCommon { + + /// The denominator for fee basis points + uint256 private constant _FEE_BASIS_DENOM = 10000; + + constructor(address owner_) PatchworkProtocolCommon(owner_) {} + + /** + @dev common to assigns + @dev fees are processed per-assignment + */ + function _handleAssignFee(uint256 value, string memory scopeName, IPatchworkProtocol.Scope storage scope, address fragmentAddress) private returns (uint256 scopeFee, uint256 protocolFee, uint256 valueRemaining) { + uint256 assignFee = scope.assignFees[fragmentAddress]; + if (value < assignFee) { + revert IPatchworkProtocol.IncorrectFeeAmount(); + } + if (value > 0) { + uint256 assignBp; + IPatchworkProtocol.FeeConfigOverride storage feeOverride = _scopeFeeOverrides[scopeName]; + if (feeOverride.active) { + assignBp = feeOverride.assignBp; + } else { + assignBp = _protocolFeeConfig.assignBp; + } + protocolFee = assignFee * assignBp / _FEE_BASIS_DENOM; + scopeFee = assignFee - protocolFee; + _protocolBalance += protocolFee; + scope.balance += scopeFee; + valueRemaining = value - assignFee; + } + } + + /** + @dev See {IPatchworkProtocol-assign} + */ + function assign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) public payable mustNotBeFrozen(target, targetTokenId) { + address targetOwner = IERC721(target).ownerOf(targetTokenId); + (uint64 ref, uint256 valueRemaining) = _doAssign(msg.value, fragment, fragmentTokenId, target, targetTokenId, targetOwner); + if (valueRemaining > 0) { + revert IPatchworkProtocol.IncorrectFeeAmount(); + } + IPatchworkLiteRef(target).addReference(targetTokenId, ref); + } + + /** + @dev See {IPatchworkProtocol-assign} + */ + function assign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) public payable mustNotBeFrozen(target, targetTokenId) { + address targetOwner = IERC721(target).ownerOf(targetTokenId); + (uint64 ref, uint256 valueRemaining) = _doAssign(msg.value, fragment, fragmentTokenId, target, targetTokenId, targetOwner); + if (valueRemaining > 0) { + revert IPatchworkProtocol.IncorrectFeeAmount(); + } + IPatchworkLiteRef(target).addReference(targetTokenId, ref, targetMetadataId); + } + + /** + @dev See {IPatchworkProtocol-assignBatch} + */ + function assignBatch(address[] calldata fragments, uint256[] calldata tokenIds, address target, uint256 targetTokenId) public payable mustNotBeFrozen(target, targetTokenId) { + (uint64[] memory refs, ) = _batchAssignCommon(fragments, tokenIds, target, targetTokenId); + IPatchworkLiteRef(target).addReferenceBatch(targetTokenId, refs); + } + + /** + @dev See {IPatchworkProtocol-assignBatch} + */ + function assignBatch(address[] calldata fragments, uint256[] calldata tokenIds, address target, uint256 targetTokenId, uint256 targetMetadataId) public payable mustNotBeFrozen(target, targetTokenId) { + (uint64[] memory refs, ) = _batchAssignCommon(fragments, tokenIds, target, targetTokenId); + IPatchworkLiteRef(target).addReferenceBatch(targetTokenId, refs, targetMetadataId); + } + + /** + @dev Common function to handle the batch assignments. + */ + function _batchAssignCommon(address[] calldata fragments, uint256[] calldata tokenIds, address target, uint256 targetTokenId) private returns (uint64[] memory refs, address targetOwner) { + if (fragments.length != tokenIds.length) { + revert IPatchworkProtocol.BadInputLengths(); + } + targetOwner = IERC721(target).ownerOf(targetTokenId); + refs = new uint64[](fragments.length); + uint256 value = msg.value; + for (uint i = 0; i < fragments.length; i++) { + address fragment = fragments[i]; + uint256 fragmentTokenId = tokenIds[i]; + (refs[i], value) = _doAssign(value, fragment, fragmentTokenId, target, targetTokenId, targetOwner); + } + // If the correct fee amount is provided there should be no remainder after all assignments are processed + if (value > 0) { + revert IPatchworkProtocol.IncorrectFeeAmount(); + } + } + + /** + @notice Performs assignment of an IPatchworkAssignable to an IPatchworkLiteRef + @param value the remaining message value after any previous assignments in this tx + @param fragment the IPatchworkAssignable's address + @param fragmentTokenId the IPatchworkAssignable's tokenId + @param target the IPatchworkLiteRef target's address + @param targetTokenId the IPatchworkLiteRef target's tokenId + @param targetOwner the owner address of the target + @return ref literef of assignable in target + @return valueRemaining message value remaining after fee + */ + function _doAssign(uint256 value, address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, address targetOwner) private mustNotBeFrozen(fragment, fragmentTokenId) returns (uint64 ref, uint256 valueRemaining) { + if (fragment == target && fragmentTokenId == targetTokenId) { + revert IPatchworkProtocol.SelfAssignmentNotAllowed(fragment, fragmentTokenId); + } + uint256 scopeFee; + uint256 protocolFee; + // Use the target's scope for general permission and check the fragment for detailed permissions + (scopeFee, protocolFee, valueRemaining) = _doAssignPermissionsAndFees(value, fragment, fragmentTokenId, target, targetTokenId, targetOwner); + // Handle storage and duplicate checks + ref = _doAssignStorageAndDupes(fragment, fragmentTokenId, target, targetTokenId); + // these two end up beyond stack depth on some compiler settings. + emit IPatchworkProtocol.Assign(targetOwner, fragment, fragmentTokenId, target, targetTokenId, scopeFee, protocolFee); + return (ref, valueRemaining); + } + + /** + @notice Handles assignment permissions and fees + @param value the remaining message value after any previous assignments in this tx + @param fragment the IPatchworkAssignable's address + @param fragmentTokenId the IPatchworkAssignable's tokenId + @param target the IPatchworkLiteRef target's address + @param targetTokenId the IPatchworkLiteRef target's tokenId + @param targetOwner the owner address of the target + @return scopeFee the scope fee taken + @return protocolFee the protocol fee taken + @return valueRemaining the remaining message value after fees taken + */ + function _doAssignPermissionsAndFees(uint256 value, address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, address targetOwner) private returns (uint256 scopeFee, uint256 protocolFee, uint256 valueRemaining) { + string memory targetScopeName = _getScopeName(target); + if (!IPatchworkAssignable(fragment).allowAssignment(fragmentTokenId, target, targetTokenId, targetOwner, msg.sender, targetScopeName)) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + IPatchworkProtocol.Scope storage targetScope = _mustHaveScope(targetScopeName); + _mustBeWhitelisted(targetScopeName, targetScope, target); + if (targetScope.owner == msg.sender || targetScope.operators[msg.sender]) { + // all good + } else if (targetScope.allowUserAssign) { + // msg.sender must own the target + if (targetOwner != msg.sender) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + } else { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + if (_isLocked(fragment, fragmentTokenId)) { + revert IPatchworkProtocol.Locked(fragment, fragmentTokenId); + } + // Whitelist check, these variables do not need to stay in the function level stack + string memory fragmentScopeName = _getScopeName(fragment); + IPatchworkProtocol.Scope storage fragmentScope = _mustHaveScope(fragmentScopeName); + _mustBeWhitelisted(fragmentScopeName, fragmentScope, fragment); + (scopeFee, protocolFee, valueRemaining) = _handleAssignFee(value, fragmentScopeName, fragmentScope, fragment); + } + + /** + @notice Handles assignment storage and duplicate checks + @param fragment the IPatchworkAssignable's address + @param fragmentTokenId the IPatchworkAssignable's tokenId + @param target the IPatchworkLiteRef target's address + @param targetTokenId the IPatchworkLiteRef target's tokenId + */ + function _doAssignStorageAndDupes(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) private returns (uint64 ref) { + bool redacted; + (ref, redacted) = IPatchworkLiteRef(target).getLiteReference(fragment, fragmentTokenId); + if (redacted) { + revert IPatchworkProtocol.FragmentRedacted(address(fragment)); + } + if (ref == 0) { + revert IPatchworkProtocol.FragmentUnregistered(address(fragment)); + } + // targetRef is a compound key (targetAddr+targetTokenID+ref) - blocks duplicate assignments + bytes32 targetRef = keccak256(abi.encodePacked(target, targetTokenId, ref)); + if (_liteRefs[targetRef]) { + revert IPatchworkProtocol.FragmentAlreadyAssigned(address(fragment), fragmentTokenId); + } + // add to our storage of assignments + _liteRefs[targetRef] = true; + // call assign on the fragment + IPatchworkAssignable(fragment).assign(fragmentTokenId, target, targetTokenId); + } + + /** + @dev See {IPatchworkProtocol-unassign} + */ + function unassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) public mustNotBeFrozen(target, targetTokenId) { + _unassign(fragment, fragmentTokenId, target, targetTokenId, false, 0); + } + + /** + @dev See {IPatchworkProtocol-unassign} + */ + function unassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) public mustNotBeFrozen(target, targetTokenId) { + _unassign(fragment, fragmentTokenId, target, targetTokenId, true, targetMetadataId); + } + + /** + @dev Common function to handle unassignments. + */ + function _unassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, bool isDirect, uint256 targetMetadataId) private { + if (IERC165(fragment).supportsInterface(type(IPatchworkMultiAssignable).interfaceId)) { + if (isDirect) { + unassignMulti(fragment, fragmentTokenId, target, targetTokenId, targetMetadataId); + } else { + unassignMulti(fragment, fragmentTokenId, target, targetTokenId); + } + } else if (IERC165(fragment).supportsInterface(type(IPatchworkSingleAssignable).interfaceId)) { + (address _target, uint256 _targetTokenId) = IPatchworkSingleAssignable(fragment).getAssignedTo(fragmentTokenId); + if (target != _target || _targetTokenId != targetTokenId) { + revert IPatchworkProtocol.FragmentNotAssignedToTarget(fragment, fragmentTokenId, target, targetTokenId); + } + if (isDirect) { + unassignSingle(fragment, fragmentTokenId, targetMetadataId); + } else { + unassignSingle(fragment, fragmentTokenId); + } + } else { + revert IPatchworkProtocol.UnsupportedContract(); + } + } + + /** + @dev See {IPatchworkProtocol-unassignMulti} + */ + function unassignMulti(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) public mustNotBeFrozen(target, targetTokenId) { + _unassignMultiCommon(fragment, fragmentTokenId, target, targetTokenId, false, 0); + } + + /** + @dev See {IPatchworkProtocol-unassignMulti} + */ + function unassignMulti(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) public mustNotBeFrozen(target, targetTokenId) { + _unassignMultiCommon(fragment, fragmentTokenId, target, targetTokenId, true, targetMetadataId); + } + + /** + @dev Common function to handle the unassignment of multi assignables. + */ + function _unassignMultiCommon(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, bool isDirect, uint256 targetMetadataId) private { + IPatchworkMultiAssignable assignable = IPatchworkMultiAssignable(fragment); + if (!assignable.isAssignedTo(fragmentTokenId, target, targetTokenId)) { + revert IPatchworkProtocol.FragmentNotAssignedToTarget(fragment, fragmentTokenId, target, targetTokenId); + } + string memory scopeName = _getScopeName(target); + _doUnassign(fragment, fragmentTokenId, target, targetTokenId, isDirect, targetMetadataId, scopeName); + assignable.unassign(fragmentTokenId, target, targetTokenId); + } + + /** + @dev See {IPatchworkProtocol-unassignSingle} + */ + function unassignSingle(address fragment, uint256 fragmentTokenId) public mustNotBeFrozen(fragment, fragmentTokenId) { + _unassignSingleCommon(fragment, fragmentTokenId, false, 0); + } + + /** + @dev See {IPatchworkProtocol-unassignSingle} + */ + function unassignSingle(address fragment, uint256 fragmentTokenId, uint256 targetMetadataId) public mustNotBeFrozen(fragment, fragmentTokenId) { + _unassignSingleCommon(fragment, fragmentTokenId, true, targetMetadataId); + } + + /** + @dev Common function to handle the unassignment of single assignables. + */ + function _unassignSingleCommon(address fragment, uint256 fragmentTokenId, bool isDirect, uint256 targetMetadataId) private { + IPatchworkSingleAssignable assignable = IPatchworkSingleAssignable(fragment); + (address target, uint256 targetTokenId) = assignable.getAssignedTo(fragmentTokenId); + if (target == address(0)) { + revert IPatchworkProtocol.FragmentNotAssigned(fragment, fragmentTokenId); + } + string memory scopeName = _getScopeName(target); + _doUnassign(fragment, fragmentTokenId, target, targetTokenId, isDirect, targetMetadataId, scopeName); + assignable.unassign(fragmentTokenId); + } + + /** + @notice Performs unassignment of an IPatchworkAssignable to an IPatchworkLiteRef + @param fragment the IPatchworkAssignable's address + @param fragmentTokenId the IPatchworkAssignable's tokenId + @param target the IPatchworkLiteRef target's address + @param targetTokenId the IPatchworkLiteRef target's tokenId + @param direct If this is calling the direct function + @param targetMetadataId the metadataId to use on the target + @param scopeName the name of the target's scope + */ + function _doUnassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, bool direct, uint256 targetMetadataId, string memory scopeName) private { + IPatchworkProtocol.Scope storage scope = _mustHaveScope(scopeName); + if (scope.owner == msg.sender || scope.operators[msg.sender]) { + // continue + } else if (scope.allowUserAssign) { + if (IERC721(target).ownerOf(targetTokenId) != msg.sender) { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + // continue + } else { + revert IPatchworkProtocol.NotAuthorized(msg.sender); + } + (uint64 ref, ) = IPatchworkLiteRef(target).getLiteReference(fragment, fragmentTokenId); + if (ref == 0) { + revert IPatchworkProtocol.FragmentUnregistered(address(fragment)); + } + bytes32 targetRef = keccak256(abi.encodePacked(target, targetTokenId, ref)); + if (!_liteRefs[targetRef]) { + revert IPatchworkProtocol.RefNotFound(target, fragment, fragmentTokenId); + } + delete _liteRefs[targetRef]; + if (direct) { + IPatchworkLiteRef(target).removeReference(targetTokenId, ref, targetMetadataId); + } else { + IPatchworkLiteRef(target).removeReference(targetTokenId, ref); + } + emit IPatchworkProtocol.Unassign(IERC721(fragment).ownerOf(fragmentTokenId), fragment, fragmentTokenId, target, targetTokenId); + } + + /** + @notice Requires that nft is not frozen + @dev will revert with Frozen if nft is frozen + @param nft the address of nft + @param tokenId the tokenId of nft + */ + modifier mustNotBeFrozen(address nft, uint256 tokenId) { + if (_isFrozen(nft, tokenId)) { + revert IPatchworkProtocol.Frozen(nft, tokenId); + } + _; + } + + /** + @notice Determines if nft is frozen using ownership hierarchy + @param nft the address of nft + @param tokenId the tokenId of nft + @return frozen if the nft or an owner up the tree is frozen + */ + function _isFrozen(address nft, uint256 tokenId) private view returns (bool frozen) { + if (IERC165(nft).supportsInterface(type(IPatchwork721).interfaceId)) { + if (IPatchwork721(nft).frozen(tokenId)) { + return true; + } + if (IERC165(nft).supportsInterface(type(IPatchworkSingleAssignable).interfaceId)) { + (address assignedAddr, uint256 assignedTokenId) = IPatchworkSingleAssignable(nft).getAssignedTo(tokenId); + if (assignedAddr != address(0)) { + return _isFrozen(assignedAddr, assignedTokenId); + } + } + } + return false; + } + + /** + @notice Determines if nft is locked + @param nft the address of nft + @param tokenId the tokenId of nft + @return locked if the nft is locked + */ + function _isLocked(address nft, uint256 tokenId) private view returns (bool locked) { + if (IERC165(nft).supportsInterface(type(IPatchwork721).interfaceId)) { + if (IPatchwork721(nft).locked(tokenId)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/PatchworkProtocolCommon.sol b/src/PatchworkProtocolCommon.sol new file mode 100644 index 0000000..915f811 --- /dev/null +++ b/src/PatchworkProtocolCommon.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** + + ____ __ __ __ + / __ \____ _/ /______/ /_ _ ______ _____/ /__ + / /_/ / __ `/ __/ ___/ __ \ | /| / / __ \/ ___/ //_/ + / ____/ /_/ / /_/ /__/ / / / |/ |/ / /_/ / / / ,< +/_/ ___\__,_/\__/\___/_/ /_/|__/|__/\____/_/ /_/|_| + / __ \_________ / /_____ _________ / / + / /_/ / ___/ __ \/ __/ __ \/ ___/ __ \/ / + / ____/ / / /_/ / /_/ /_/ / /__/ /_/ / / +/_/ /_/ \____/\__/\____/\___/\____/_/ + +Storage layout and common functions + +*/ + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "./interfaces/IPatchworkProtocol.sol"; +import "./interfaces/IPatchworkScoped.sol"; + +/** +@title Patchwork Protocol Storage layout and common functions +@author Runic Labs, Inc +*/ +contract PatchworkProtocolCommon is Ownable, ReentrancyGuard{ + + constructor(address owner_) Ownable(owner_) {} + + /// Scopes + mapping(string => IPatchworkProtocol.Scope) internal _scopes; + + /** + @notice unique references + @dev A hash of target + targetTokenId + literef provides uniqueness + */ + mapping(bytes32 => bool) internal _liteRefs; + + /** + @notice unique patches + @dev Hash of the patch mapped to a boolean indicating its uniqueness + */ + mapping(bytes32 => bool) internal _uniquePatches; + + /// Balance of the protocol + uint256 internal _protocolBalance; + + /** + @notice protocol bankers + @dev Map of addresses authorized to set fees and withdraw funds for the protocol + @dev Does not allow for scope balance withdrawl + */ + mapping(address => bool) internal _protocolBankers; + + /// Current protocol fee configuration + IPatchworkProtocol.FeeConfig internal _protocolFeeConfig; + + /// Proposed protocol fee configuration + mapping(string => IPatchworkProtocol.ProposedFeeConfig) internal _proposedFeeConfigs; + + /// scope-based fee overrides + mapping(string => IPatchworkProtocol.FeeConfigOverride) internal _scopeFeeOverrides; + + /// Scope name cache + mapping(address => string) internal _scopeNameCache; + + /// Proposed assigner delegate + IPatchworkProtocol.ProposedAssignerDelegate internal _proposedAssignerDelegate; + + /// Assigner module + address internal _assignerDelegate; + + /** + @notice Memoizing wrapper for IPatchworkScoped.getScopeName() + @param addr Address to check + @return scopeName return value of IPatchworkScoped(addr).getScopeName() + */ + function _getScopeName(address addr) internal returns (string memory scopeName) { + scopeName = _scopeNameCache[addr]; + if (bytes(scopeName).length == 0) { + scopeName = IPatchworkScoped(addr).getScopeName(); + _scopeNameCache[addr] = scopeName; + } + } + + /** + @notice Requires that scopeName is present + @dev will revert with ScopeDoesNotExist if not present + @return scope the scope + */ + function _mustHaveScope(string memory scopeName) internal view returns (IPatchworkProtocol.Scope storage scope) { + scope = _scopes[scopeName]; + if (scope.owner == address(0)) { + revert IPatchworkProtocol.ScopeDoesNotExist(scopeName); + } + } + + /** + @notice Requires that addr is whitelisted if whitelisting is enabled + @dev will revert with NotWhitelisted if whitelisting is enabled and address is not whitelisted + @param scopeName the name of the scope + @param scope the scope + @param addr the address to check + */ + function _mustBeWhitelisted(string memory scopeName, IPatchworkProtocol.Scope storage scope, address addr) internal view { + if (scope.requireWhitelist && !scope.whitelist[addr]) { + revert IPatchworkProtocol.NotWhitelisted(scopeName, addr); + } + } +} \ No newline at end of file diff --git a/src/PatchworkUtils.sol b/src/PatchworkUtils.sol index 0a0c8bd..83ef7ad 100644 --- a/src/PatchworkUtils.sol +++ b/src/PatchworkUtils.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; /** @title Patchwork Contract Utilities @@ -20,7 +20,7 @@ library PatchworkUtils { } /** - @notice Converts uint64 raw data to a 16 character string + @notice Converts uint128 raw data to a 16 character string @param raw the raw data @return out the string */ @@ -34,7 +34,7 @@ library PatchworkUtils { } /** - @notice Converts uint64 raw data to a 32 character string + @notice Converts uint256 raw data to a 32 character string @param raw the raw data @return out the string */ @@ -67,4 +67,18 @@ library PatchworkUtils { } out = string(trimmedByteArray); } + + /** + @notice Converts a uint16 into a 2-byte array + @param input the uint16 + @return bytes the array + */ + function convertUint16ToBytes(uint16 input) public pure returns (bytes memory) { + // Extract the higher and lower bytes + bytes1 high = bytes1(uint8(input >> 8)); + bytes1 low = bytes1(uint8(input & 0xFF)); + + // Return the two bytes as a dynamic bytes array + return abi.encodePacked(high, low); + } } \ No newline at end of file diff --git a/src/IERC4906.sol b/src/interfaces/IERC4906.sol similarity index 80% rename from src/IERC4906.sol rename to src/interfaces/IERC4906.sol index 002be59..747153b 100644 --- a/src/IERC4906.sol +++ b/src/interfaces/IERC4906.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.23; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; @@ -9,10 +9,10 @@ interface IERC4906 is IERC165, IERC721 { /// @dev This event emits when the metadata of a token is changed. /// So that the third-party platforms such as NFT market could /// timely update the images and related attributes of the NFT. - event MetadataUpdate(uint256 _tokenId); + event MetadataUpdate(uint256 indexed _tokenId); /// @dev This event emits when the metadata of a range of tokens is changed. /// So that the third-party platforms such as NFT market could /// timely update the images and related attributes of the NFTs. - event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + event BatchMetadataUpdate(uint256 indexed _fromTokenId, uint256 indexed _toTokenId); } \ No newline at end of file diff --git a/src/IERC5192.sol b/src/interfaces/IERC5192.sol similarity index 87% rename from src/IERC5192.sol rename to src/interfaces/IERC5192.sol index 2d49971..3982143 100644 --- a/src/IERC5192.sol +++ b/src/interfaces/IERC5192.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.23; interface IERC5192 { /// @notice Emitted when the locking status is changed to locked. /// @dev If a token is minted and the status is locked, this event should be emitted. /// @param tokenId The identifier for a token. - event Locked(uint256 tokenId); + event Locked(uint256 indexed tokenId); /// @notice Emitted when the locking status is changed to unlocked. /// @dev If a token is minted and the status is unlocked, this event should be emitted. /// @param tokenId The identifier for a token. - event Unlocked(uint256 tokenId); + event Unlocked(uint256 indexed tokenId); /// @notice Returns the locking status of an Soulbound Token /// @dev SBTs assigned to zero address are considered invalid, and queries diff --git a/src/interfaces/IPatchwork1155Patch.sol b/src/interfaces/IPatchwork1155Patch.sol new file mode 100644 index 0000000..3e49ed9 --- /dev/null +++ b/src/interfaces/IPatchwork1155Patch.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IPatchworkScoped.sol"; + +/** +@title Patchwork Protocol 1155 Patch Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork patch standard +*/ +interface IPatchwork1155Patch is IPatchworkScoped { + /// @dev A canonical path to an 1155 patched target + struct PatchTarget { + address addr; // The address of the 1155 + uint256 tokenId; // The tokenId of the 1155 + address account; // The account for the 1155 + } + + /** + @notice Creates a new token for the owner, representing a patch + @param to Address of the owner of the patch token + @param target Path to an 1155 to patch + @return tokenId ID of the newly minted token + */ + function mintPatch(address to, PatchTarget memory target) external payable returns (uint256 tokenId); +} + +/** +@title Patchwork Protocol Reversible 1155 Patch Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork patch standard with reverse lookup +*/ +interface IPatchworkReversible1155Patch is IPatchwork1155Patch { + /** + @notice Returns the token ID (if it exists) for an 1155 that may have been patched + @dev Requires reverse storage enabled + @param target The 1155 target that was patched + @return tokenId token ID of the patch + */ + function getTokenIdByTarget(PatchTarget memory target) external returns (uint256 tokenId); +} \ No newline at end of file diff --git a/src/interfaces/IPatchwork721.sol b/src/interfaces/IPatchwork721.sol new file mode 100644 index 0000000..3409856 --- /dev/null +++ b/src/interfaces/IPatchwork721.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "./IERC5192.sol"; +import "./IPatchworkScoped.sol"; + +/** +@title Patchwork Protocol Interface Metadata +@author Runic Labs, Inc +@notice Metadata for IPatchwork721 and related contract interfaces +*/ +interface IPatchworkMetadata { + /** + @notice Enumeration of possible field data types. + @dev This defines the various basic data types for the fields. + */ + enum FieldType { + BOOLEAN, ///< A Boolean type (true or false). + INT8, ///< An 8-bit signed integer. + INT16, ///< A 16-bit signed integer. + INT32, ///< A 32-bit signed integer. + INT64, ///< A 64-bit signed integer. + INT128, ///< A 128-bit signed integer. + INT256, ///< A 256-bit signed integer. + UINT8, ///< An 8-bit unsigned integer. + UINT16, ///< A 16-bit unsigned integer. + UINT32, ///< A 32-bit unsigned integer. + UINT64, ///< A 64-bit unsigned integer. + UINT128, ///< A 128-bit unsigned integer. + UINT256, ///< A 256-bit unsigned integer. + CHAR8, ///< An 8-character string. + CHAR16, ///< A 16-character string. + CHAR32, ///< A 32-character string. + CHAR64, ///< A 64-character string. + LITEREF ///< A Literef reference to a patchwork fragment + } + + /** + @notice Struct defining the metadata schema. + @dev This defines the overall structure of the metadata and contains entries describing each data field. + */ + struct MetadataSchema { + uint256 version; ///< Version of the metadata schema. + MetadataSchemaEntry[] entries; ///< Array of entries in the schema. + } + + /** + @notice Struct defining individual entries within the metadata schema. + @dev Represents each data field in the schema, detailing its properties and type. + */ + struct MetadataSchemaEntry { + uint256 id; ///< Index or unique identifier of the entry. + uint256 permissionId; ///< Permission identifier associated with the entry. + FieldType fieldType; ///< Type of field data (from the FieldType enum). + uint256 fieldCount; ///< Number of elements of this field (0 = Dynamic Array, 1 = Single, >1 = Static Array) + FieldVisibility visibility; ///< Visibility level of the field. + uint256 slot; ///< Starting storage slot, may span multiple slots based on width. + uint256 offset; ///< Offset in bits within the storage slot. + string key; ///< Key or name associated with the field. + } + + /** + @notice Enumeration of field visibility options. + @dev Specifies whether a field is publicly accessible or private. + */ + enum FieldVisibility { + PUBLIC, ///< Field is publicly accessible. + PRIVATE ///< Field is private + } +} + +/** +@title Patchwork Protocol 721 Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork metadata standard +*/ +interface IPatchwork721 is IPatchworkScoped, IPatchworkMetadata, IERC5192, IERC721 { + /** + @notice Emitted when the freeze status is changed to frozen. + @param tokenId The identifier for a token. + */ + event Frozen(uint256 indexed tokenId); + + /** + @notice Emitted when the locking status is changed to not frozen. + @param tokenId The identifier for a token. + */ + event Thawed(uint256 indexed tokenId); + + /** + @notice Emitted when the permissions are changed + @param to The address the permissions are assigned to + @param permissions The permissions + */ + event PermissionChange(address indexed to, uint256 permissions); + + /** + @notice Emitted when the schema has changed + @param addr the address of the Patchwork721 + */ + event SchemaChange(address indexed addr); + + /** + @notice Returns the URI of the schema + @return string the URI of the schema + */ + function schemaURI() external view returns (string memory); + + /** + @notice Returns the metadata schema + @return MetadataSchema the metadata schema + */ + function schema() external view returns (MetadataSchema memory); + + /** + @notice Returns the URI of the image associated with the given token ID + @param tokenId ID of the token + @return string the image URI + */ + function imageURI(uint256 tokenId) external view returns (string memory); + + /** + @notice Sets permissions for a given address + @param to Address to set permissions for + @param permissions Permissions value + */ + function setPermissions(address to, uint256 permissions) external; + + /** + @notice Stores packed metadata for a given token ID and slot + @param tokenId ID of the token + @param slot Slot to store metadata + @param data Metadata to store + */ + function storePackedMetadataSlot(uint256 tokenId, uint256 slot, uint256 data) external; + + /** + @notice Stores packed metadata for a given token ID + @param tokenId ID of the token + @param data Metadata to store + */ + function storePackedMetadata(uint256 tokenId, uint256[] memory data) external; + + /** + @notice Loads packed metadata for a given token ID and slot + @param tokenId ID of the token + @param slot Slot to load metadata from + @return uint256 the raw slot data as a uint256 + */ + function loadPackedMetadataSlot(uint256 tokenId, uint256 slot) external view returns (uint256); + + /** + @notice Loads packed metadata for a given token ID + @param tokenId ID of the token + @return uint256[] the raw slot data as a uint256 array + */ + function loadPackedMetadata(uint256 tokenId) external view returns (uint256[] memory); + + /** + @notice Returns the freeze nonce for a given token ID + @param tokenId ID of the token + @return nonce the nonce + */ + function getFreezeNonce(uint256 tokenId) external view returns (uint256 nonce); + + /** + @notice Sets the freeze status of a token + @param tokenId ID of the token + @param frozen Freeze status to set + */ + function setFrozen(uint256 tokenId, bool frozen) external; + + /** + @notice Gets the freeze status of a token (ERC-5192) + @param tokenId ID of the token + @return bool true if frozen, false if not + */ + function frozen(uint256 tokenId) external view returns (bool); + + /** + @notice Sets the lock status of a token + @param tokenId ID of the token + @param locked Lock status to set + */ + function setLocked(uint256 tokenId, bool locked) external; +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkAccountPatch.sol b/src/interfaces/IPatchworkAccountPatch.sol new file mode 100644 index 0000000..60c6a36 --- /dev/null +++ b/src/interfaces/IPatchworkAccountPatch.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IPatchworkScoped.sol"; + +/** +@title Patchwork Protocol Account Patch Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork patch standard +*/ +interface IPatchworkAccountPatch is IPatchworkScoped { + /** + @notice Creates a new token for the owner, representing a patch + @param owner Address of the owner of the token + @param target Address of the original account + @return tokenId ID of the newly minted token + */ + function mintPatch(address owner, address target) external payable returns (uint256 tokenId); +} + +/** +@title Patchwork Protocol Reversible Account Patch Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork account patch standard with reverse lookup +*/ +interface IPatchworkReversibleAccountPatch is IPatchworkAccountPatch { + /** + @notice Returns the token ID (if it exists) for an NFT that may have been patched + @dev Requires reverse storage enabled + @param target Address of the original account + @return tokenId ID of the newly minted token + */ + function getTokenIdByTarget(address target) external returns (uint256 tokenId); +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkAssignable.sol b/src/interfaces/IPatchworkAssignable.sol new file mode 100644 index 0000000..77ccd85 --- /dev/null +++ b/src/interfaces/IPatchworkAssignable.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IPatchworkScoped.sol"; + +/** +@title Patchwork Protocol Assignable NFT Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork assignment +*/ +interface IPatchworkAssignable is IPatchworkScoped { + + /// Represents an assignment of a token from an external NFT contract to a token in this contract. + struct Assignment { + address tokenAddr; /// The address of the external NFT contract. + uint256 tokenId; /// The ID of the token in the external NFT contract. + } + + /** + @notice Assigns a token to another + @param ourTokenId ID of our token + @param to Address to assign to + @param tokenId ID of the token to assign + */ + function assign(uint256 ourTokenId, address to, uint256 tokenId) external; + + /** + @notice Checks permissions for assignment + @param ourTokenId the tokenID to assign + @param target the address of the target + @param targetTokenId the tokenID of the target + @param targetOwner the ownerOf of the target + @param by the account invoking the assignment to Patchwork Protocol + @param scopeName the scope name of the contract to assign to + */ + function allowAssignment(uint256 ourTokenId, address target, uint256 targetTokenId, address targetOwner, address by, string memory scopeName) external view returns (bool); +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkLiteRef.sol b/src/interfaces/IPatchworkLiteRef.sol new file mode 100644 index 0000000..9ec46f6 --- /dev/null +++ b/src/interfaces/IPatchworkLiteRef.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** +@title Patchwork Protocol LiteRef NFT Interface +@author Runic Labs, Inc +@notice Interface for contracts that have Lite Reference ID support +*/ +interface IPatchworkLiteRef { + /** + @notice Emitted when a contract redacts a fragment + @param target the contract which issued the redaction + @param fragment the fragment that was redacted + */ + event Redact(address indexed target, address indexed fragment); + + /** + @notice Emitted when a contract unredacts a fragment + @param target the contract which revoked the redaction + @param fragment the fragment that was unredacted + */ + event Unredact(address indexed target, address indexed fragment); + + /** + @notice Emitted when a contract registers a fragment + @param target the contract that registered the fragment + @param fragment the fragment that was registered + @param idx the idx of the literef + */ + event Register(address indexed target, address indexed fragment, uint8 idx); + + /** + @notice Registers a reference address + @param addr Address to register + @return id ID assigned to the address + */ + function registerReferenceAddress(address addr) external returns (uint8 id); + + /** + @notice Gets the ID assigned to the address from registration + @param addr Registered address + @return id ID assigned to the address + @return redacted Redacted status + */ + function getReferenceId(address addr) external view returns (uint8 id, bool redacted); + + /** + @notice Gets the address assigned to this id + @param id ID assigned to the address + @return addr Registered address + @return redacted Redacted status + */ + function getReferenceAddress(uint8 id) external view returns (address addr, bool redacted); + + /** + @notice Redacts a reference address + @param id ID of the address to redact + */ + function redactReferenceAddress(uint8 id) external; + + /** + @notice Unredacts a reference address + @param id ID of the address to unredact + */ + function unredactReferenceAddress(uint8 id) external; + + /** + @notice Returns a lite reference for a given address and token ID + @param addr Address to get reference for + @param tokenId ID of the token + @return liteRef Lite reference + @return redacted Redacted status + */ + function getLiteReference(address addr, uint256 tokenId) external view returns (uint64 liteRef, bool redacted); + + /** + @notice Returns an address and token ID for a given lite reference + @param liteRef Lite reference to get address and token ID for + @return addr Address + @return tokenId Token ID + */ + function getReferenceAddressAndTokenId(uint64 liteRef) external view returns (address addr, uint256 tokenId); + + /** + @notice Adds a reference to a token + @param tokenId ID of the token + @param liteRef LiteRef to add + */ + function addReference(uint256 tokenId, uint64 liteRef) external; + + /** + @notice Adds a reference to a token + @param tokenId ID of the token + @param liteRef LiteRef to add + @param targetMetadataId The metadata ID on the target to assign to + */ + function addReference(uint256 tokenId, uint64 liteRef, uint256 targetMetadataId) external; + + /** + @notice Adds multiple references to a token + @param tokenId ID of the token + @param liteRefs Array of lite references to add + */ + function addReferenceBatch(uint256 tokenId, uint64[] calldata liteRefs) external; + + /** + @notice Adds multiple references to a token + @param tokenId ID of the token + @param liteRefs Array of lite references to add + @param targetMetadataId The metadata ID on the target to assign to + */ + function addReferenceBatch(uint256 tokenId, uint64[] calldata liteRefs, uint256 targetMetadataId) external; + + /** + @notice Removes a reference from a token + @param tokenId ID of the token + @param liteRef Lite reference to remove + */ + function removeReference(uint256 tokenId, uint64 liteRef) external; + + /** + @notice Removes a reference from a token + @param tokenId ID of the token + @param liteRef Lite reference to remove + @param targetMetadataId The metadata ID on the target to unassign from + */ + function removeReference(uint256 tokenId, uint64 liteRef, uint256 targetMetadataId) external; + + /** + @notice Loads a reference address and token ID at a given index + @param ourTokenId ID of the token + @param idx Index to load from + @return addr Address + @return tokenId Token ID + */ + function loadReferenceAddressAndTokenId(uint256 ourTokenId, uint256 idx) external view returns (address addr, uint256 tokenId); + + /** + @notice Loads all static references for a given token ID + @param tokenId ID of the token + @return addresses Array of addresses + @return tokenIds Array of token IDs + */ + function loadAllStaticReferences(uint256 tokenId) external view returns (address[] memory addresses, uint256[] memory tokenIds); + + /** + @notice Count all dynamic references for a given token ID + @param tokenId ID of the token + @return count the number of dynamic references + */ + function getDynamicReferenceCount(uint256 tokenId) external view returns (uint256 count); + + /** + @notice Load a page of dynamic references for a given token ID + @param tokenId ID of the token + @param offset The starting offset 0-indexed + @param count The maximum number of references to return + @return addresses An array of reference addresses + @return tokenIds An array of reference token IDs + */ + function loadDynamicReferencePage(uint256 tokenId, uint256 offset, uint256 count) external view returns (address[] memory addresses, uint256[] memory tokenIds); +} diff --git a/src/interfaces/IPatchworkMintable.sol b/src/interfaces/IPatchworkMintable.sol new file mode 100644 index 0000000..f90a1c6 --- /dev/null +++ b/src/interfaces/IPatchworkMintable.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IPatchworkScoped.sol"; + +/** +@title Patchwork Mintable Interface +@author Runic Labs, Inc +*/ +interface IPatchworkMintable is IPatchworkScoped { + + /** + @notice Mint a new token + @dev Mints a single token to a specified address. + @param to The address to which the token will be minted. + @param data Additional data to be passed to the minting process. + @return tokenId The ID of the minted token. + */ + function mint(address to, bytes calldata data) external payable returns (uint256 tokenId); + + /** + @notice Mint a batch of new tokens + @dev Mints multiple tokens to a specified address. + @param to The address to which the tokens will be minted. + @param data Additional data to be passed to the minting process. + @param quantity The number of tokens to mint. + @return tokenIds An array of the IDs of the minted tokens. + */ + function mintBatch(address to, bytes calldata data, uint256 quantity) external payable returns (uint256[] memory tokenIds); +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkMultiAssignable.sol b/src/interfaces/IPatchworkMultiAssignable.sol new file mode 100644 index 0000000..d7679dc --- /dev/null +++ b/src/interfaces/IPatchworkMultiAssignable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IPatchworkAssignable.sol"; + +/** +@title Patchwork Protocol Assignable NFT Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork assignment +*/ +interface IPatchworkMultiAssignable is IPatchworkAssignable { + + /** + @notice Checks if this fragment is assigned to a target + @param ourTokenId the tokenId of the fragment + @param target the address of the target + @param targetTokenId the tokenId of the target + */ + function isAssignedTo(uint256 ourTokenId, address target, uint256 targetTokenId) external view returns (bool); + + /** + @notice Unassigns a token + @param ourTokenId tokenId of our fragment + */ + function unassign(uint256 ourTokenId, address target, uint256 targetTokenId) external; + + /** + @notice Counts the number of unique assignments this token has + @param tokenId tokenId of our fragment + */ + function getAssignmentCount(uint256 tokenId) external view returns (uint256); + + /** + @notice Gets assignments for a fragment + @param tokenId tokenId of our fragment + @param offset the page offset + @param count the maximum numer of entries to return + */ + function getAssignments(uint256 tokenId, uint256 offset, uint256 count) external view returns (Assignment[] memory); +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkPatch.sol b/src/interfaces/IPatchworkPatch.sol new file mode 100644 index 0000000..0b96cc5 --- /dev/null +++ b/src/interfaces/IPatchworkPatch.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IPatchworkScoped.sol"; + +/** +@title Patchwork Protocol Patch Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork patch standard +*/ +interface IPatchworkPatch is IPatchworkScoped { + /// @dev A canonical path to an 721 patched + struct PatchTarget { + address addr; // The address of the 721 + uint256 tokenId; // The tokenId of the 721 + } + + /** + @notice Creates a new token for the owner, representing a patch + @param owner Address of the owner of the token + @param target path to target of patch + @return tokenId ID of the newly minted token + */ + function mintPatch(address owner, PatchTarget memory target) external payable returns (uint256 tokenId); + + /** + @notice Updates the real underlying ownership of a token in storage (if different from current) + @param tokenId ID of the token + */ + function updateOwnership(uint256 tokenId) external; + + /** + @notice Returns the underlying stored owner of a token ignoring real patched NFT ownership + @param tokenId ID of the token + @return address Address of the owner + */ + function ownerOfPatch(uint256 tokenId) external view returns (address); +} + +/** +@title Patchwork Protocol Reversible Patch Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork patch standard with reverse lookup +*/ +interface IPatchworkReversiblePatch is IPatchworkPatch { + /** + @notice Returns the token ID (if it exists) for an NFT that may have been patched + @dev Requires reverse storage enabled + @param target Patch to target of patch + @return tokenId token ID of the patch + */ + function getTokenIdByTarget(PatchTarget memory target) external view returns (uint256 tokenId); +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkProtocol.sol b/src/interfaces/IPatchworkProtocol.sol new file mode 100644 index 0000000..e249de1 --- /dev/null +++ b/src/interfaces/IPatchworkProtocol.sol @@ -0,0 +1,1066 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** +@title Patchwork Protocol Interface +@author Runic Labs, Inc +@notice Interface for Patchwork Protocol +*/ +interface IPatchworkProtocol { + /** + @notice The address is not authorized to perform this action + @param addr The address attempting to perform the action + */ + error NotAuthorized(address addr); + + /** + @notice The scope with the provided name already exists + @param scopeName Name of the scope + */ + error ScopeExists(string scopeName); + + /** + @notice The scope with the provided name does not exist + @param scopeName Name of the scope + */ + error ScopeDoesNotExist(string scopeName); + + /** + @notice Transfer of the scope to the provided address is not allowed + @param to Address not allowed for scope transfer + */ + error ScopeTransferNotAllowed(address to); + + /** + @notice The token with the provided ID at the given address is frozen + @param addr Address of the token owner + @param tokenId ID of the frozen token + */ + error Frozen(address addr, uint256 tokenId); + + /** + @notice The token with the provided ID at the given address is locked + @param addr Address of the token owner + @param tokenId ID of the locked token + */ + error Locked(address addr, uint256 tokenId); + + /** + @notice The address is not whitelisted for the given scope + @param scopeName Name of the scope + @param addr Address that isn't whitelisted + */ + error NotWhitelisted(string scopeName, address addr); + + /** + @notice The address at the given address has already been patched + @param addr The address that was patched + @param patchAddress Address of the patch applied + */ + error AccountAlreadyPatched(address addr, address patchAddress); + + /** + @notice The token at the given address has already been patched + @param addr Address of the original 721 + @param tokenId ID of the patched token + @param patchAddress Address of the patch applied + */ + error AlreadyPatched(address addr, uint256 tokenId, address patchAddress); + + /** + @notice The ERC1155 path has already been patched + @param addr Address of the 1155 + @param tokenId ID of the patched token + @param account The account patched + @param patchAddress Address of the patch applied + */ + error ERC1155AlreadyPatched(address addr, uint256 tokenId, address account, address patchAddress); + + /** + @notice The provided input lengths are not compatible or valid + @dev for any multi array inputs, they must be the same length + */ + error BadInputLengths(); + + /** + @notice The fragment at the given address is unregistered + @param addr Address of the unregistered fragment + */ + error FragmentUnregistered(address addr); + + /** + @notice The fragment at the given address has been redacted + @param addr Address of the redacted fragment + */ + error FragmentRedacted(address addr); + + /** + @notice The fragment with the provided ID at the given address is already assigned + @param addr Address of the fragment + @param tokenId ID of the assigned fragment + */ + error FragmentAlreadyAssigned(address addr, uint256 tokenId); + + /** + @notice The reference was not found for the given fragment and target + @param target Address of the target token + @param fragment Address of the fragment + @param tokenId ID of the fragment + */ + error RefNotFound(address target, address fragment, uint256 tokenId); + + /** + @notice The fragment with the provided ID at the given address is not assigned + @param addr Address of the fragment + @param tokenId ID of the fragment + */ + error FragmentNotAssigned(address addr, uint256 tokenId); + + /** + @notice The fragment with the provided ID at the given address is not assigned to the target + @param addr Address of the fragment + @param tokenId ID of the fragment + @param targetAddress Address of the target + @param targetTokenId ID of the target + */ + error FragmentNotAssignedToTarget(address addr, uint256 tokenId, address targetAddress, uint256 targetTokenId); + + /** + @notice The fragment at the given address is already registered + @param addr Address of the registered fragment + */ + error FragmentAlreadyRegistered(address addr); + + /** + @notice Ran out of available IDs for allocation + @dev Max 255 IDs per target + */ + error OutOfIDs(); + + /** + @notice The provided token ID is unsupported + @dev TokenIds may only be 56 bits long + @param tokenId The unsupported token ID + */ + error UnsupportedTokenId(uint256 tokenId); + + /** + @notice Cannot lock the soulbound patch at the given address + @param addr Address of the soulbound patch + */ + error CannotLockSoulboundPatch(address addr); + + /** + @notice The token with the provided ID at the given address is not frozen + @param addr Address of the token owner + @param tokenId ID of the token + */ + error NotFrozen(address addr, uint256 tokenId); + + /** + @notice The nonce for the token with the provided ID at the given address is incorrect + @dev It may be incorrect or a newer nonce may be present + @param addr Address of the token owner + @param tokenId ID of the token + @param nonce The incorrect nonce + */ + error IncorrectNonce(address addr, uint256 tokenId, uint256 nonce); + + /** + @notice Self assignment of the token with the provided ID at the given address is not allowed + @param addr Address of the token owner + @param tokenId ID of the token + */ + error SelfAssignmentNotAllowed(address addr, uint256 tokenId); + + /** + @notice Transfer of the token with the provided ID at the given address is not allowed + @param addr Address of the token owner + @param tokenId ID of the token + */ + error TransferNotAllowed(address addr, uint256 tokenId); + + /** + @notice Transfer of the token with the provided ID at the given address is blocked by an assignment + @param addr Address of the token owner + @param tokenId ID of the token + */ + error TransferBlockedByAssignment(address addr, uint256 tokenId); + + /** + @notice A rule is blocking the mint to this owner address + @param addr Address of the token owner + */ + error MintNotAllowed(address addr); + + /** + @notice The token at the given address is not IPatchworkAssignable + @param addr Address of the non-assignable token + */ + error NotPatchworkAssignable(address addr); + + /** + @notice A data integrity error has been detected + @dev Addr+TokenId is expected where addr2+tokenId2 is present + @param addr Address of the first token + @param tokenId ID of the first token + @param addr2 Address of the second token + @param tokenId2 ID of the second token + */ + error DataIntegrityError(address addr, uint256 tokenId, address addr2, uint256 tokenId2); + + /** + @notice The available balance does not satisfy the amount + */ + error InsufficientFunds(); + + /** + @notice The supplied fee is not the corret amount + */ + error IncorrectFeeAmount(); + + /** + @notice Minting is not active for this address + */ + error MintNotActive(); + + /** + @notice The value could not be sent + */ + error FailedToSend(); + + /** + @notice The contract is not supported + */ + error UnsupportedContract(); + + /** + @notice The operation is not supported + */ + error UnsupportedOperation(); + + /** + @notice No proposed fee is set + */ + error NoProposedFeeSet(); + + /** + @notice Timelock has not elapsed + */ + error TimelockNotElapsed(); + + /** + @notice Invalid fee value + */ + error InvalidFeeValue(); + + /** + @notice No delegate proposed + */ + error NoDelegateProposed(); + + /** + @notice Fee Configuration + */ + struct FeeConfig { + uint256 mintBp; /// mint basis points (10000 = 100%) + uint256 patchBp; /// patch basis points (10000 = 100%) + uint256 assignBp; /// assign basis points (10000 = 100%) + } + + /** + @notice Fee Configuration Override + */ + struct FeeConfigOverride { + uint256 mintBp; /// mint basis points (10000 = 100%) + uint256 patchBp; /// patch basis points (10000 = 100%) + uint256 assignBp; /// assign basis points (10000 = 100%) + bool active; /// true for present + } + + /** + @notice Proposal to change a fee configuration for either protocol or scope override + */ + struct ProposedFeeConfig { + FeeConfig config; + uint256 timestamp; + bool active; /// Used to enable/disable overrides - ignored for protocol + } + + /** + @notice Mint configuration + */ + struct MintConfig { + uint256 flatFee; /// fee per 1 quantity mint in wei + bool active; /// If the mint is active + } + + /** + @notice Proposed assigner delegate + */ + struct ProposedAssignerDelegate { + uint256 timestamp; + address addr; + } + + /** + @notice Represents a defined scope within the system + @dev Contains details about the scope ownership, permissions, and mappings for references and assignments + */ + struct Scope { + /** + @notice Owner of this scope + @dev Address of the account or contract that owns this scope + */ + address owner; + + /** + @notice Owner-elect + @dev Used in two-step transfer process. If this is set, only this owner can accept the transfer + */ + address ownerElect; + + /** + @notice Indicates whether a user is allowed to patch within this scope + @dev True if a user can patch, false otherwise. If false, only operators and the scope owner can perform patching. + */ + bool allowUserPatch; + + /** + @notice Indicates whether a user is allowed to assign within this scope + @dev True if a user can assign, false otherwise. If false, only operators and the scope owner can perform assignments. + */ + bool allowUserAssign; + + /** + @notice Indicates if a whitelist is required for operations within this scope + @dev True if whitelist is required, false otherwise + */ + bool requireWhitelist; + + /** + @notice Mapped list of operator addresses for this scope + @dev Address of the operator mapped to a boolean indicating if they are an operator + */ + mapping(address => bool) operators; + + /** + @notice Mapped whitelist of addresses that belong to this scope + @dev Address mapped to a boolean indicating if it's whitelisted + */ + mapping(address => bool) whitelist; + + /** + @notice Mapped list of mint configurations for this scope + @dev Address of the IPatchworkMintable mapped to the configuration + */ + mapping(address => MintConfig) mintConfigurations; + + /** + @notice Mapped list of patch fees for this scope + @dev Address of a 721, 1155 or account patch mapped to the fee in wei + */ + mapping(address => uint256) patchFees; + + /** + @notice Mapped list of assign fees for this scope + @dev Address of an IPatchworkAssignable mapped to the fee in wei + */ + mapping(address => uint256) assignFees; + + /** + @notice Balance in wei for this scope + @dev accrued in mint, patch and assign fees, may only be withdrawn by scope bankers + */ + uint256 balance; + + /** + @notice Mapped list of addresses that are designated bankers for this scope + @dev Address mapped to a boolean indicating if they are a banker + */ + mapping(address => bool) bankers; + } + + /** + @notice Emitted when a fragment is assigned + @param owner The owner of the target and fragment + @param fragmentAddress The address of the fragment's contract + @param fragmentTokenId The tokenId of the fragment + @param targetAddress The address of the target's contract + @param targetTokenId The tokenId of the target + @param scopeFee The fee collected to the scope + @param protocolFee The fee collected to the protocol + */ + event Assign(address indexed owner, address fragmentAddress, uint256 fragmentTokenId, address indexed targetAddress, uint256 indexed targetTokenId, uint256 scopeFee, uint256 protocolFee); + + /** + @notice Emitted when a fragment is unassigned + @param owner The owner of the fragment + @param fragmentAddress The address of the fragment's contract + @param fragmentTokenId The tokenId of the fragment + @param targetAddress The address of the target's contract + @param targetTokenId The tokenId of the target + */ + event Unassign(address indexed owner, address fragmentAddress, uint256 fragmentTokenId, address indexed targetAddress, uint256 indexed targetTokenId); + + /** + @notice Emitted when a patch is minted + @param owner The owner of the patch + @param originalAddress The address of the original 721's contract + @param originalTokenId The tokenId of the original 721 + @param patchAddress The address of the patch's contract + @param patchTokenId The tokenId of the patch + @param scopeFee The fee collected to the scope + @param protocolFee The fee collected to the protocol + */ + event Patch(address indexed owner, address originalAddress, uint256 originalTokenId, address indexed patchAddress, uint256 indexed patchTokenId, uint256 scopeFee, uint256 protocolFee); + + /** + @notice Emitted when a patch is minted + @param owner The owner of the patch + @param originalAddress The address of the original 1155's contract + @param originalTokenId The tokenId of the original 1155 + @param originalAccount The address of the original 1155's account + @param patchAddress The address of the patch's contract + @param patchTokenId The tokenId of the patch + @param scopeFee The fee collected to the scope + @param protocolFee The fee collected to the protocol + */ + event ERC1155Patch(address indexed owner, address originalAddress, uint256 originalTokenId, address originalAccount, address indexed patchAddress, uint256 indexed patchTokenId, uint256 scopeFee, uint256 protocolFee); + + + /** + @notice Emitted when an account patch is minted + @param owner The owner of the patch + @param originalAddress The address of the original account + @param patchAddress The address of the patch's contract + @param patchTokenId The tokenId of the patch + @param scopeFee The fee collected to the scope + @param protocolFee The fee collected to the protocol + */ + event AccountPatch(address indexed owner, address originalAddress, address indexed patchAddress, uint256 indexed patchTokenId, uint256 scopeFee, uint256 protocolFee); + + /** + @notice Emitted when a new scope is claimed + @param scopeName The name of the claimed scope + @param owner The owner of the scope + */ + event ScopeClaim(string scopeName, address indexed owner); + + /** + @notice Emitted when a scope has elected a new owner to transfer to + @param scopeName The name of the transferred scope + @param from The owner of the scope + @param to The owner-elect of the scope + */ + event ScopeTransferElect(string scopeName, address indexed from, address indexed to); + + /** + @notice Emitted when a scope transfer is canceled + @param scopeName The name of the transferred scope + @param from The owner of the scope + @param to The owner-elect of the scope + */ + event ScopeTransferCancel(string scopeName, address indexed from, address indexed to); + + /** + @notice Emitted when a scope is transferred + @param scopeName The name of the transferred scope + @param from The address transferring the scope + @param to The recipient of the scope + */ + event ScopeTransfer(string scopeName, address indexed from, address indexed to); + + /** + @notice Emitted when a scope has an operator added + @param scopeName The name of the scope + @param actor The address responsible for the action + @param operator The new operator's address + */ + event ScopeAddOperator(string scopeName, address indexed actor, address indexed operator); + + /** + @notice Emitted when a scope has an operator removed + @param scopeName The name of the scope + @param actor The address responsible for the action + @param operator The operator's address being removed + */ + event ScopeRemoveOperator(string scopeName, address indexed actor, address indexed operator); + + /** + @notice Emitted when a scope's rules are changed + @param scopeName The name of the scope + @param actor The address responsible for the action + @param allowUserPatch Indicates whether user patches are allowed + @param allowUserAssign Indicates whether user assignments are allowed + @param requireWhitelist Indicates whether a whitelist is required + */ + event ScopeRuleChange(string scopeName, address indexed actor, bool allowUserPatch, bool allowUserAssign, bool requireWhitelist); + + /** + @notice Emitted when a scope has an address added to the whitelist + @param scopeName The name of the scope + @param actor The address responsible for the action + @param addr The address being added to the whitelist + */ + event ScopeWhitelistAdd(string scopeName, address indexed actor, address indexed addr); + + /** + @notice Emitted when a scope has an address removed from the whitelist + @param scopeName The name of the scope + @param actor The address responsible for the action + @param addr The address being removed from the whitelist + */ + event ScopeWhitelistRemove(string scopeName, address indexed actor, address indexed addr); + + /** + @notice Emitted when a mint is configured + @param scopeName The name of the scope + @param mintable The address of the IPatchworkMintable + @param config The mint configuration + */ + event MintConfigure(string scopeName, address indexed actor, address indexed mintable, MintConfig config); + + /** + @notice Emitted when a banker is added to a scope + @param scopeName The name of the scope + @param actor The address responsible for the action + @param banker The banker that was added + */ + event ScopeBankerAdd(string scopeName, address indexed actor, address indexed banker); + + /** + @notice Emitted when a banker is removed from a scope + @param scopeName The name of the scope + @param actor The address responsible for the action + @param banker The banker that was removed + */ + event ScopeBankerRemove(string scopeName, address indexed actor, address indexed banker); + + /** + @notice Emitted when a withdrawl is made from a scope + @param scopeName The name of the scope + @param actor The address responsible for the action + @param amount The amount withdrawn + */ + event ScopeWithdraw(string scopeName, address indexed actor, uint256 amount); + + /** + @notice Emitted when a banker is added to the protocol + @param actor The address responsible for the action + @param banker The banker that was added + */ + event ProtocolBankerAdd(address indexed actor, address indexed banker); + + /** + @notice Emitted when a banker is removed from the protocol + @param actor The address responsible for the action + @param banker The banker that was removed + */ + event ProtocolBankerRemove(address indexed actor, address indexed banker); + + /** + @notice Emitted when a withdrawl is made from the protocol + @param actor The address responsible for the action + @param amount The amount withdrawn + */ + event ProtocolWithdraw(address indexed actor, uint256 amount); + + /** + @notice Emitted on mint + @param actor The address responsible for the action + @param scopeName The scope of the IPatchworkMintable + @param to The receipient of the mint + @param mintable The IPatchworkMintable minted + @param data The data used to mint + @param scopeFee The fee collected to the scope + @param protocolFee The fee collected to the protocol + */ + event Mint(address indexed actor, string scopeName, address indexed to, address indexed mintable, bytes data, uint256 scopeFee, uint256 protocolFee); + + /** + @notice Emitted on batch mint + @param actor The address responsible for the action + @param scopeName The scope of the IPatchworkMintable + @param to The receipient of the mint + @param mintable The IPatchworkMintable minted + @param data The data used to mint + @param quantity The quantity minted + @param scopeFee The fee collected to the scope + @param protocolFee The fee collected to the protocol + */ + event MintBatch(address indexed actor, string scopeName, address indexed to, address indexed mintable, bytes data, uint256 quantity, uint256 scopeFee, uint256 protocolFee); + + /** + @notice Emitted on protocol fee config proposed + @param config The fee configuration + */ + event ProtocolFeeConfigPropose(FeeConfig config); + + /** + @notice Emitted on protocol fee config committed + @param config The fee configuration + */ + event ProtocolFeeConfigCommit(FeeConfig config); + + /** + @notice Emitted on scope fee config override proposed + @param scopeName The scope + @param config The fee configuration + */ + event ScopeFeeOverridePropose(string scopeName, FeeConfigOverride config); + + /** + @notice Emitted on scope fee config override committed + @param scopeName The scope + @param config The fee configuration + */ + event ScopeFeeOverrideCommit(string scopeName, FeeConfigOverride config); + + /** + @notice Emitted on patch fee change + @param scopeName The scope of the patch + @param addr The address of the patch + @param fee The new fee + */ + event PatchFeeChange(string scopeName, address indexed addr, uint256 fee); + + /** + @notice Emitted on assign fee change + @param scopeName The scope of the assignable + @param addr The address of the assignable + @param fee The new fee + */ + event AssignFeeChange(string scopeName, address indexed addr, uint256 fee); + + /** + @notice Emitted on assigner delegate propose + @param addr The address of the delegate + */ + event AssignerDelegatePropose(address indexed addr); + + /** + @notice Emitted on assigner delegate commit + @param addr The address of the delegate + */ + event AssignerDelegateCommit(address indexed addr); + + /** + @notice Claim a scope + @param scopeName the name of the scope + */ + function claimScope(string calldata scopeName) external; + + /** + @notice Transfer ownership of a scope + @dev must be accepted by transferee - see {acceptScopeTransfer} + @param scopeName Name of the scope + @param newOwner Address of the new owner + */ + function transferScopeOwnership(string calldata scopeName, address newOwner) external; + + /** + @notice Cancel a pending scope transfer + @param scopeName Name of the scope + */ + function cancelScopeTransfer(string calldata scopeName) external; + + /** + @notice Accept a scope transfer + @param scopeName Name of the scope + */ + function acceptScopeTransfer(string calldata scopeName) external; + + /** + @notice Get owner-elect of a scope + @param scopeName Name of the scope + @return ownerElect Address of the scope's owner-elect + */ + function getScopeOwnerElect(string calldata scopeName) external view returns (address ownerElect); + + /** + @notice Get owner of a scope + @param scopeName Name of the scope + @return owner Address of the scope owner + */ + function getScopeOwner(string calldata scopeName) external view returns (address owner); + + /** + @notice Add an operator to a scope + @param scopeName Name of the scope + @param op Address of the operator + */ + function addOperator(string calldata scopeName, address op) external; + + /** + @notice Remove an operator from a scope + @param scopeName Name of the scope + @param op Address of the operator + */ + function removeOperator(string calldata scopeName, address op) external; + + /** + @notice Set rules for a scope + @param scopeName Name of the scope + @param allowUserPatch Boolean indicating whether user patches are allowed + @param allowUserAssign Boolean indicating whether user assignments are allowed + @param requireWhitelist Boolean indicating whether whitelist is required + */ + function setScopeRules(string calldata scopeName, bool allowUserPatch, bool allowUserAssign, bool requireWhitelist) external; + + /** + @notice Add an address to a scope's whitelist + @param scopeName Name of the scope + @param addr Address to be whitelisted + */ + function addWhitelist(string calldata scopeName, address addr) external; + + /** + @notice Remove an address from a scope's whitelist + @param scopeName Name of the scope + @param addr Address to be removed from the whitelist + */ + function removeWhitelist(string calldata scopeName, address addr) external; + + /** + @notice Set the mint configuration for a given address + @param addr The address for which to set the mint configuration, must be IPatchworkMintable + @param config The mint configuration to be set + */ + function setMintConfiguration(address addr, MintConfig memory config) external; + + /** + @notice Get the mint configuration for a given address + @param addr The address for which to get the mint configuration + @return config The mint configuration of the given address + */ + function getMintConfiguration(address addr) external view returns (MintConfig memory config); + + /** + @notice Set the patch fee for a given address + @dev must be banker of scope claimed by addr to call + @param addr The address for which to set the patch fee + @param baseFee The patch fee to be set in wei + */ + function setPatchFee(address addr, uint256 baseFee) external; + + /** + @notice Get the patch fee for a given address + @param addr The address for which to get the patch fee + @return baseFee The patch fee of the given address in wei + */ + function getPatchFee(address addr) external view returns (uint256 baseFee); + + /** + @notice Set the assign fee for a given fragment address + @dev must be banker of scope claimed by fragmentAddress to call + @param fragmentAddress The address of the fragment for which to set the fee + @param baseFee The assign fee to be set in wei + */ + function setAssignFee(address fragmentAddress, uint256 baseFee) external; + + /** + @notice Get the assign fee for a given fragment address + @param fragmentAddress The address of the fragment for which to get the fee + @return baseFee The assign fee of the given fragment address in wei + */ + function getAssignFee(address fragmentAddress) external view returns (uint256 baseFee); + + /** + @notice Add a banker to a given scope + @dev must be owner of scope to call + @param scopeName The name of the scope + @param addr The address to be added as a banker + */ + function addBanker(string memory scopeName, address addr) external; + + /** + @notice Remove a banker from a given scope + @dev must be owner of scope to call + @param scopeName The name of the scope + @param addr The address to be removed as a banker + */ + function removeBanker(string memory scopeName, address addr) external; + + /** + @notice Withdraw an amount from the balance of a given scope + @dev must be owner of scope or banker of scope to call + @dev transfers to the msg.sender + @param scopeName The name of the scope + @param amount The amount to be withdrawn in wei + */ + function withdraw(string memory scopeName, uint256 amount) external; + + /** + @notice Get the balance of a given scope + @param scopeName The name of the scope + @return balance The balance of the given scope in wei + */ + function balanceOf(string memory scopeName) external view returns (uint256 balance); + + /** + @notice Mint a new token + @param to The address to which the token will be minted + @param mintable The address of the IPatchworkMintable contract + @param data Additional data to be passed to the minting + @return tokenId The ID of the minted token + */ + function mint(address to, address mintable, bytes calldata data) external payable returns (uint256 tokenId); + + /** + @notice Mint a batch of new tokens + @param to The address to which the tokens will be minted + @param mintable The address of the IPatchworkMintable contract + @param data Additional data to be passed to the minting + @param quantity The number of tokens to mint + @return tokenIds An array of the IDs of the minted tokens + */ + function mintBatch(address to, address mintable, bytes calldata data, uint256 quantity) external payable returns (uint256[] memory tokenIds); + + /** + @notice Proposes a protocol fee configuration + @dev must be protocol owner or banker to call + @dev configuration does not apply until commitProtocolFeeConfig is called + @param config The protocol fee configuration to be set + */ + function proposeProtocolFeeConfig(FeeConfig memory config) external; + + /** + @notice Commits the current proposed protocol fee configuration + @dev must be protocol owner or banker to call + @dev may only be called after timelock has passed + */ + function commitProtocolFeeConfig() external; + + /** + @notice Get the current protocol fee configuration + @return config The current protocol fee configuration + */ + function getProtocolFeeConfig() external view returns (FeeConfig memory config); + + /** + @notice Proposes a protocol fee override for a scope + @dev must be protocol owner or banker to call + @param config The protocol fee override configuration to be set + */ + function proposeScopeFeeOverride(string memory scopeName, FeeConfigOverride memory config) external; + + /** + @notice Commits the current proposed protocol fee override configuration for a scope + @dev must be protocol owner or banker to call + @dev may only be called after timelock has passed + */ + function commitScopeFeeOverride(string memory scopeName) external; + + /** + @notice Get the protocol fee override for a scope + @return config The current protocol fee override + */ + function getScopeFeeOverride(string memory scopeName) external view returns (FeeConfigOverride memory config); + + /** + @notice Add a banker to the protocol + @dev must be protocol owner to call + @param addr The address to be added as a protocol banker + */ + function addProtocolBanker(address addr) external; + + /** + @notice Remove a banker from the protocol + @dev must be protocol owner to call + @param addr The address to be removed as a protocol banker + */ + function removeProtocolBanker(address addr) external; + + /** + @notice Withdraw a specified amount from the protocol balance + @dev must be protocol owner or banker to call + @dev transfers to the msg.sender + @param balance The amount to be withdrawn in wei + */ + function withdrawFromProtocol(uint256 balance) external; + + /** + @notice Get the current balance of the protocol + @return balance The balance of the protocol in wei + */ + function balanceOfProtocol() external view returns (uint256 balance); + + /** + @notice Create a new patch + @param owner The owner of the patch + @param originalAddress Address of the original 721 + @param originalTokenId Token ID of the original 721 + @param patchAddress Address of the IPatchworkPatch to mint + @return tokenId Token ID of the newly created patch + */ + function patch(address owner, address originalAddress, uint originalTokenId, address patchAddress) external payable returns (uint256 tokenId); + + /** + @notice Callback for when a patch is burned + @dev can only be called from the patchAddress + @param originalAddress Address of the original 721 + @param originalTokenId Token ID of the original 721 + @param patchAddress Address of the IPatchworkPatch to mint + */ + function patchBurned(address originalAddress, uint originalTokenId, address patchAddress) external; + + /** + @notice Create a new 1155 patch + @param originalAddress Address of the original 1155 + @param originalTokenId Token ID of the original 1155 + @param originalAccount Address of the account to patch + @param patchAddress Address of the IPatchworkPatch to mint + @return tokenId Token ID of the newly created patch + */ + function patch1155(address to, address originalAddress, uint originalTokenId, address originalAccount, address patchAddress) external payable returns (uint256 tokenId); + + /** + @notice Callback for when an 1155 patch is burned + @dev can only be called from the patchAddress + @param originalAddress Address of the original 1155 + @param originalTokenId Token ID of the original 1155 + @param originalAccount Address of the account to patch + @param patchAddress Address of the IPatchworkPatch to mint + */ + function patchBurned1155(address originalAddress, uint originalTokenId, address originalAccount, address patchAddress) external; + + /** + @notice Create a new account patch + @param owner The owner of the patch + @param originalAddress Address of the original account + @param patchAddress Address of the IPatchworkPatch to mint + @return tokenId Token ID of the newly created patch + */ + function patchAccount(address owner, address originalAddress, address patchAddress) external payable returns (uint256 tokenId); + + /** + @notice Callback for when an account patch is burned + @dev can only be called from the patchAddress + @param originalAddress Address of the original 1155 + @param patchAddress Address of the IPatchworkPatch to mint + */ + function patchBurnedAccount(address originalAddress, address patchAddress) external; + + /** + @notice Assigns a relation to have an IPatchworkLiteRef form a LiteRef to a IPatchworkAssignable + @param fragment The IPatchworkAssignable address to assign + @param fragmentTokenId The IPatchworkAssignable Token ID to assign + @param target The IPatchworkLiteRef address to hold the reference to the fragment + @param targetTokenId The IPatchworkLiteRef Token ID to hold the reference to the fragment + */ + function assign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) external payable; + + /** + @notice Assigns a relation to have an IPatchworkLiteRef form a LiteRef to a IPatchworkAssignable + @param fragment The IPatchworkAssignable address to assign + @param fragmentTokenId The IPatchworkAssignable Token ID to assign + @param target The IPatchworkLiteRef address to hold the reference to the fragment + @param targetTokenId The IPatchworkLiteRef Token ID to hold the reference to the fragment + @param targetMetadataId The metadata ID on the target to store the reference in + */ + function assign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) external payable; + + /** + @notice Assign multiple fragments to a target in batch + @param fragments The array of addresses of the fragment IPatchworkAssignables + @param tokenIds The array of token IDs of the fragment IPatchworkAssignables + @param target The address of the target IPatchworkLiteRef + @param targetTokenId The token ID of the target IPatchworkLiteRef + */ + function assignBatch(address[] calldata fragments, uint256[] calldata tokenIds, address target, uint256 targetTokenId) external payable; + + /** + @notice Assign multiple fragments to a target in batch + @param fragments The array of addresses of the fragment IPatchworkAssignables + @param tokenIds The array of token IDs of the fragment IPatchworkAssignables + @param target The address of the target IPatchworkLiteRef + @param targetTokenId The token ID of the target IPatchworkLiteRef + @param targetMetadataId The metadata ID on the target to store the references in + */ + function assignBatch(address[] calldata fragments, uint256[] calldata tokenIds, address target, uint256 targetTokenId, uint256 targetMetadataId) external payable; + + /** + @notice Unassign a fragment from a target + @param fragment The IPatchworkSingleAssignable address of the fragment + @param fragmentTokenId The IPatchworkSingleAssignable token ID of the fragment + @dev reverts if fragment is not an IPatchworkSingleAssignable + */ + function unassignSingle(address fragment, uint256 fragmentTokenId) external; + + /** + @notice Unassign a fragment from a target + @param fragment The IPatchworkSingleAssignable address of the fragment + @param fragmentTokenId The IPatchworkSingleAssignable token ID of the fragment + @param targetMetadataId The metadata ID on the target to unassign from + @dev reverts if fragment is not an IPatchworkSingleAssignable + */ + function unassignSingle(address fragment, uint256 fragmentTokenId, uint256 targetMetadataId) external; + + /** + @notice Unassigns a multi relation + @param fragment The IPatchworMultiAssignable address to unassign + @param fragmentTokenId The IPatchworkMultiAssignable Token ID to unassign + @param target The IPatchworkLiteRef address which holds a reference to the fragment + @param targetTokenId The IPatchworkLiteRef Token ID which holds a reference to the fragment + @dev reverts if fragment is not an IPatchworkMultiAssignable + */ + function unassignMulti(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) external; + + /** + @notice Unassigns a multi relation + @param fragment The IPatchworMultiAssignable address to unassign + @param fragmentTokenId The IPatchworkMultiAssignable Token ID to unassign + @param target The IPatchworkLiteRef address which holds a reference to the fragment + @param targetTokenId The IPatchworkLiteRef Token ID which holds a reference to the fragment + @param targetMetadataId The metadata ID on the target to unassign from + @dev reverts if fragment is not an IPatchworkMultiAssignable + */ + function unassignMulti(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) external; + + /** + @notice Unassigns a relation (single or multi) + @param fragment The IPatchworkAssignable address to unassign + @param fragmentTokenId The IPatchworkAssignable Token ID to unassign + @param target The IPatchworkLiteRef address which holds a reference to the fragment + @param targetTokenId The IPatchworkLiteRef Token ID which holds a reference to the fragment + */ + function unassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId) external; + + /** + @notice Unassigns a relation (single or multi) + @param fragment The IPatchworkAssignable address to unassign + @param fragmentTokenId The IPatchworkAssignable Token ID to unassign + @param target The IPatchworkLiteRef address which holds a reference to the fragment + @param targetTokenId The IPatchworkLiteRef Token ID which holds a reference to the fragment + @param targetMetadataId The metadata ID on the target to unassign from + */ + function unassign(address fragment, uint256 fragmentTokenId, address target, uint256 targetTokenId, uint256 targetMetadataId) external; + + /** + @notice Apply transfer rules and actions of a specific token from one address to another + @param from The address of the sender + @param to The address of the receiver + @param tokenId The ID of the token to be transferred + */ + function applyTransfer(address from, address to, uint256 tokenId) external; + + /** + @notice Update the ownership tree of a specific Patchwork 721 + @param addr The address of the Patchwork 721 + @param tokenId The ID of the token whose ownership tree needs to be updated + */ + function updateOwnershipTree(address addr, uint256 tokenId) external; + + /** + @notice Propose an assigner delegate module + @param addr The address of the new delegate module + */ + function proposeAssignerDelegate(address addr) external; + + /** + @notice Commit the proposed assigner delegate module + @dev must be past timelock + */ + function commitAssignerDelegate() external; +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkScoped.sol b/src/interfaces/IPatchworkScoped.sol new file mode 100644 index 0000000..5e1266a --- /dev/null +++ b/src/interfaces/IPatchworkScoped.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** +@title Patchwork Protocol Scoped Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting scopes +*/ +interface IPatchworkScoped { + /** + @notice Get the scope this NFT claims to belong to + @return string the name of the scope + */ + function getScopeName() external view returns (string memory); +} \ No newline at end of file diff --git a/src/interfaces/IPatchworkSingleAssignable.sol b/src/interfaces/IPatchworkSingleAssignable.sol new file mode 100644 index 0000000..b6ccd63 --- /dev/null +++ b/src/interfaces/IPatchworkSingleAssignable.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IPatchworkAssignable.sol"; + +/** +@title Patchwork Protocol Assignable Interface +@author Runic Labs, Inc +@notice Interface for contracts supporting Patchwork assignment +*/ +interface IPatchworkSingleAssignable is IPatchworkAssignable { + /** + @notice Unassigns a token + @param ourTokenId ID of our token + */ + function unassign(uint256 ourTokenId) external; + + /** + @notice Returns the address and token ID that our token is assigned to + @param ourTokenId ID of our token + @return address the address this is assigned to + @return uint256 the tokenId this is assigned to + */ + function getAssignedTo(uint256 ourTokenId) external view returns (address, uint256); + + /** + @notice Returns the underlying stored owner of a token ignoring current assignment + @param ourTokenId ID of our token + @return address address of the owner + */ + function unassignedOwnerOf(uint256 ourTokenId) external view returns (address); + + /** + @notice Sends events for a token when the assigned-to token has been transferred + @param from Sender address + @param to Recipient address + @param tokenId ID of the token + */ + function onAssignedTransfer(address from, address to, uint256 tokenId) external; + + /** + @notice Updates the real underlying ownership of a token in storage (if different from current) + @param tokenId ID of the token + */ + function updateOwnership(uint256 tokenId) external; +} \ No newline at end of file diff --git a/src/sampleNFTs/TestPatchworkNFT.sol b/src/sampleNFTs/TestPatchworkNFT.sol deleted file mode 100644 index 3be3677..0000000 --- a/src/sampleNFTs/TestPatchworkNFT.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "../PatchworkNFTInterface.sol"; -import "../PatchworkNFTBase.sol"; - -contract TestPatchworkNFT is PatchworkNFT { - - struct TestPatchworkNFTMetadata { - uint256 thing; - } - - constructor(address manager_) PatchworkNFT("testscope", "TestPatchworkNFT", "TPLR", msg.sender, manager_) { - } - - function schemaURI() pure external returns (string memory) { - return "https://mything/my-nft-metadata.json"; - } - - function imageURI(uint256 _tokenId) pure external returns (string memory) { - return string(abi.encodePacked("https://mything/nft-", _tokenId)); - } - - function schema() pure external returns (MetadataSchema memory) { - MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](1); - entries[0] = MetadataSchemaEntry(1, 0, FieldType.UINT256, 0, FieldVisibility.PUBLIC, 2, 0, "thing"); - return MetadataSchema(1, entries); - } - - function mint(address to, uint256 tokenId) public { - _mint(to, tokenId); - _metadataStorage[tokenId] = new uint256[](1); - } -} \ No newline at end of file diff --git a/test/AssignerDelegate.t.sol b/test/AssignerDelegate.t.sol new file mode 100644 index 0000000..6820976 --- /dev/null +++ b/test/AssignerDelegate.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; + +contract AssignerDelegateTest is Test { + + PatchworkProtocol _prot; + address _defaultUser; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + + vm.startPrank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + vm.stopPrank(); + } + + function testTimelock() public { + // test wrong user + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _defaultUser)); + _prot.commitAssignerDelegate(); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _defaultUser)); + _prot.proposeAssignerDelegate(address(5)); + + // test no proposal + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NoDelegateProposed.selector)); + vm.prank(_patchworkOwner); + _prot.commitAssignerDelegate(); + + // happy path propose + vm.prank(_patchworkOwner); + _prot.proposeAssignerDelegate(address(5)); + + // timelock not met + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.TimelockNotElapsed.selector)); + vm.prank(_patchworkOwner); + _prot.commitAssignerDelegate(); + + // test cancel + vm.prank(_patchworkOwner); + _prot.proposeAssignerDelegate(address(0)); + + // no proposal after cancel + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NoDelegateProposed.selector)); + vm.prank(_patchworkOwner); + _prot.commitAssignerDelegate(); + + // happy path propose, skip and commit + vm.prank(_patchworkOwner); + _prot.proposeAssignerDelegate(address(5)); + skip(2000000); + vm.prank(_patchworkOwner); + _prot.commitAssignerDelegate(); + + // no proposal after commit + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NoDelegateProposed.selector)); + vm.prank(_patchworkOwner); + _prot.commitAssignerDelegate(); + } +} \ No newline at end of file diff --git a/test/Fees.t.sol b/test/Fees.t.sol new file mode 100644 index 0000000..9c748e0 --- /dev/null +++ b/test/Fees.t.sol @@ -0,0 +1,504 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/Test1155PatchNFT.sol"; +import "./nfts/TestBase1155.sol"; +import "./nfts/TestFragmentLiteRefNFT.sol"; +import "./nfts/TestDynamicArrayLiteRefNFT.sol"; +import "./nfts/TestMultiFragmentNFT.sol"; +import "./nfts/TestPatchLiteRefNFT.sol"; +import "./nfts/TestAccountPatchNFT.sol"; +import "./nfts/TestBaseNFT.sol"; + +contract FeesTest is Test { + + PatchworkProtocol _prot; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.startPrank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + _prot.proposeProtocolFeeConfig(IPatchworkProtocol.FeeConfig(1000, 1000, 1000)); // 10%, 10%, 10% + skip(20000000); + _prot.commitProtocolFeeConfig(); + vm.stopPrank(); + + vm.startPrank(_scopeOwner); + _scopeName = "testscope"; + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + vm.stopPrank(); + vm.deal(_scopeOwner, 2 ether); + } + + function testProtocolBankers() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _defaultUser)); + _prot.addProtocolBanker(_defaultUser); + vm.prank(_patchworkOwner); + _prot.addProtocolBanker(_user2Address); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _prot.proposeProtocolFeeConfig(IPatchworkProtocol.FeeConfig(1000, 1000, 1000)); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NoProposedFeeSet.selector)); + vm.prank(_patchworkOwner); + _prot.commitProtocolFeeConfig(); + + vm.prank(_patchworkOwner); + _prot.proposeProtocolFeeConfig(IPatchworkProtocol.FeeConfig(150, 150, 150)); + IPatchworkProtocol.FeeConfig memory feeConfig = _prot.getProtocolFeeConfig(); + assertEq(1000, feeConfig.mintBp); + assertEq(1000, feeConfig.assignBp); + assertEq(1000, feeConfig.patchBp); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _prot.commitProtocolFeeConfig(); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.TimelockNotElapsed.selector)); + vm.prank(_patchworkOwner); + _prot.commitProtocolFeeConfig(); + + skip(2000000); + vm.prank(_patchworkOwner); + _prot.commitProtocolFeeConfig(); + + feeConfig = _prot.getProtocolFeeConfig(); + assertEq(150, feeConfig.mintBp); + assertEq(150, feeConfig.assignBp); + assertEq(150, feeConfig.patchBp); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NoProposedFeeSet.selector)); + vm.prank(_patchworkOwner); + _prot.commitProtocolFeeConfig(); + + vm.prank(_user2Address); + _prot.proposeProtocolFeeConfig(IPatchworkProtocol.FeeConfig(1000, 1000, 1000)); + + feeConfig = _prot.getProtocolFeeConfig(); + assertEq(150, feeConfig.mintBp); + assertEq(150, feeConfig.assignBp); + assertEq(150, feeConfig.patchBp); + + skip(2000000); + vm.prank(_patchworkOwner); + _prot.commitProtocolFeeConfig(); + + vm.prank(_patchworkOwner); + _prot.addProtocolBanker(_defaultUser); + + vm.startPrank(_scopeOwner); + TestFragmentLiteRefNFT lr = new TestFragmentLiteRefNFT(address(_prot)); + _prot.addWhitelist(_scopeName, address(lr)); + _prot.setMintConfiguration(address(lr), IPatchworkProtocol.MintConfig(1000000000, true)); + vm.stopPrank(); + // mint something just to get some money in the account + IPatchworkProtocol.MintConfig memory mc = _prot.getMintConfiguration(address(lr)); + uint256 mintCost = mc.flatFee; + assertEq(1000000000, mintCost); + _prot.mint{value: mintCost}(_userAddress, address(lr), ""); + assertEq(900000000, _prot.balanceOf(_scopeName)); + assertEq(100000000, _prot.balanceOfProtocol()); + // default user not authorized + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.withdrawFromProtocol(100000000); + vm.prank(_user2Address); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InsufficientFunds.selector)); + _prot.withdrawFromProtocol(500000000); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.withdrawFromProtocol(50000000); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FailedToSend.selector)); + vm.prank(_defaultUser); + _prot.withdrawFromProtocol(50000000); + // banker + owner should work + vm.prank(_user2Address); + _prot.withdrawFromProtocol(50000000); + // Remove a banker + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _defaultUser)); + _prot.removeProtocolBanker(_user2Address); + vm.prank(_patchworkOwner); + _prot.removeProtocolBanker(_user2Address); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.withdrawFromProtocol(50000000); + vm.prank(_patchworkOwner); + _prot.withdrawFromProtocol(50000000); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InsufficientFunds.selector)); + vm.prank(_patchworkOwner); + _prot.withdrawFromProtocol(1); + } + + function testScopeBankers() public { + vm.startPrank(_scopeOwner); + TestFragmentLiteRefNFT lr = new TestFragmentLiteRefNFT(address(_prot)); + _prot.addWhitelist(_scopeName, address(lr)); + _prot.setMintConfiguration(address(lr), IPatchworkProtocol.MintConfig(1000000000, true)); + _prot.addBanker(_scopeName, _user2Address); + _prot.addBanker(_scopeName, _defaultUser); + vm.stopPrank(); + // mint something just to get some money in the account + IPatchworkProtocol.MintConfig memory mc = _prot.getMintConfiguration(address(lr)); + uint256 mintCost = mc.flatFee; + assertEq(1000000000, mintCost); + _prot.mint{value: mintCost}(_userAddress, address(lr), ""); + assertEq(900000000, _prot.balanceOf(_scopeName)); + assertEq(100000000, _prot.balanceOfProtocol()); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.withdraw(_scopeName, 450000000); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FailedToSend.selector)); + vm.prank(_defaultUser); + _prot.withdraw(_scopeName, 450000000); + vm.prank(_user2Address); + _prot.withdraw(_scopeName, 450000000); + vm.prank(_scopeOwner); + _prot.removeBanker(_scopeName, _user2Address); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.withdraw(_scopeName, 450000000); + // will work and take balance to 0 + vm.prank(_scopeOwner); + _prot.withdraw(_scopeName, 450000000); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InsufficientFunds.selector)); + vm.prank(_scopeOwner); + _prot.withdraw(_scopeName, 1); + } + + function testUnsupportedContracts() public { + vm.startPrank(_scopeOwner); + TestBaseNFT tBase = new TestBaseNFT(); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.setMintConfiguration(address(tBase), IPatchworkProtocol.MintConfig(1000000000, true)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.getMintConfiguration(address(tBase)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.setPatchFee(address(tBase), 1); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.getPatchFee(address(tBase)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.setAssignFee(address(tBase), 1); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.getAssignFee(address(tBase)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.mint(_userAddress, address(tBase), ""); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.mintBatch(_userAddress, address(tBase), "", 5); + } + + function testMints() public { + vm.startPrank(_scopeOwner); + TestFragmentLiteRefNFT lr = new TestFragmentLiteRefNFT(address(_prot)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.MintNotActive.selector)); + _prot.mint(_userAddress, address(lr), ""); + _prot.setScopeRules(_scopeName, false, false, true); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(lr))); + _prot.setMintConfiguration(address(lr), IPatchworkProtocol.MintConfig(1000000000, true)); + vm.stopPrank(); + IPatchworkProtocol.MintConfig memory mc = _prot.getMintConfiguration(address(lr)); + uint256 mintCost = mc.flatFee; + assertEq(0, mintCost); // Couldn't be set due to whitelisting + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(lr))); + _prot.mint{value: mintCost}(_userAddress, address(lr), ""); + assertEq(0, _prot.balanceOf(_scopeName)); + assertEq(0, _prot.balanceOfProtocol()); + // Now whitelisted + vm.startPrank(_scopeOwner); + _prot.addWhitelist(_scopeName, address(lr)); + _prot.setMintConfiguration(address(lr), IPatchworkProtocol.MintConfig(1000000000, true)); + vm.stopPrank(); + mc = _prot.getMintConfiguration(address(lr)); + mintCost = mc.flatFee; + assertEq(1000000000, mintCost); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.mint{value: 50}(_userAddress, address(lr), ""); + _prot.mint{value: mintCost}(_userAddress, address(lr), ""); + assertEq(900000000, _prot.balanceOf(_scopeName)); + assertEq(100000000, _prot.balanceOfProtocol()); + } + + function testBatchMints() public { + vm.startPrank(_scopeOwner); + _prot.setScopeRules(_scopeName, false, false, true); + TestFragmentLiteRefNFT lr = new TestFragmentLiteRefNFT(address(_prot)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(lr))); + _prot.setMintConfiguration(address(lr), IPatchworkProtocol.MintConfig(1000000000, true)); + vm.stopPrank(); + IPatchworkProtocol.MintConfig memory mc = _prot.getMintConfiguration(address(lr)); + uint256 mintCost = mc.flatFee; + assertEq(0, mintCost); // Couldn't be set due to whitelisting + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(lr))); + _prot.mintBatch{value: mintCost}(_userAddress, address(lr), "", 5); + assertEq(0, _prot.balanceOf(_scopeName)); + assertEq(0, _prot.balanceOfProtocol()); + // Now whitelisted + vm.startPrank(_scopeOwner); + _prot.addWhitelist(_scopeName, address(lr)); + _prot.setMintConfiguration(address(lr), IPatchworkProtocol.MintConfig(1000000000, true)); + vm.stopPrank(); + mc = _prot.getMintConfiguration(address(lr)); + mintCost = mc.flatFee * 5; + assertEq(5000000000, mintCost); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.mintBatch{value: 50}(_userAddress, address(lr), "", 5); + _prot.mintBatch{value: mintCost}(_userAddress, address(lr), "", 5); + assertEq(4500000000, _prot.balanceOf(_scopeName)); + assertEq(500000000, _prot.balanceOfProtocol()); + } + + function testPatchFees() public { + vm.startPrank(_scopeOwner); + _prot.setScopeRules(_scopeName, false, false, true); + TestBaseNFT tBase = new TestBaseNFT(); + TestBase1155 tBase1155 = new TestBase1155(); + TestPatchLiteRefNFT t721 = new TestPatchLiteRefNFT(address(_prot)); + Test1155PatchNFT t1155 = new Test1155PatchNFT(address(_prot)); + TestAccountPatchNFT tAccount = new TestAccountPatchNFT(address(_prot), false); + vm.stopPrank(); + + // 721 + _testPatchFees(address(t721)); + vm.startPrank(_scopeOwner); + uint256 tId = tBase.mint(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.patch(_userAddress, address(tBase), tId, address(t721)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.patch{value: 1 ether}(_userAddress, address(tBase), tId, address(t721)); + _prot.patch{value: _prot.getPatchFee(address(t721))}(_userAddress, address(tBase), tId, address(t721)); + vm.stopPrank(); + + // 1155 + _testPatchFees(address(t1155)); + vm.startPrank(_scopeOwner); + tId = tBase1155.mint(_userAddress, 1, 1); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.patch1155(_userAddress, address(tBase1155), tId, _userAddress, address(t1155)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.patch1155{value: 1 ether}(_userAddress, address(tBase1155), tId, _userAddress, address(t1155)); + _prot.patch1155{value: _prot.getPatchFee(address(t1155))}(_userAddress, address(tBase1155), tId, _userAddress, address(t1155)); + vm.stopPrank(); + + // account + _testPatchFees(address(tAccount)); + vm.startPrank(_scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.patchAccount(_userAddress, _user2Address, address(tAccount)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.patchAccount{value: 1 ether}(_userAddress, _user2Address, address(tAccount)); + _prot.patchAccount{value: _prot.getPatchFee(address(tAccount))}(_userAddress, _user2Address, address(tAccount)); + vm.stopPrank(); + + // patch wrong types + vm.startPrank(_scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.patch(_userAddress, address(tBase), 2, address(t1155)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.patch1155(_userAddress, address(tBase1155), 3, address(0), address(t721)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.patchAccount(_userAddress, _user2Address, address(t721)); + } + + function _testPatchFees(address patchAddr) private { + vm.startPrank(_scopeOwner); + // error cases + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, patchAddr)); + _prot.setPatchFee(patchAddr, 1); + _prot.addWhitelist(_scopeName, patchAddr); + vm.stopPrank(); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.setPatchFee(patchAddr, 1); + // success + vm.startPrank(_scopeOwner); + _prot.setPatchFee(patchAddr, 1); + vm.stopPrank(); + assertEq(1, _prot.getPatchFee(patchAddr)); + } + + function testAssignFees() public { + vm.startPrank(_scopeOwner); + _prot.setScopeRules(_scopeName, false, false, true); + TestFragmentLiteRefNFT nft = new TestFragmentLiteRefNFT(address(_prot)); + nft.registerReferenceAddress(address(nft)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(nft))); + _prot.setAssignFee(address(nft), 1); + _prot.addWhitelist(_scopeName, address(nft)); + vm.stopPrank(); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.setAssignFee(address(nft), 1); + // success + vm.startPrank(_scopeOwner); + _prot.setAssignFee(address(nft), 1); + assertEq(1, _prot.getAssignFee(address(nft))); + uint256 n1 = nft.mint(_userAddress, ""); + uint256 n2 = nft.mint(_userAddress, ""); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.assign(address(nft), n2, address(nft), n1); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.assign{value: 1 ether}(address(nft), n2, address(nft), n1); + _prot.assign{value: _prot.getAssignFee(address(nft))}(address(nft), n2, address(nft), n1); + } + + function testFeeOverrides() public { + vm.startPrank(_scopeOwner); + _prot.setScopeRules(_scopeName, false, false, true); + TestBaseNFT tBase = new TestBaseNFT(); + TestPatchLiteRefNFT t721 = new TestPatchLiteRefNFT(address(_prot)); + TestFragmentLiteRefNFT fragLr = new TestFragmentLiteRefNFT(address(_prot)); + _prot.addWhitelist(_scopeName, address(tBase)); + _prot.addWhitelist(_scopeName, address(t721)); + _prot.addWhitelist(_scopeName, address(fragLr)); + _prot.setMintConfiguration(address(fragLr), IPatchworkProtocol.MintConfig(1000000000, true)); + // Scope owner cannot set fee overrides for anyone + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _scopeOwner)); + _prot.proposeScopeFeeOverride(_scopeName, IPatchworkProtocol.FeeConfigOverride(100, 100, 100, true)); // 1% + + IPatchworkProtocol.FeeConfigOverride memory protFee = _prot.getScopeFeeOverride(_scopeName); + assertEq(false, protFee.active); + assertEq(0, protFee.mintBp); + assertEq(0, protFee.assignBp); + assertEq(0, protFee.patchBp); + + vm.stopPrank(); + + vm.prank(_patchworkOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NoProposedFeeSet.selector)); + _prot.commitScopeFeeOverride(_scopeName); + + vm.prank(_patchworkOwner); + _prot.proposeScopeFeeOverride(_scopeName, IPatchworkProtocol.FeeConfigOverride(100, 100, 100, true)); // 1% + protFee = _prot.getScopeFeeOverride(_scopeName); + assertEq(false, protFee.active); + assertEq(0, protFee.mintBp); + assertEq(0, protFee.assignBp); + assertEq(0, protFee.patchBp); + + vm.prank(_patchworkOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.TimelockNotElapsed.selector)); + _prot.commitScopeFeeOverride(_scopeName); + skip(2000000); + vm.prank(_scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _scopeOwner)); + _prot.commitScopeFeeOverride(_scopeName); + vm.prank(_patchworkOwner); + _prot.commitScopeFeeOverride(_scopeName); + vm.prank(_patchworkOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NoProposedFeeSet.selector)); + _prot.commitScopeFeeOverride(_scopeName); + + vm.prank(_patchworkOwner); + _prot.addProtocolBanker(_user2Address); + vm.prank(_user2Address); + _prot.proposeScopeFeeOverride(_scopeName, IPatchworkProtocol.FeeConfigOverride(100, 100, 100, true)); // 1% + protFee = _prot.getScopeFeeOverride(_scopeName); + assertEq(true, protFee.active); + assertEq(100, protFee.mintBp); + assertEq(100, protFee.assignBp); + assertEq(100, protFee.patchBp); + + vm.startPrank(_scopeOwner); + _prot.mint{value: 1000000000}(_userAddress, address(fragLr), ""); + + assertEq(990000000, _prot.balanceOf(_scopeName)); + assertEq(10000000, _prot.balanceOfProtocol()); + + fragLr.registerReferenceAddress(address(fragLr)); + _prot.setAssignFee(address(fragLr), 1000000000); + uint256 n1 = fragLr.mint(_userAddress, ""); + uint256 n2 = fragLr.mint(_userAddress, ""); + _prot.assign{value: _prot.getAssignFee(address(fragLr))}(address(fragLr), n2, address(fragLr), n1); + + assertEq(1980000000, _prot.balanceOf(_scopeName)); + assertEq(20000000, _prot.balanceOfProtocol()); + + _prot.setPatchFee(address(t721), 1000000000); + uint256 tId = tBase.mint(_userAddress); + _prot.patch{value: _prot.getPatchFee(address(t721))}(_userAddress, address(tBase), tId, address(t721)); + + assertEq(2970000000, _prot.balanceOf(_scopeName)); + assertEq(30000000, _prot.balanceOfProtocol()); + + vm.stopPrank(); + vm.prank(_patchworkOwner); + _prot.proposeScopeFeeOverride(_scopeName, IPatchworkProtocol.FeeConfigOverride(0, 0, 0, false)); // 1% + skip(2000000); + vm.prank(_patchworkOwner); + _prot.commitScopeFeeOverride(_scopeName); + protFee = _prot.getScopeFeeOverride(_scopeName); + assertEq(false, protFee.active); + assertEq(0, protFee.mintBp); + assertEq(0, protFee.assignBp); + assertEq(0, protFee.patchBp); + } + + function testInvalidFeeValues() public { + vm.startPrank(_patchworkOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InvalidFeeValue.selector)); + _prot.proposeProtocolFeeConfig(IPatchworkProtocol.FeeConfig(3001, 1000, 1000)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InvalidFeeValue.selector)); + _prot.proposeProtocolFeeConfig(IPatchworkProtocol.FeeConfig(1000, 3001, 1000)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InvalidFeeValue.selector)); + _prot.proposeProtocolFeeConfig(IPatchworkProtocol.FeeConfig(1000, 1000, 3001)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InvalidFeeValue.selector)); + _prot.proposeScopeFeeOverride(_scopeName, IPatchworkProtocol.FeeConfigOverride(3001, 0, 0, true)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InvalidFeeValue.selector)); + _prot.proposeScopeFeeOverride(_scopeName, IPatchworkProtocol.FeeConfigOverride(0, 3001, 0, true)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.InvalidFeeValue.selector)); + _prot.proposeScopeFeeOverride(_scopeName, IPatchworkProtocol.FeeConfigOverride(0, 0, 3001, true)); + } + + function testAssignBatchFees() public { + vm.startPrank(_scopeOwner); + _prot.setScopeRules(_scopeName, false, false, true); + TestFragmentLiteRefNFT nft = new TestFragmentLiteRefNFT(address(_prot)); + nft.registerReferenceAddress(address(nft)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(nft))); + _prot.setAssignFee(address(nft), 1); + _prot.addWhitelist(_scopeName, address(nft)); + vm.stopPrank(); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.setAssignFee(address(nft), 1); + // success + vm.startPrank(_scopeOwner); + _prot.setAssignFee(address(nft), 1); + uint256 nftAssignFee = _prot.getAssignFee(address(nft)); + assertEq(1, nftAssignFee); + uint256 n1 = nft.mint(_userAddress, ""); + uint256[] memory fragmentIds = new uint256[](8); + address[] memory fragmentAddresses = new address[](8); + for (uint8 i = 0; i < 8; i++) { + fragmentAddresses[i] = address(nft); + fragmentIds[i] = nft.mint(_userAddress, ""); + } + // No fee given should revert + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.assignBatch(fragmentAddresses, fragmentIds, address(nft), n1); + // too little fee should revert + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.assignBatch{value: nftAssignFee}(fragmentAddresses, fragmentIds, address(nft), n1); + // too much fee should revert + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectFeeAmount.selector)); + _prot.assignBatch{value: nftAssignFee * 9}(fragmentAddresses, fragmentIds, address(nft), n1); + // correct fee should pass + _prot.assignBatch{value: nftAssignFee * 8}(fragmentAddresses, fragmentIds, address(nft), n1); + } +} \ No newline at end of file diff --git a/test/Gas.t.sol b/test/Gas.t.sol new file mode 100644 index 0000000..d88654c --- /dev/null +++ b/test/Gas.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "./nfts/Test1155PatchNFT.sol"; +import "./nfts/TestBase1155.sol"; +import "./nfts/TestFragmentLiteRefNFT.sol"; +import "./nfts/TestDynamicArrayLiteRefNFT.sol"; +import "./nfts/TestMultiFragmentNFT.sol"; +import "./nfts/TestPatchLiteRefNFT.sol"; +import "./nfts/TestAccountPatchNFT.sol"; +import "./nfts/TestBaseNFT.sol"; + +contract GasTest is Test { + + PatchworkProtocol _prot; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + TestFragmentLiteRefNFT _lr; + mapping(bytes32 => uint8) private _supportedInterfaceCache; + mapping(address => string) private _scopeNameCache; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.startPrank(_scopeOwner); + _scopeName = "testscope"; + _lr = new TestFragmentLiteRefNFT(address(_prot)); + // call for every case as control / baseline + _setupCache(); + } + + function testDirect165() public { + assertTrue(_lr.supportsInterface(type(IERC721).interfaceId)); + assertTrue(_lr.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(_lr.supportsInterface(type(IPatchworkScoped).interfaceId)); + assertTrue(_lr.supportsInterface(type(IPatchworkSingleAssignable).interfaceId)); + } + + function testStored165() public { + assertEq(1, _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IERC721).interfaceId))]); + assertEq(1, _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IPatchwork721).interfaceId))]); + assertEq(1, _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IPatchworkScoped).interfaceId))]); + assertEq(1, _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IPatchworkSingleAssignable).interfaceId))]); + } + + function testDirectScopeName1() public { + // control for cold access + assertEq(_scopeName, _lr.getScopeName()); + } + + function testDirectScopeName4() public { + // control for warm access + assertEq(_scopeName, _lr.getScopeName()); + assertEq(_scopeName, _lr.getScopeName()); + assertEq(_scopeName, _lr.getScopeName()); + assertEq(_scopeName, _lr.getScopeName()); + } + + function testStoredScopeName1() public { + // cold access + assertEq(_scopeName, _scopeNameCache[address(_lr)]); + } + + function testStoredScopeName4() public { + // warm access + assertEq(_scopeName, _scopeNameCache[address(_lr)]); + assertEq(_scopeName, _scopeNameCache[address(_lr)]); + assertEq(_scopeName, _scopeNameCache[address(_lr)]); + assertEq(_scopeName, _scopeNameCache[address(_lr)]); + } + + function _setupCache() private { + if (_lr.supportsInterface(type(IERC721).interfaceId)) { + _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IERC721).interfaceId))] = 1; + } + if (_lr.supportsInterface(type(IPatchwork721).interfaceId)) { + _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IPatchwork721).interfaceId))] = 1; + } + if (_lr.supportsInterface(type(IPatchworkScoped).interfaceId)) { + _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IPatchworkScoped).interfaceId))] = 1; + } + if (_lr.supportsInterface(type(IPatchworkSingleAssignable).interfaceId)) { + _supportedInterfaceCache[keccak256(abi.encodePacked(address(_lr), type(IPatchworkSingleAssignable).interfaceId))] = 1; + } + _scopeNameCache[address(_lr)] = _lr.getScopeName(); + } + +} \ No newline at end of file diff --git a/test/Patchwork1155Patch.t.sol b/test/Patchwork1155Patch.t.sol new file mode 100644 index 0000000..692757d --- /dev/null +++ b/test/Patchwork1155Patch.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/Test1155PatchNFT.sol"; +import "./nfts/TestBase1155.sol"; + +contract Patchwork1155PatchTest is Test { + + PatchworkProtocol _prot; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + + vm.startPrank(_scopeOwner); + _scopeName = "testscope"; + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + vm.stopPrank(); + } + + function testScopeName() public { + vm.prank(_scopeOwner); + Test1155PatchNFT testAccountPatchNFT = new Test1155PatchNFT(address(_prot)); + assertEq(_scopeName, testAccountPatchNFT.getScopeName()); + } + + function testSupportsInterface() public { + vm.prank(_scopeOwner); + Test1155PatchNFT testAccountPatchNFT = new Test1155PatchNFT(address(_prot)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC165).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC721).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IPatchwork1155Patch).interfaceId)); + assertFalse(testAccountPatchNFT.supportsInterface(type(IPatchworkReversible1155Patch).interfaceId)); + + TestReversible1155PatchNFT t = new TestReversible1155PatchNFT(address(_prot)); + assertTrue(t.supportsInterface(type(IERC165).interfaceId)); + assertTrue(t.supportsInterface(type(IERC721).interfaceId)); + assertTrue(t.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(t.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(t.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(t.supportsInterface(type(IPatchwork1155Patch).interfaceId)); + assertTrue(t.supportsInterface(type(IPatchworkReversible1155Patch).interfaceId)); + } + + function test1155Patch() public { + vm.startPrank(_scopeOwner); + Test1155PatchNFT test1155PatchNFT = new Test1155PatchNFT(address(_prot)); + TestBase1155 base1155 = new TestBase1155(); + uint256 b = base1155.mint(_userAddress, 1, 5); + vm.stopPrank(); + vm.startPrank(address(_prot)); + // basic mints should work + test1155PatchNFT.mintPatch(_userAddress, IPatchwork1155Patch.PatchTarget(address(base1155), b, _userAddress)); + // global + test1155PatchNFT.mintPatch(_userAddress, IPatchwork1155Patch.PatchTarget(address(base1155), b, address(0))); + vm.stopPrank(); + // no auth + vm.expectRevert(); + test1155PatchNFT.mintPatch(_userAddress, IPatchwork1155Patch.PatchTarget(address(base1155), b, _userAddress)); + // global + vm.expectRevert(); + test1155PatchNFT.mintPatch(_userAddress, IPatchwork1155Patch.PatchTarget(address(base1155), b, address(0))); + } + + function test1155PatchProto() public { + vm.startPrank(_scopeOwner); + Test1155PatchNFT test1155PatchNFT = new Test1155PatchNFT(address(_prot)); + TestBase1155 base1155 = new TestBase1155(); + uint256 b = base1155.mint(_userAddress, 1, 5); + + // Account patch + _prot.patch1155(_userAddress, address(base1155), b, _userAddress, address(test1155PatchNFT)); + + // Account patch can't have duplicate + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ERC1155AlreadyPatched.selector, address(base1155), b, _userAddress, address(test1155PatchNFT))); + _prot.patch1155(_userAddress, address(base1155), b, _userAddress, address(test1155PatchNFT)); + // Global patch + _prot.patch1155(_scopeOwner, address(base1155), b, address(0), address(test1155PatchNFT)); + // Global patch can't have duplicate + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ERC1155AlreadyPatched.selector, address(base1155), b, address(0), address(test1155PatchNFT))); + _prot.patch1155(_scopeOwner, address(base1155), b, address(0), address(test1155PatchNFT)); + // no user patching allowed + vm.stopPrank(); + vm.startPrank(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + _prot.patch1155(_scopeOwner, address(base1155), b, address(1), address(test1155PatchNFT)); + } + + function test1155PatchUserPatch() public { + vm.prank(_scopeOwner); + _prot.setScopeRules(_scopeName, true, false, false); + // Not same owner model, yes transferrable + Test1155PatchNFT test1155PatchNFT = new Test1155PatchNFT(address(_prot)); + TestBase1155 base1155 = new TestBase1155(); + uint256 b = base1155.mint(_userAddress, 1, 5); + // user can mint + _prot.patch1155(_userAddress, address(base1155), b, _userAddress, address(test1155PatchNFT)); + } + + function testBurn() public { + vm.startPrank(_scopeOwner); + Test1155PatchNFT test1155PatchNFT = new Test1155PatchNFT(address(_prot)); + TestBase1155 base1155 = new TestBase1155(); + uint256 b = base1155.mint(_userAddress, 1, 5); + uint256 pId = _prot.patch1155(_userAddress, address(base1155), b, _userAddress, address(test1155PatchNFT)); + test1155PatchNFT.burn(pId); + // Should be able to re-patch now + pId = _prot.patch1155(_userAddress, address(base1155), b, _userAddress, address(test1155PatchNFT)); + } + + function testReverseLookups() public { + vm.startPrank(_scopeOwner); + TestReversible1155PatchNFT test1155PatchNFT = new TestReversible1155PatchNFT(address(_prot)); + TestBase1155 base1155 = new TestBase1155(); + uint256 b = base1155.mint(_userAddress, 1, 5); + uint256 pId = _prot.patch1155(_userAddress, address(base1155), b, _userAddress, address(test1155PatchNFT)); + assertEq(pId, test1155PatchNFT.getTokenIdByTarget(IPatchwork1155Patch.PatchTarget(address(base1155), b, _userAddress))); + } +} \ No newline at end of file diff --git a/test/PatchworkAccountPatch.t.sol b/test/PatchworkAccountPatch.t.sol new file mode 100644 index 0000000..a8069bc --- /dev/null +++ b/test/PatchworkAccountPatch.t.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestAccountPatchNFT.sol"; + +contract PatchworkAccountPatchTest is Test { + + PatchworkProtocol _prot; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + + vm.startPrank(_scopeOwner); + _scopeName = "testscope"; + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + vm.stopPrank(); + } + + function testScopeName() public { + vm.prank(_scopeOwner); + TestAccountPatchNFT testAccountPatchNFT = new TestAccountPatchNFT(address(_prot), false); + assertEq(_scopeName, testAccountPatchNFT.getScopeName()); + } + + function testSupportsInterface() public { + vm.prank(_scopeOwner); + TestAccountPatchNFT testAccountPatchNFT = new TestAccountPatchNFT(address(_prot), false); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC165).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC721).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IPatchworkAccountPatch).interfaceId)); + assertTrue(testAccountPatchNFT.supportsInterface(type(IPatchworkReversibleAccountPatch).interfaceId)); + } + + function testAccountPatchNotSameOwner() public { + // Not same owner model, yes transferrable + vm.prank(_scopeOwner); + TestAccountPatchNFT testAccountPatchNFT = new TestAccountPatchNFT(address(_prot), false); + // User patching is off, not authorized + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _prot.patchAccount(_userAddress, _user2Address, address(testAccountPatchNFT)); + vm.prank(_scopeOwner); + uint256 tokenId = _prot.patchAccount(_userAddress, _user2Address, address(testAccountPatchNFT)); + assertEq(_userAddress, testAccountPatchNFT.ownerOf(tokenId)); + // Duplicate should fail + vm.prank(_scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.AccountAlreadyPatched.selector, _user2Address, address(testAccountPatchNFT))); + _prot.patchAccount(_userAddress, _user2Address, address(testAccountPatchNFT)); + // Test transfer + vm.prank(_userAddress); + testAccountPatchNFT.transferFrom(_userAddress, address(55), tokenId); + vm.prank(address(55)); + testAccountPatchNFT.burn(tokenId); + // Should be able to re-patch now + vm.prank(_scopeOwner); + tokenId = _prot.patchAccount(_userAddress, _user2Address, address(testAccountPatchNFT)); + } + + function testAccountPatchSameOwner() public { + // Same owner model, not transferrable + vm.prank(_scopeOwner); + TestAccountPatchNFT testAccountPatchNFT = new TestAccountPatchNFT(address(_prot), true); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.MintNotAllowed.selector, _userAddress)); + vm.prank(_scopeOwner); + _prot.patchAccount(_userAddress, _user2Address, address(testAccountPatchNFT)); + vm.prank(_scopeOwner); + uint256 tokenId = _prot.patchAccount(_userAddress, _userAddress, address(testAccountPatchNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.TransferNotAllowed.selector, address(testAccountPatchNFT), tokenId)); + vm.prank(_userAddress); + testAccountPatchNFT.transferFrom(_userAddress, address(55), tokenId); + } + + function testAccountPatchUserPatch() public { + vm.prank(_scopeOwner); + _prot.setScopeRules(_scopeName, true, false, false); + // Not same owner model, yes transferrable + vm.prank(_scopeOwner); + TestAccountPatchNFT testAccountPatchNFT = new TestAccountPatchNFT(address(_prot), false); + // User patching is on + _prot.patchAccount(_userAddress, _user2Address, address(testAccountPatchNFT)); + } + + function testReverseLookups() public { + vm.startPrank(_scopeOwner); + TestAccountPatchNFT testAccountPatchNFT = new TestAccountPatchNFT(address(_prot), false); + // User patching is on + uint256 pId = _prot.patchAccount(_userAddress, _user2Address, address(testAccountPatchNFT)); + assertEq(pId, testAccountPatchNFT.getTokenIdByTarget(_user2Address)); + } +} \ No newline at end of file diff --git a/test/PatchworkFragmentMulti.t.sol b/test/PatchworkFragmentMulti.t.sol new file mode 100644 index 0000000..9ce0ee9 --- /dev/null +++ b/test/PatchworkFragmentMulti.t.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestFragmentLiteRefNFT.sol"; +import "./nfts/TestBaseNFT.sol"; +import "./nfts/TestMultiFragmentNFT.sol"; + +contract PatchworkFragmentMultiTest is Test { + PatchworkProtocol _prot; + TestFragmentLiteRefNFT _testFragmentLiteRefNFT; + TestMultiFragmentNFT _testMultiNFT; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + _scopeName = "testscope"; + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + _testFragmentLiteRefNFT = new TestFragmentLiteRefNFT(address(_prot)); + _testMultiNFT = new TestMultiFragmentNFT(address(_prot)); + + vm.stopPrank(); + } + + function testScopeName() public { + assertEq(_scopeName, _testMultiNFT.getScopeName()); + } + + function testSupportsInterface() public { + assertTrue(_testMultiNFT.supportsInterface(type(IERC165).interfaceId)); + assertTrue(_testMultiNFT.supportsInterface(type(IERC721).interfaceId)); + assertTrue(_testMultiNFT.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(_testMultiNFT.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(_testMultiNFT.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(_testMultiNFT.supportsInterface(type(IPatchworkAssignable).interfaceId)); + assertTrue(_testMultiNFT.supportsInterface(type(IPatchworkMultiAssignable).interfaceId)); + } + + function testMultiAssign() public { + vm.startPrank(_scopeOwner); + uint256 m1 = _testMultiNFT.mint(_user2Address, ""); + uint256 lr1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 lr2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 lr3 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 lr4 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + // must be registered + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentUnregistered.selector, address(_testMultiNFT))); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr1); + // happy path + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testMultiNFT)); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr1); + assertTrue(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr1)); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + assertTrue(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr2)); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr3); + assertTrue(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr3)); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr4); + assertTrue(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr4)); + assertEq(_testMultiNFT.ownerOf(m1), _user2Address); + // don't allow duplicate + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentAlreadyAssigned.selector, address(_testMultiNFT), m1)); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + // Don't allow duplicate (direct on NFT contract) + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentAlreadyAssigned.selector, address(_testMultiNFT), m1)); + _testMultiNFT.assign(m1, address(_testFragmentLiteRefNFT), lr2); + // don't allow either owner or random user to unassign + vm.stopPrank(); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, address(500))); + vm.prank(address(500)); + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + vm.startPrank(_scopeOwner); + // test unassign + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + assertFalse(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr2)); + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr1); + assertFalse(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr1)); + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr3); + assertFalse(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr3)); + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr4); + assertFalse(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr4)); + // not assigned + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentNotAssignedToTarget.selector, address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2)); + _prot.unassign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + // not assigned (direct to contract) + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentNotAssigned.selector, address(_testMultiNFT), m1)); + _testMultiNFT.unassign(m1, address(_testFragmentLiteRefNFT), lr2); + // test reassign + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + assertTrue(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr2)); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr3); + assertTrue(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr3)); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr1); + assertTrue(_testMultiNFT.isAssignedTo(m1, address(_testFragmentLiteRefNFT), lr1)); + } + + function testMultiAssignUserAssign() public { + vm.startPrank(_scopeOwner); + // Enable user assign + _prot.setScopeRules(_scopeName, false, true, false); + uint256 m1 = _testMultiNFT.mint(_user2Address, ""); + uint256 lr1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 lr2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + // must be registered + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentUnregistered.selector, address(_testMultiNFT))); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr1); + // happy path + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testMultiNFT)); + // as scope owner, should not revert. Only as user if they don't match, but they should work if they match. + // should revert + vm.stopPrank(); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr1); + // target owner should be able to assign to their target as well as the scope owner + vm.prank(_userAddress); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr1); + vm.prank(_scopeOwner); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), lr2); + vm.prank(_scopeOwner); + uint256 m2 = _testMultiNFT.mint(_userAddress, ""); + // This should also work because both are owned by the same user + vm.prank(_userAddress); + _prot.assign(address(_testMultiNFT), m2, address(_testFragmentLiteRefNFT), lr2); + } + + function testGetAssignments() public { + vm.startPrank(_scopeOwner); + uint256 m1 = _testMultiNFT.mint(_user2Address, ""); + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testMultiNFT)); + IPatchworkMultiAssignable.Assignment[] memory page0 = _testMultiNFT.getAssignments(m1, 0, 8); + assertEq(0, page0.length); + uint256[] memory liteRefIds = new uint256[](20); + for (uint256 i = 0; i < liteRefIds.length; i++) { + liteRefIds[i] = _testFragmentLiteRefNFT.mint(_userAddress, ""); + _prot.assign(address(_testMultiNFT), m1, address(_testFragmentLiteRefNFT), liteRefIds[i]); + } + assertEq(20, _testMultiNFT.getAssignmentCount(m1)); + IPatchworkMultiAssignable.Assignment[] memory page1 = _testMultiNFT.getAssignments(m1, 0, 8); + IPatchworkMultiAssignable.Assignment[] memory page2 = _testMultiNFT.getAssignments(m1, 8, 8); + IPatchworkMultiAssignable.Assignment[] memory page3 = _testMultiNFT.getAssignments(m1, 16, 8); + IPatchworkMultiAssignable.Assignment[] memory page4 = _testMultiNFT.getAssignments(m1, 20, 8); + IPatchworkMultiAssignable.Assignment[] memory page5 = _testMultiNFT.getAssignments(m1, 100, 8); + assertEq(8, page1.length); + assertEq(8, page2.length); + assertEq(4, page3.length); + assertEq(0, page4.length); + assertEq(0, page5.length); + assertEq(page1[0].tokenAddr, address(_testFragmentLiteRefNFT)); + assertEq(page1[0].tokenId, liteRefIds[0]); + assertEq(page2[0].tokenAddr, address(_testFragmentLiteRefNFT)); + assertEq(page2[0].tokenId, liteRefIds[8]); + assertEq(page3[0].tokenAddr, address(_testFragmentLiteRefNFT)); + assertEq(page3[0].tokenId, liteRefIds[16]); + // Check non existant + IPatchworkMultiAssignable.Assignment[] memory np1 = _testMultiNFT.getAssignments(11902381, 100, 8); + assertEq(0, np1.length); + } + + function testCrossScopeMulti() public { + string memory publicScope = "publicmulti"; + address publicScopeOwner = address(129837123); + vm.startPrank(publicScopeOwner); + TestMultiFragmentNFT multi = new TestMultiFragmentNFT(address(_prot)); + multi.setScopeName(publicScope); + _prot.claimScope(publicScope); + _prot.setScopeRules(publicScope, false, false, true); + _prot.addWhitelist(publicScope, address(multi)); + uint256 m1 = multi.mint(publicScopeOwner, ""); + vm.stopPrank(); + // mow we have a multi fragment in "publicmulti" scope and another scope wants to use it - both require whitelisting + vm.startPrank(_scopeOwner); + _prot.setScopeRules(_scopeName, false, false, true); + _prot.addWhitelist(_scopeName, address(_testFragmentLiteRefNFT)); + _testFragmentLiteRefNFT.registerReferenceAddress(address(multi)); + uint256 lr1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + _prot.assign(address(multi), m1, address(_testFragmentLiteRefNFT), lr1); + vm.stopPrank(); + // fragment scope can not unassign + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, publicScopeOwner)); + vm.prank(publicScopeOwner); + _prot.unassign(address(multi), m1, address(_testFragmentLiteRefNFT), lr1); + // target scope can unassign + vm.prank(_scopeOwner); + _prot.unassign(address(multi), m1, address(_testFragmentLiteRefNFT), lr1); + } +} \ No newline at end of file diff --git a/test/PatchworkFragmentSingle.t.sol b/test/PatchworkFragmentSingle.t.sol new file mode 100644 index 0000000..a7006a6 --- /dev/null +++ b/test/PatchworkFragmentSingle.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestFragmentLiteRefNFT.sol"; +import "./nfts/TestBaseNFT.sol"; + +contract PatchworkFragmentSingleTest is Test { + PatchworkProtocol _prot; + TestFragmentLiteRefNFT _testFragmentLiteRefNFT; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + _scopeName = "testscope"; + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + _testFragmentLiteRefNFT = new TestFragmentLiteRefNFT(address(_prot)); + + vm.stopPrank(); + } + + function testScopeName() public { + assertEq(_scopeName, _testFragmentLiteRefNFT.getScopeName()); + } + + function testSupportsInterface() public { + assertTrue(_testFragmentLiteRefNFT.supportsInterface(type(IERC165).interfaceId)); + assertTrue(_testFragmentLiteRefNFT.supportsInterface(type(IERC721).interfaceId)); + assertTrue(_testFragmentLiteRefNFT.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(_testFragmentLiteRefNFT.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(_testFragmentLiteRefNFT.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(_testFragmentLiteRefNFT.supportsInterface(type(IPatchworkAssignable).interfaceId)); + assertTrue(_testFragmentLiteRefNFT.supportsInterface(type(IPatchworkSingleAssignable).interfaceId)); + } + + function testOnAssignedTransferError() public { + vm.expectRevert(); + _testFragmentLiteRefNFT.onAssignedTransfer(address(0), address(1), 1); + } + + function testNotAssignedUnassign() public { + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentNotAssigned.selector, address(_testFragmentLiteRefNFT), 5)); + vm.prank(_scopeOwner); + _testFragmentLiteRefNFT.unassign(5); + } + + function testLiteref56bitlimit() public { + vm.prank(_scopeOwner); + uint8 r1 = _testFragmentLiteRefNFT.registerReferenceAddress(address(1)); + (uint64 ref, bool redacted) = _testFragmentLiteRefNFT.getLiteReference(address(1), 1); + assertEq((uint256(r1) << 56) + 1, ref); + (ref, redacted) = _testFragmentLiteRefNFT.getLiteReference(address(1), 0xFFFFFFFFFFFFFF); + assertEq((uint256(r1) << 56) + 0xFFFFFFFFFFFFFF, ref); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedTokenId.selector, 1 << 56)); + _testFragmentLiteRefNFT.getLiteReference(address(1), 1 << 56); + } +} \ No newline at end of file diff --git a/test/PatchworkNFT.t.sol b/test/PatchworkNFT.t.sol new file mode 100644 index 0000000..ba88887 --- /dev/null +++ b/test/PatchworkNFT.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestPatchworkNFT.sol"; + +contract PatchworkNFTTest is Test { + PatchworkProtocol _prot; + TestPatchworkNFT _testPatchworkNFT; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + _scopeName = "testscope"; + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + _testPatchworkNFT = new TestPatchworkNFT(address(_prot)); + vm.stopPrank(); + } + + function testScopeName() public { + assertEq(_scopeName, _testPatchworkNFT.getScopeName()); + } + + function testSupportsInterface() public { + assertTrue(_testPatchworkNFT.supportsInterface(type(IERC165).interfaceId)); + assertTrue(_testPatchworkNFT.supportsInterface(type(IERC721).interfaceId)); + assertTrue(_testPatchworkNFT.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(_testPatchworkNFT.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(_testPatchworkNFT.supportsInterface(type(IPatchwork721).interfaceId)); + } + + function testLoadStorePackedMetadataSlot() public { + uint256 n = _testPatchworkNFT.mint(_userAddress, ""); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _testPatchworkNFT.storePackedMetadataSlot(n, 0, 0x505050); + vm.prank(_scopeOwner); + _testPatchworkNFT.storePackedMetadataSlot(n, 0, 0x505050); + assertEq(0x505050, _testPatchworkNFT.loadPackedMetadataSlot(n, 0)); + } + + function testLoadStorePackedMetadata() public { + uint256 n = _testPatchworkNFT.mint(_userAddress, ""); + uint256[] memory slots = _testPatchworkNFT.loadPackedMetadata(n); + slots[0] = 0x505050; + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _testPatchworkNFT.storePackedMetadata(n, slots); + vm.prank(_scopeOwner); + _testPatchworkNFT.storePackedMetadata(n, slots); + slots = _testPatchworkNFT.loadPackedMetadata(n); + assertEq(0x505050, slots[0]); + } + + function testTransferFrom() public { + // TODO make sure these are calling checkTransfer on proto + uint256 n = _testPatchworkNFT.mint(_userAddress, ""); + assertEq(_userAddress, _testPatchworkNFT.ownerOf(n)); + vm.prank(_userAddress); + _testPatchworkNFT.transferFrom(_userAddress, _user2Address, n); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.prank(_user2Address); + _testPatchworkNFT.safeTransferFrom(_user2Address, _userAddress, n); + assertEq(_userAddress, _testPatchworkNFT.ownerOf(n)); + vm.prank(_userAddress); + _testPatchworkNFT.safeTransferFrom(_userAddress, _user2Address, n, bytes("abcd")); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + + // test wrong user revert + vm.startPrank(_userAddress); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, _userAddress, n)); + _testPatchworkNFT.transferFrom(_user2Address, _userAddress, n); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, _userAddress, n)); + _testPatchworkNFT.safeTransferFrom(_user2Address, _userAddress, n); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, _userAddress, n)); + _testPatchworkNFT.safeTransferFrom(_user2Address, _userAddress, n, bytes("abcd")); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + } + + function testLockFreezeSeparation() public { + uint256 n = _testPatchworkNFT.mint(_userAddress, ""); + vm.startPrank(_userAddress); + assertFalse(_testPatchworkNFT.locked(n)); + _testPatchworkNFT.setLocked(n, true); + assertTrue(_testPatchworkNFT.locked(n)); + assertFalse(_testPatchworkNFT.frozen(n)); + _testPatchworkNFT.setFrozen(n, true); + assertTrue(_testPatchworkNFT.frozen(n)); + assertTrue(_testPatchworkNFT.locked(n)); + _testPatchworkNFT.setLocked(n, false); + assertTrue(_testPatchworkNFT.frozen(n)); + assertFalse(_testPatchworkNFT.locked(n)); + _testPatchworkNFT.setFrozen(n, false); + assertFalse(_testPatchworkNFT.frozen(n)); + assertFalse(_testPatchworkNFT.locked(n)); + _testPatchworkNFT.setFrozen(n, true); + assertTrue(_testPatchworkNFT.frozen(n)); + assertFalse(_testPatchworkNFT.locked(n)); + _testPatchworkNFT.setLocked(n, true); + assertTrue(_testPatchworkNFT.frozen(n)); + assertTrue(_testPatchworkNFT.locked(n)); + } + + function testTransferFromWithFreezeNonce() public { + // TODO make sure these are calling checkTransfer on proto + uint256 n = _testPatchworkNFT.mint(_userAddress, ""); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _testPatchworkNFT.setFrozen(n, true); + vm.prank(_userAddress); + _testPatchworkNFT.setFrozen(n, true); + assertEq(_userAddress, _testPatchworkNFT.ownerOf(n)); + vm.prank(_userAddress); + _testPatchworkNFT.transferFromWithFreezeNonce(_userAddress, _user2Address, n, 0); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.prank(_user2Address); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_user2Address, _userAddress, n, 0); + assertEq(_userAddress, _testPatchworkNFT.ownerOf(n)); + vm.prank(_userAddress); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_userAddress, _user2Address, n, bytes("abcd"), 0); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + + vm.startPrank(_user2Address); + // test not frozen revert + _testPatchworkNFT.setFrozen(n, false); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotFrozen.selector, _testPatchworkNFT, n)); + _testPatchworkNFT.transferFromWithFreezeNonce(_user2Address, _userAddress, n, 1); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotFrozen.selector, _testPatchworkNFT, n)); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_user2Address, _userAddress, n, 1); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotFrozen.selector, _testPatchworkNFT, n)); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_user2Address, _userAddress, n, bytes("abcd"), 1); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + + // test incorrect nonce revert + _testPatchworkNFT.setFrozen(n, true); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectNonce.selector, _testPatchworkNFT, n, 0)); + _testPatchworkNFT.transferFromWithFreezeNonce(_user2Address, _userAddress, n, 0); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectNonce.selector, _testPatchworkNFT, n, 0)); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_user2Address, _userAddress, n, 0); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectNonce.selector, _testPatchworkNFT, n, 0)); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_user2Address, _userAddress, n, bytes("abcd"), 0); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.stopPrank(); + + // test wrong user revert + vm.startPrank(_userAddress); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, _userAddress, n)); + _testPatchworkNFT.transferFromWithFreezeNonce(_user2Address, _userAddress, n, 1); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, _userAddress, n)); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_user2Address, _userAddress, n, 1); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, _userAddress, n)); + _testPatchworkNFT.safeTransferFromWithFreezeNonce(_user2Address, _userAddress, n, bytes("abcd"), 1); + assertEq(_user2Address, _testPatchworkNFT.ownerOf(n)); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/PatchworkNFTBase.t.sol b/test/PatchworkNFTBase.t.sol deleted file mode 100644 index b28fef1..0000000 --- a/test/PatchworkNFTBase.t.sol +++ /dev/null @@ -1,287 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -import "../src/PatchworkProtocol.sol"; -import "../src/sampleNFTs/TestPatchLiteRefNFT.sol"; -import "../src/sampleNFTs/TestFragmentLiteRefNFT.sol"; -import "../src/sampleNFTs/TestBaseNFT.sol"; -import "../src/sampleNFTs/TestPatchworkNFT.sol"; - -contract PatchworkNFTBaseTest is Test { - event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); - - PatchworkProtocol prot; - TestBaseNFT testBaseNFT; - TestPatchworkNFT testPatchworkNFT; - TestPatchLiteRefNFT testPatchLiteRefNFT; - TestFragmentLiteRefNFT testFragmentLiteRefNFT; - - string scopeName; - address defaultUser; - address scopeOwner; - address patchworkOwner; - address userAddress; - address user2Address; - - function setUp() public { - defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; - patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; - userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; - user2Address = address(550001); - scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; - - vm.prank(patchworkOwner); - prot = new PatchworkProtocol(); - scopeName = "testscope"; - vm.prank(scopeOwner); - prot.claimScope(scopeName); - - vm.prank(userAddress); - testBaseNFT = new TestBaseNFT(); - - vm.prank(scopeOwner); - testPatchLiteRefNFT = new TestPatchLiteRefNFT(address(prot)); - vm.prank(scopeOwner); - testFragmentLiteRefNFT = new TestFragmentLiteRefNFT(address(prot)); - vm.prank(scopeOwner); - testPatchworkNFT = new TestPatchworkNFT(address(prot)); - } - - function testScopeName() public { - assertEq(scopeName, testPatchworkNFT.getScopeName()); - assertEq(scopeName, testPatchLiteRefNFT.getScopeName()); - assertEq(scopeName, testFragmentLiteRefNFT.getScopeName()); - } - - function testLoadStorePackedMetadataSlot() public { - testPatchworkNFT.mint(userAddress, 1); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, defaultUser)); - testPatchworkNFT.storePackedMetadataSlot(1, 0, 0x505050); - vm.prank(scopeOwner); - testPatchworkNFT.storePackedMetadataSlot(1, 0, 0x505050); - assertEq(0x505050, testPatchworkNFT.loadPackedMetadataSlot(1, 0)); - } - - function testTransferFrom() public { - // TODO make sure these are calling checkTransfer on proto - testPatchworkNFT.mint(userAddress, 1); - assertEq(userAddress, testPatchworkNFT.ownerOf(1)); - vm.prank(userAddress); - testPatchworkNFT.transferFrom(userAddress, user2Address, 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.prank(user2Address); - testPatchworkNFT.safeTransferFrom(user2Address, userAddress, 1); - assertEq(userAddress, testPatchworkNFT.ownerOf(1)); - vm.prank(userAddress); - testPatchworkNFT.safeTransferFrom(userAddress, user2Address, 1, bytes("abcd")); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - - // test wrong user revert - vm.startPrank(userAddress); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - testPatchworkNFT.transferFrom(user2Address, userAddress, 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - testPatchworkNFT.safeTransferFrom(user2Address, userAddress, 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - testPatchworkNFT.safeTransferFrom(user2Address, userAddress, 1, bytes("abcd")); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - } - - function testLockFreezeSeparation() public { - testPatchworkNFT.mint(userAddress, 1); - vm.startPrank(userAddress); - assertFalse(testPatchworkNFT.locked(1)); - testPatchworkNFT.setLocked(1, true); - assertTrue(testPatchworkNFT.locked(1)); - assertFalse(testPatchworkNFT.frozen(1)); - testPatchworkNFT.setFrozen(1, true); - assertTrue(testPatchworkNFT.frozen(1)); - assertTrue(testPatchworkNFT.locked(1)); - testPatchworkNFT.setLocked(1, false); - assertTrue(testPatchworkNFT.frozen(1)); - assertFalse(testPatchworkNFT.locked(1)); - testPatchworkNFT.setFrozen(1, false); - assertFalse(testPatchworkNFT.frozen(1)); - assertFalse(testPatchworkNFT.locked(1)); - testPatchworkNFT.setFrozen(1, true); - assertTrue(testPatchworkNFT.frozen(1)); - assertFalse(testPatchworkNFT.locked(1)); - testPatchworkNFT.setLocked(1, true); - assertTrue(testPatchworkNFT.frozen(1)); - assertTrue(testPatchworkNFT.locked(1)); - } - - function testTransferFromWithFreezeNonce() public { - // TODO make sure these are calling checkTransfer on proto - testPatchworkNFT.mint(userAddress, 1); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, defaultUser)); - testPatchworkNFT.setFrozen(1, true); - vm.prank(userAddress); - testPatchworkNFT.setFrozen(1, true); - assertEq(userAddress, testPatchworkNFT.ownerOf(1)); - vm.prank(userAddress); - testPatchworkNFT.transferFromWithFreezeNonce(userAddress, user2Address, 1, 0); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.prank(user2Address); - testPatchworkNFT.safeTransferFromWithFreezeNonce(user2Address, userAddress, 1, 0); - assertEq(userAddress, testPatchworkNFT.ownerOf(1)); - vm.prank(userAddress); - testPatchworkNFT.safeTransferFromWithFreezeNonce(userAddress, user2Address, 1, bytes("abcd"), 0); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - - vm.startPrank(user2Address); - // test not frozen revert - testPatchworkNFT.setFrozen(1, false); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotFrozen.selector, testPatchworkNFT, 1)); - testPatchworkNFT.transferFromWithFreezeNonce(user2Address, userAddress, 1, 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotFrozen.selector, testPatchworkNFT, 1)); - testPatchworkNFT.safeTransferFromWithFreezeNonce(user2Address, userAddress, 1, 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotFrozen.selector, testPatchworkNFT, 1)); - testPatchworkNFT.safeTransferFromWithFreezeNonce(user2Address, userAddress, 1, bytes("abcd"), 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - - // test incorrect nonce revert - testPatchworkNFT.setFrozen(1, true); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.IncorrectNonce.selector, testPatchworkNFT, 1, 0)); - testPatchworkNFT.transferFromWithFreezeNonce(user2Address, userAddress, 1, 0); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.IncorrectNonce.selector, testPatchworkNFT, 1, 0)); - testPatchworkNFT.safeTransferFromWithFreezeNonce(user2Address, userAddress, 1, 0); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.IncorrectNonce.selector, testPatchworkNFT, 1, 0)); - testPatchworkNFT.safeTransferFromWithFreezeNonce(user2Address, userAddress, 1, bytes("abcd"), 0); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.stopPrank(); - - // test wrong user revert - vm.startPrank(userAddress); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - testPatchworkNFT.transferFromWithFreezeNonce(user2Address, userAddress, 1, 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - testPatchworkNFT.safeTransferFromWithFreezeNonce(user2Address, userAddress, 1, 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - testPatchworkNFT.safeTransferFromWithFreezeNonce(user2Address, userAddress, 1, bytes("abcd"), 1); - assertEq(user2Address, testPatchworkNFT.ownerOf(1)); - vm.stopPrank(); - } - - function testLocks() public { - uint256 baseTokenId = testBaseNFT.mint(userAddress); - vm.prank(scopeOwner); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), baseTokenId, address(testPatchLiteRefNFT)); - bool locked = testPatchLiteRefNFT.locked(patchTokenId); - assertFalse(locked); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.CannotLockSoulboundPatch.selector, testPatchLiteRefNFT)); - testPatchLiteRefNFT.setLocked(patchTokenId, true); - } - - function testReferenceAddresses() public { - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, defaultUser)); - uint8 refIdx = testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - (uint64 ref, bool redacted) = testPatchLiteRefNFT.getLiteReference(address(testFragmentLiteRefNFT), 1); - assertEq(0, ref); - vm.prank(scopeOwner); - refIdx = testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - (ref, redacted) = testPatchLiteRefNFT.getLiteReference(address(testFragmentLiteRefNFT), 1); - (address refAddr, uint256 tokenId) = testPatchLiteRefNFT.getReferenceAddressAndTokenId(ref); - assertEq(address(testFragmentLiteRefNFT), refAddr); - assertEq(1, tokenId); - - // test assign perms - uint256 baseTokenId = testBaseNFT.mint(userAddress); - uint256 fragmentTokenId = testFragmentLiteRefNFT.mint(userAddress); - assertEq(userAddress, testFragmentLiteRefNFT.ownerOf(fragmentTokenId)); // TODO why doesn't this cover the branch != address(0) - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), baseTokenId, address(testPatchLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); // must have user patch enabled - patchTokenId = prot.createPatch(address(testBaseNFT), baseTokenId, address(testPatchLiteRefNFT)); - vm.prank(scopeOwner); - patchTokenId = prot.createPatch(address(testBaseNFT), baseTokenId, address(testPatchLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); // can't call directly - testFragmentLiteRefNFT.assign(fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); // must be owner/manager - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - - vm.prank(scopeOwner); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - assertEq(userAddress, testFragmentLiteRefNFT.ownerOf(fragmentTokenId)); // TODO why doesn't this cover the branch != address(0) - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentAlreadyAssigned.selector, address(testFragmentLiteRefNFT), fragmentTokenId)); - vm.prank(scopeOwner); // not normal to call directly but need to test the correct error - testFragmentLiteRefNFT.assign(fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); // can't call directly - testFragmentLiteRefNFT.unassign(fragmentTokenId); - - uint256 newFrag = testFragmentLiteRefNFT.mint(userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, defaultUser)); - testPatchLiteRefNFT.redactReferenceAddress(refIdx); - vm.prank(scopeOwner); - testPatchLiteRefNFT.redactReferenceAddress(refIdx); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentRedacted.selector, address(testFragmentLiteRefNFT))); - vm.prank(scopeOwner); - prot.assignNFT(address(testFragmentLiteRefNFT), newFrag, address(testPatchLiteRefNFT), patchTokenId); - - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, defaultUser)); - testPatchLiteRefNFT.unredactReferenceAddress(refIdx); - vm.prank(scopeOwner); - testPatchLiteRefNFT.unredactReferenceAddress(refIdx); - vm.prank(scopeOwner); - prot.assignNFT(address(testFragmentLiteRefNFT), newFrag, address(testPatchLiteRefNFT), patchTokenId); - } - - function testReferenceAddressErrors() public { - vm.startPrank(scopeOwner); - uint8 refIdx = testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - assertEq(1, refIdx); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentAlreadyRegistered.selector, address(testFragmentLiteRefNFT))); - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - // Fill ID 2 to 254 then test overflow - for (uint8 i = 2; i < 255; i++) { - refIdx = testPatchLiteRefNFT.registerReferenceAddress(address(bytes20(uint160(i)))); - } - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.OutOfIDs.selector)); - refIdx = testPatchLiteRefNFT.registerReferenceAddress(address(256)); - } - - function testOnAssignedTransferError() public { - vm.expectRevert(); - testFragmentLiteRefNFT.onAssignedTransfer(address(0), address(1), 1); - } - - function testPatchworkCompatible() public { - bytes1 r1 = testPatchLiteRefNFT.patchworkCompatible_(); - assertEq(0, r1); - bytes2 r2 = testFragmentLiteRefNFT.patchworkCompatible_(); - assertEq(0, r2); - } - - function testLiteref56bitlimit() public { - vm.prank(scopeOwner); - uint8 r1 = testFragmentLiteRefNFT.registerReferenceAddress(address(1)); - (uint64 ref, bool redacted) = testFragmentLiteRefNFT.getLiteReference(address(1), 1); - assertEq((uint256(r1) << 56) + 1, ref); - (ref, redacted) = testFragmentLiteRefNFT.getLiteReference(address(1), 0xFFFFFFFFFFFFFF); - assertEq((uint256(r1) << 56) + 0xFFFFFFFFFFFFFF, ref); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.UnsupportedTokenId.selector, 1 << 56)); - testFragmentLiteRefNFT.getLiteReference(address(1), 1 << 56); - } - -} \ No newline at end of file diff --git a/test/PatchworkNFTInterface.t.sol b/test/PatchworkNFTInterface.t.sol deleted file mode 100644 index 2859e7a..0000000 --- a/test/PatchworkNFTInterface.t.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -import "../src/PatchworkNFTInterface.sol"; -import "../src/sampleNFTs/TestPatchLiteRefNFT.sol"; -import "../src/sampleNFTs/TestFragmentLiteRefNFT.sol"; - -contract PatchworkNFTInterfaceTest is Test { - TestPatchLiteRefNFT testPatchLiteRefNFT; - TestFragmentLiteRefNFT testFragmentLiteRefNFT; - - function setUp() public { - testPatchLiteRefNFT = new TestPatchLiteRefNFT(address(1)); - testFragmentLiteRefNFT = new TestFragmentLiteRefNFT(0x0000000000000000000000000000000000000000); - } - - function testSupportsInterface() public { - assertTrue(testPatchLiteRefNFT.supportsInterface(type(IERC165).interfaceId)); - assertTrue(testPatchLiteRefNFT.supportsInterface(type(IPatchworkNFT).interfaceId)); - assertTrue(testPatchLiteRefNFT.supportsInterface(type(IPatchworkLiteRef).interfaceId)); - assertTrue(testPatchLiteRefNFT.supportsInterface(type(IPatchworkPatch).interfaceId)); - - assertTrue(testFragmentLiteRefNFT.supportsInterface(type(IERC165).interfaceId)); - assertTrue(testFragmentLiteRefNFT.supportsInterface(type(IPatchworkNFT).interfaceId)); - assertTrue(testFragmentLiteRefNFT.supportsInterface(type(IPatchworkAssignableNFT).interfaceId)); - } - -} \ No newline at end of file diff --git a/test/PatchworkNFTReferences.t.sol b/test/PatchworkNFTReferences.t.sol new file mode 100644 index 0000000..2cf0053 --- /dev/null +++ b/test/PatchworkNFTReferences.t.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestPatchLiteRefNFT.sol"; +import "./nfts/TestFragmentLiteRefNFT.sol"; +import "./nfts/TestBaseNFT.sol"; +import "./nfts/TestFragmentSingleNFT.sol"; + +contract PatchworkNFTCombinedTest is Test { + PatchworkProtocol _prot; + TestBaseNFT _testBaseNFT; + TestPatchLiteRefNFT _testPatchLiteRefNFT; + TestFragmentLiteRefNFT _testFragmentLiteRefNFT; + TestFragmentSingleNFT _testFragmentSingleNFT; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + _scopeName = "testscope"; + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + _testPatchLiteRefNFT = new TestPatchLiteRefNFT(address(_prot)); + _testFragmentLiteRefNFT = new TestFragmentLiteRefNFT(address(_prot)); + _testFragmentSingleNFT = new TestFragmentSingleNFT(address(_prot)); + + vm.stopPrank(); + vm.prank(_userAddress); + _testBaseNFT = new TestBaseNFT(); + } + + function testReferenceAddresses() public { + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + uint8 refIdx = _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + (uint64 ref, bool redacted) = _testPatchLiteRefNFT.getLiteReference(address(_testFragmentLiteRefNFT), 1); + assertEq(0, ref); + vm.prank(_scopeOwner); + refIdx = _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + (uint8 _id, bool _redacted) = _testPatchLiteRefNFT.getReferenceId(address(_testFragmentLiteRefNFT)); + assertEq(refIdx, _id); + assertFalse(_redacted); + (address _addr, bool _redacted2) = _testPatchLiteRefNFT.getReferenceAddress(refIdx); + assertEq(address(_testFragmentLiteRefNFT), _addr); + assertFalse(_redacted2); + (ref, redacted) = _testPatchLiteRefNFT.getLiteReference(address(_testFragmentLiteRefNFT), 1); + (address refAddr, uint256 tokenId) = _testPatchLiteRefNFT.getReferenceAddressAndTokenId(ref); + assertEq(address(_testFragmentLiteRefNFT), refAddr); + assertEq(1, tokenId); + + vm.prank(_scopeOwner); + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentSingleNFT)); + // test assign perms + uint256 baseTokenId = _testBaseNFT.mint(_userAddress); + uint256 fragmentTokenId = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragSingleTokenId = _testFragmentSingleNFT.mint(_userAddress, ""); + assertEq(_userAddress, _testFragmentSingleNFT.ownerOf(fragSingleTokenId)); + assertEq(_userAddress, _testFragmentLiteRefNFT.ownerOf(fragmentTokenId)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); // must have user patch enabled + patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + vm.prank(_scopeOwner); + patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); // can't call directly + _testFragmentLiteRefNFT.assign(fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); // must be owner/manager + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + + vm.prank(_scopeOwner); + _prot.assign(address(_testFragmentSingleNFT), fragSingleTokenId, address(_testPatchLiteRefNFT), patchTokenId); + assertEq(_userAddress, _testFragmentSingleNFT.ownerOf(fragSingleTokenId)); + vm.prank(_scopeOwner); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + assertEq(_userAddress, _testFragmentLiteRefNFT.ownerOf(fragmentTokenId)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentAlreadyAssigned.selector, address(_testFragmentLiteRefNFT), fragmentTokenId)); + vm.prank(_scopeOwner); // not normal to call directly but need to test the correct error + _testFragmentLiteRefNFT.assign(fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); // can't call directly + _testFragmentLiteRefNFT.unassign(fragmentTokenId); + + uint256 newFrag = _testFragmentLiteRefNFT.mint(_userAddress, ""); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _testPatchLiteRefNFT.redactReferenceAddress(refIdx); + vm.prank(_scopeOwner); + _testPatchLiteRefNFT.redactReferenceAddress(refIdx); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentRedacted.selector, address(_testFragmentLiteRefNFT))); + vm.prank(_scopeOwner); + _prot.assign(address(_testFragmentLiteRefNFT), newFrag, address(_testPatchLiteRefNFT), patchTokenId); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _testPatchLiteRefNFT.unredactReferenceAddress(refIdx); + vm.prank(_scopeOwner); + _testPatchLiteRefNFT.unredactReferenceAddress(refIdx); + vm.prank(_scopeOwner); + _prot.assign(address(_testFragmentLiteRefNFT), newFrag, address(_testPatchLiteRefNFT), patchTokenId); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentUnregistered.selector, address(0))); + vm.prank(_scopeOwner); + _testPatchLiteRefNFT.redactReferenceAddress(100); + + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentUnregistered.selector, address(0))); + vm.prank(_scopeOwner); + _testPatchLiteRefNFT.unredactReferenceAddress(100); + } + + function testReferenceAddressErrors() public { + vm.startPrank(_scopeOwner); + uint8 refIdx = _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + assertEq(1, refIdx); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentAlreadyRegistered.selector, address(_testFragmentLiteRefNFT))); + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + // Fill ID 2 to 254 then test overflow + for (uint8 i = 2; i < 255; i++) { + refIdx = _testPatchLiteRefNFT.registerReferenceAddress(address(bytes20(uint160(i)))); + } + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.OutOfIDs.selector)); + refIdx = _testPatchLiteRefNFT.registerReferenceAddress(address(256)); + } +} \ No newline at end of file diff --git a/test/PatchworkPatch.t.sol b/test/PatchworkPatch.t.sol new file mode 100644 index 0000000..b2278c8 --- /dev/null +++ b/test/PatchworkPatch.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestPatchLiteRefNFT.sol"; +import "./nfts/TestBaseNFT.sol"; +import "./nfts/TestPatchFragmentNFT.sol"; + +contract PatchworkPatchTest is Test { + PatchworkProtocol _prot; + TestBaseNFT _testBaseNFT; + TestPatchLiteRefNFT _testPatchLiteRefNFT; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + PatchworkProtocolAssigner assigner = new PatchworkProtocolAssigner(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(assigner)); + _scopeName = "testscope"; + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + _testPatchLiteRefNFT = new TestPatchLiteRefNFT(address(_prot)); + + vm.stopPrank(); + vm.prank(_userAddress); + _testBaseNFT = new TestBaseNFT(); + } + + function testScopeName() public { + assertEq(_scopeName, _testPatchLiteRefNFT.getScopeName()); + } + + function testSupportsInterface() public { + assertTrue(_testPatchLiteRefNFT.supportsInterface(type(IERC165).interfaceId)); + assertTrue(_testPatchLiteRefNFT.supportsInterface(type(IERC721).interfaceId)); + assertTrue(_testPatchLiteRefNFT.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(_testPatchLiteRefNFT.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(_testPatchLiteRefNFT.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(_testPatchLiteRefNFT.supportsInterface(type(IPatchworkLiteRef).interfaceId)); + assertTrue(_testPatchLiteRefNFT.supportsInterface(type(IPatchworkPatch).interfaceId)); + assertFalse(_testPatchLiteRefNFT.supportsInterface(type(IPatchworkReversiblePatch).interfaceId)); + TestPatchFragmentNFT testPatchFragmentNFT = new TestPatchFragmentNFT(address(_prot)); + assertTrue(testPatchFragmentNFT.supportsInterface(type(IERC165).interfaceId)); + assertTrue(testPatchFragmentNFT.supportsInterface(type(IERC721).interfaceId)); + assertTrue(testPatchFragmentNFT.supportsInterface(type(IERC4906).interfaceId)); + assertTrue(testPatchFragmentNFT.supportsInterface(type(IERC5192).interfaceId)); + assertTrue(testPatchFragmentNFT.supportsInterface(type(IPatchwork721).interfaceId)); + assertTrue(testPatchFragmentNFT.supportsInterface(type(IPatchworkPatch).interfaceId)); + assertTrue(testPatchFragmentNFT.supportsInterface(type(IPatchworkReversiblePatch).interfaceId)); + } + + function testLocks() public { + uint256 baseTokenId = _testBaseNFT.mint(_userAddress); + vm.prank(_scopeOwner); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + bool locked = _testPatchLiteRefNFT.locked(patchTokenId); + assertFalse(locked); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.CannotLockSoulboundPatch.selector, _testPatchLiteRefNFT)); + _testPatchLiteRefNFT.setLocked(patchTokenId, true); + } + + function testBurn() public { + uint256 baseTokenId = _testBaseNFT.mint(_userAddress); + vm.prank(_scopeOwner); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + _testPatchLiteRefNFT.burn(patchTokenId); + // Should be able to re-patch now + vm.prank(_scopeOwner); + patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + } + + function testOtherOwnerDisallowed() public { + uint256 baseTokenId = _testBaseNFT.mint(_userAddress); + vm.prank(_scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + _prot.patch(_user2Address, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + } + + function testPatchFragment() public { + vm.startPrank(_scopeOwner); + uint256 baseTokenId = _testBaseNFT.mint(_userAddress); + uint256 baseTokenId2 = _testBaseNFT.mint(_user2Address); + uint256 baseTokenId3 = _testBaseNFT.mint(_userAddress); + TestPatchFragmentNFT testPatchFragmentNFT = new TestPatchFragmentNFT(address(_prot)); + _testPatchLiteRefNFT.registerReferenceAddress(address(testPatchFragmentNFT)); + uint256 liteRefId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId, address(_testPatchLiteRefNFT)); + uint256 liteRefId2 = _prot.patch(_user2Address, address(_testBaseNFT), baseTokenId2, address(_testPatchLiteRefNFT)); + uint256 fragmentTokenId = _prot.patch(_userAddress, address(_testBaseNFT), baseTokenId3, address(testPatchFragmentNFT)); + // check reverse lookups + assertEq(fragmentTokenId, testPatchFragmentNFT.getTokenIdByTarget(IPatchworkPatch.PatchTarget(address(_testBaseNFT), baseTokenId3))); + // cannot assign patch to a literef that this person does not own + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _scopeOwner)); + _prot.assign(address(testPatchFragmentNFT), fragmentTokenId, address(_testPatchLiteRefNFT), liteRefId2); + // can assign to same owner + _prot.assign(address(testPatchFragmentNFT), fragmentTokenId, address(_testPatchLiteRefNFT), liteRefId); + // transfer the underlying patched nft and check ownership + vm.stopPrank(); + assertEq(_userAddress, _testBaseNFT.ownerOf(baseTokenId)); + assertEq(_userAddress, _testPatchLiteRefNFT.ownerOf(baseTokenId)); + assertEq(_userAddress, testPatchFragmentNFT.ownerOf(fragmentTokenId)); + vm.prank(_userAddress); + _testBaseNFT.transferFrom(_userAddress, _user2Address, baseTokenId); + assertEq(_user2Address, _testBaseNFT.ownerOf(baseTokenId)); + assertEq(_user2Address, _testPatchLiteRefNFT.ownerOf(baseTokenId)); + assertEq(_userAddress, testPatchFragmentNFT.ownerOf(fragmentTokenId)); + vm.prank(_userAddress); + _testBaseNFT.transferFrom(_userAddress, _user2Address, baseTokenId3); + assertEq(_user2Address, testPatchFragmentNFT.ownerOf(fragmentTokenId)); + _prot.updateOwnershipTree(address(testPatchFragmentNFT), fragmentTokenId); + assertEq(_user2Address, testPatchFragmentNFT.ownerOf(fragmentTokenId)); + } +} \ No newline at end of file diff --git a/test/PatchworkProtocol.t.sol b/test/PatchworkProtocol.t.sol index e9201d6..a867a60 100644 --- a/test/PatchworkProtocol.t.sol +++ b/test/PatchworkProtocol.t.sol @@ -1,768 +1,916 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "../src/PatchworkProtocol.sol"; -import "../src/sampleNFTs/TestPatchLiteRefNFT.sol"; -import "../src/sampleNFTs/TestFragmentLiteRefNFT.sol"; -import "../src/sampleNFTs/TestBaseNFT.sol"; -import "../src/sampleNFTs/TestPatchworkNFT.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestPatchLiteRefNFT.sol"; +import "./nfts/TestFragmentLiteRefNFT.sol"; +import "./nfts/TestBaseNFT.sol"; +import "./nfts/TestPatchworkNFT.sol"; +import "./nfts/TestMultiFragmentNFT.sol"; +import "./nfts/TestPatchNFT.sol"; contract PatchworkProtocolTest is Test { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); - PatchworkProtocol prot; - TestBaseNFT testBaseNFT; - TestPatchworkNFT testPatchworkNFT; - TestPatchLiteRefNFT testPatchLiteRefNFT; - TestFragmentLiteRefNFT testFragmentLiteRefNFT; + PatchworkProtocol _prot; + TestBaseNFT _testBaseNFT; + TestPatchworkNFT _testPatchworkNFT; + TestPatchLiteRefNFT _testPatchLiteRefNFT; + TestFragmentLiteRefNFT _testFragmentLiteRefNFT; + TestMultiFragmentNFT _testMultiFragmentNFT; - string scopeName; - address defaultUser; - address patchworkOwner; - address userAddress; - address user2Address; - address scopeOwner; + string _scopeName; + address _defaultUser; + address _patchworkOwner; + address _userAddress; + address _user2Address; + address _scopeOwner; function setUp() public { - defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; - patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; - userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; - user2Address = address(550001); - scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; - - vm.prank(patchworkOwner); - prot = new PatchworkProtocol(); - scopeName = "testscope"; - - vm.prank(userAddress); - testBaseNFT = new TestBaseNFT(); - - vm.prank(scopeOwner); - testPatchLiteRefNFT = new TestPatchLiteRefNFT(address(prot)); - vm.prank(scopeOwner); - testFragmentLiteRefNFT = new TestFragmentLiteRefNFT(address(prot)); - vm.prank(scopeOwner); - testPatchworkNFT = new TestPatchworkNFT(address(prot)); + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + _scopeName = "testscope"; + + vm.prank(_userAddress); + _testBaseNFT = new TestBaseNFT(); + + vm.prank(_scopeOwner); + _testPatchLiteRefNFT = new TestPatchLiteRefNFT(address(_prot)); + vm.prank(_scopeOwner); + _testFragmentLiteRefNFT = new TestFragmentLiteRefNFT(address(_prot)); + vm.prank(_scopeOwner); + _testPatchworkNFT = new TestPatchworkNFT(address(_prot)); + vm.prank(_scopeOwner); + _testMultiFragmentNFT = new TestMultiFragmentNFT(address(_prot)); } function testScopeOwnerOperator() public { - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - assertEq(prot.getScopeOwner(scopeName), scopeOwner); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.ScopeExists.selector, scopeName)); - prot.claimScope(scopeName); + vm.startPrank(_scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _scopeOwner)); + _prot.claimScope(""); + _prot.claimScope(_scopeName); + assertEq(_prot.getScopeOwner(_scopeName), _scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ScopeExists.selector, _scopeName)); + _prot.claimScope(_scopeName); vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, defaultUser)); - prot.transferScopeOwnership(scopeName, address(2)); - vm.prank(scopeOwner); - prot.transferScopeOwnership(scopeName, address(2)); - assertEq(prot.getScopeOwner(scopeName), address(2)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, scopeOwner)); - vm.prank(scopeOwner); - prot.transferScopeOwnership(scopeName, address(2)); + // Current user is not scope owner so can't transfer it + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _prot.transferScopeOwnership(_scopeName, address(2)); + // Real owner can transfer it + vm.prank(_scopeOwner); + _prot.transferScopeOwnership(_scopeName, address(3)); + // _scopeOwner still owns it until it's accepted + assertEq(_prot.getScopeOwner(_scopeName), _scopeOwner); + assertEq(_prot.getScopeOwnerElect(_scopeName), address(3)); + // test changing the pending transfer elect + vm.prank(_scopeOwner); + _prot.transferScopeOwnership(_scopeName, address(2)); + assertEq(_prot.getScopeOwnerElect(_scopeName), address(2)); + // Non-owner may not cancel the transfer + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, address(10))); + vm.prank(address(10)); + _prot.cancelScopeTransfer(_scopeName); + // Real owner can cancel the transfer + vm.prank(_scopeOwner); + _prot.cancelScopeTransfer(_scopeName); + assertEq(_prot.getScopeOwnerElect(_scopeName), address(0)); + // Now retry the transfer + vm.prank(_scopeOwner); + _prot.transferScopeOwnership(_scopeName, address(2)); + // User 10 is not elect and may not accept the transfer + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, address(10))); + vm.prank(address(10)); + _prot.acceptScopeTransfer(_scopeName); + // Finally real elect accepts scope transfer vm.prank(address(2)); - prot.transferScopeOwnership(scopeName, scopeOwner); - assertEq(prot.getScopeOwner(scopeName), scopeOwner); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, address(2))); + _prot.acceptScopeTransfer(_scopeName); + assertEq(_prot.getScopeOwner(_scopeName), address(2)); + assertEq(_prot.getScopeOwnerElect(_scopeName), address(0)); + // Old owner may not transfer it + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _scopeOwner)); + vm.prank(_scopeOwner); + _prot.transferScopeOwnership(_scopeName, address(2)); + // New owner may transfer it back to old owner vm.prank(address(2)); - prot.addOperator(scopeName, address(2)); - vm.prank(scopeOwner); - prot.addOperator(scopeName, address(2)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, address(2))); + _prot.transferScopeOwnership(_scopeName, _scopeOwner); + vm.prank(_scopeOwner); + _prot.acceptScopeTransfer(_scopeName); + assertEq(_prot.getScopeOwner(_scopeName), _scopeOwner); + // Non-owner may not add operator + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, address(2))); vm.prank(address(2)); - prot.removeOperator(scopeName, address(2)); - vm.prank(scopeOwner); - prot.removeOperator(scopeName, address(2)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, address(2))); + _prot.addOperator(_scopeName, address(2)); + // Real owner may add operator + vm.prank(_scopeOwner); + _prot.addOperator(_scopeName, address(2)); + // Non-owner may not remove operator + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, address(2))); vm.prank(address(2)); - prot.setScopeRules(scopeName, true, true, true); + _prot.removeOperator(_scopeName, address(2)); + // Real owner may remove operator + vm.prank(_scopeOwner); + _prot.removeOperator(_scopeName, address(2)); + // Non-owner may not set scope rules + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, address(2))); + vm.prank(address(2)); + _prot.setScopeRules(_scopeName, true, true, true); } function testCreatePatchNFTNoVerification() public { - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - uint256 tokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + uint256 tokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); assertEq(tokenId, 0); } function testCreatePatchNFTUnverified() public { - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - prot.setScopeRules(scopeName, false, false, true); - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotWhitelisted.selector, scopeName, address(testPatchLiteRefNFT))); - prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(_testPatchLiteRefNFT))); + _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); } function testCreatePatchNFTVerified() public { - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - prot.setScopeRules(scopeName, false, false, true); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); - prot.addWhitelist(scopeName, address(testPatchLiteRefNFT)); - vm.startPrank(scopeOwner); - prot.addWhitelist(scopeName, address(testPatchLiteRefNFT)); - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - uint256 tokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.addWhitelist(_scopeName, address(_testPatchLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.addWhitelist(_scopeName, address(_testPatchLiteRefNFT)); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + uint256 tokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); assertEq(tokenId, 0); vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); - prot.removeWhitelist(scopeName, address(testPatchLiteRefNFT)); - vm.startPrank(scopeOwner); - prot.removeWhitelist(scopeName, address(testPatchLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotWhitelisted.selector, scopeName, address(testPatchLiteRefNFT))); - tokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId + 1, address(testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.removeWhitelist(_scopeName, address(_testPatchLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.removeWhitelist(_scopeName, address(_testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(_testPatchLiteRefNFT))); + tokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId + 1, address(_testPatchLiteRefNFT)); } function testUserPermissions() public { - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - prot.setScopeRules(scopeName, false, false, true); - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - prot.addWhitelist(scopeName, address(testPatchLiteRefNFT)); - prot.addWhitelist(scopeName, address(testFragmentLiteRefNFT)); - uint256 fragmentTokenId = testFragmentLiteRefNFT.mint(userAddress); - assertEq(testFragmentLiteRefNFT.ownerOf(fragmentTokenId), userAddress); - //Register artifactNFT to testPatchLiteRefNFT - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + _prot.addWhitelist(_scopeName, address(_testPatchLiteRefNFT)); + _prot.addWhitelist(_scopeName, address(_testFragmentLiteRefNFT)); + uint256 fragmentTokenId = _testFragmentLiteRefNFT.mint(_userAddress, ""); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragmentTokenId), _userAddress); + //Register artifactNFT to _testPatchLiteRefNFT + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); vm.stopPrank(); - vm.startPrank(userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.startPrank(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); vm.stopPrank(); - vm.prank(scopeOwner); - prot.setScopeRules(scopeName, true, false, true); - vm.startPrank(userAddress); - patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); + vm.prank(_scopeOwner); + _prot.setScopeRules(_scopeName, true, false, true); + vm.startPrank(_userAddress); + patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); vm.stopPrank(); - vm.prank(scopeOwner); - prot.setScopeRules(scopeName, true, true, true); - vm.prank(userAddress); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); + vm.prank(_scopeOwner); + _prot.setScopeRules(_scopeName, true, true, true); + vm.prank(_userAddress); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); // expect revert - vm.prank(userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.TransferBlockedByAssignment.selector, testFragmentLiteRefNFT, fragmentTokenId)); - testFragmentLiteRefNFT.transferFrom(userAddress, address(5), fragmentTokenId); + vm.prank(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.TransferBlockedByAssignment.selector, _testFragmentLiteRefNFT, fragmentTokenId)); + _testFragmentLiteRefNFT.transferFrom(_userAddress, address(5), fragmentTokenId); } function testAssignNFT() public { vm.expectRevert(); // not assignable - prot.assignNFT(address(1), 1, address(1), 1); - - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.ScopeDoesNotExist.selector, scopeName)); - prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); - - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.AlreadyPatched.selector, address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT))); - patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + _prot.assign(address(1), 1, address(1), 1); + + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ScopeDoesNotExist.selector, _scopeName)); + _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); + + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.AlreadyPatched.selector, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT))); + patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); - uint256 fragmentTokenId = testFragmentLiteRefNFT.mint(userAddress); - assertEq(testFragmentLiteRefNFT.ownerOf(fragmentTokenId), userAddress); + uint256 fragmentTokenId = _testFragmentLiteRefNFT.mint(_userAddress, ""); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragmentTokenId), _userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentUnregistered.selector, address(testFragmentLiteRefNFT))); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentUnregistered.selector, address(_testFragmentLiteRefNFT))); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); - //Register artifactNFT to testPatchLiteRefNFT - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + //Register artifactNFT to _testPatchLiteRefNFT + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.SelfAssignmentNotAllowed.selector, address(testFragmentLiteRefNFT), fragmentTokenId)); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testFragmentLiteRefNFT), fragmentTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.SelfAssignmentNotAllowed.selector, address(_testFragmentLiteRefNFT), fragmentTokenId)); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testFragmentLiteRefNFT), fragmentTokenId); vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); // cover called from non-owner/op with no allowUserAssign - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId, 0); - vm.startPrank(scopeOwner); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - (address addr, uint256 tokenId) = testFragmentLiteRefNFT.getAssignedTo(fragmentTokenId); - assertEq(addr, address(testPatchLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId, 0); + (address addr, uint256 tokenId) = _testFragmentLiteRefNFT.getAssignedTo(fragmentTokenId); + assertEq(addr, address(_testPatchLiteRefNFT)); assertEq(tokenId, patchTokenId); - assertEq(testFragmentLiteRefNFT.ownerOf(fragmentTokenId), userAddress); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragmentTokenId), _userAddress); - testFragmentLiteRefNFT.setTestLockOverride(true); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentAlreadyAssignedInScope.selector, scopeName, address(testFragmentLiteRefNFT), fragmentTokenId)); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - testFragmentLiteRefNFT.setTestLockOverride(false); + _testFragmentLiteRefNFT.setTestLockOverride(true); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentAlreadyAssigned.selector, address(_testFragmentLiteRefNFT), fragmentTokenId)); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + _testFragmentLiteRefNFT.setTestLockOverride(false); vm.stopPrank(); - vm.prank(userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.SoulboundTransferNotAllowed.selector, address(testPatchLiteRefNFT), patchTokenId)); - testPatchLiteRefNFT.transferFrom(userAddress, user2Address, patchTokenId); + vm.prank(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.TransferNotAllowed.selector, address(_testPatchLiteRefNFT), patchTokenId)); + _testPatchLiteRefNFT.transferFrom(_userAddress, _user2Address, patchTokenId); } function testScopeDoesNotExist() public { - vm.startPrank(scopeOwner); + vm.startPrank(_scopeOwner); - uint256 fragmentTokenId1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragmentTokenId2 = testFragmentLiteRefNFT.mint(userAddress); - //Register testPatchLiteRefNFT to testPatchLiteRefNFT - testFragmentLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.ScopeDoesNotExist.selector, scopeName)); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId1, address(testFragmentLiteRefNFT), fragmentTokenId2); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.ScopeDoesNotExist.selector, scopeName)); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragmentTokenId1); + uint256 fragmentTokenId1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragmentTokenId2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 multi1 = _testMultiFragmentNFT.mint(_userAddress, ""); + //Register _testPatchLiteRefNFT to _testPatchLiteRefNFT + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testMultiFragmentNFT)); + // Single + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ScopeDoesNotExist.selector, _scopeName)); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId1, address(_testFragmentLiteRefNFT), fragmentTokenId2); + // Multi + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ScopeDoesNotExist.selector, _scopeName)); + _prot.assign(address(_testMultiFragmentNFT), multi1, address(_testFragmentLiteRefNFT), fragmentTokenId2); address[] memory fragmentAddresses = new address[](1); uint256[] memory fragments = new uint256[](1); - fragmentAddresses[0] = address(testFragmentLiteRefNFT); + fragmentAddresses[0] = address(_testFragmentLiteRefNFT); fragments[0] = fragmentTokenId1; - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.ScopeDoesNotExist.selector, scopeName)); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testFragmentLiteRefNFT), fragmentTokenId2); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ScopeDoesNotExist.selector, _scopeName)); + _prot.assignBatch(fragmentAddresses, fragments, address(_testFragmentLiteRefNFT), fragmentTokenId2); + // Claim scope to get assignments done to test unassign + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId1, address(_testFragmentLiteRefNFT), fragmentTokenId2); + _prot.assign(address(_testMultiFragmentNFT), multi1, address(_testFragmentLiteRefNFT), fragmentTokenId2); + // Memoization will prevent the scope change from taking effect. + _testFragmentLiteRefNFT.setScopeName("foo"); + _testMultiFragmentNFT.setScopeName("foo"); + _prot.unassign(address(_testFragmentLiteRefNFT), fragmentTokenId1, address(_testFragmentLiteRefNFT), fragmentTokenId2); + _prot.unassign(address(_testMultiFragmentNFT), multi1, address(_testFragmentLiteRefNFT), fragmentTokenId2, 0); + } + + function testUnsupportedNFTUnassign() public { + uint256 t1 = _testBaseNFT.mint(_userAddress); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.UnsupportedContract.selector)); + _prot.unassign(address(_testBaseNFT), t1, address(_testBaseNFT), t1); } function testScopeTransferCannotBeFrontrun() public { address maliciousActor = address(120938); // A malicious actor attempts to preconfigure and transfer a scope to 0 so an unsuspecting actor claims it but it already has operators preconfigured vm.startPrank(maliciousActor); - prot.claimScope("foo"); - prot.addOperator("foo", address(4)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.ScopeTransferNotAllowed.selector, address(0))); - prot.transferScopeOwnership("foo", address(0)); + _prot.claimScope("foo"); + _prot.addOperator("foo", address(4)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.ScopeTransferNotAllowed.selector, address(0))); + _prot.transferScopeOwnership("foo", address(0)); } function testUserAssignNFT() public { - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - prot.setScopeRules(scopeName, true, true, true); - prot.addWhitelist(scopeName, address(testPatchLiteRefNFT)); - //Register artifactNFT to testPatchLiteRefNFT - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, true, true, true); + _prot.addWhitelist(_scopeName, address(_testPatchLiteRefNFT)); + //Register artifactNFT to _testPatchLiteRefNFT + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); vm.stopPrank(); - vm.startPrank(userAddress); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.startPrank(_userAddress); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); - uint256 fragmentTokenId = testFragmentLiteRefNFT.mint(userAddress); - uint256 user2FragmentTokenId = testFragmentLiteRefNFT.mint(user2Address); - assertEq(testFragmentLiteRefNFT.ownerOf(fragmentTokenId), userAddress); + uint256 fragmentTokenId = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 user2FragmentTokenId = _testFragmentLiteRefNFT.mint(_user2Address, ""); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragmentTokenId), _userAddress); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotWhitelisted.selector, scopeName, address(testFragmentLiteRefNFT))); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); + // Not whitelisted + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(_testFragmentLiteRefNFT))); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); vm.stopPrank(); - vm.prank(scopeOwner); - prot.addWhitelist(scopeName, address(testFragmentLiteRefNFT)); - - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - prot.assignNFT(address(testFragmentLiteRefNFT), user2FragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.startPrank(userAddress); - prot.assignNFT(address(testFragmentLiteRefNFT), user2FragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); - (address addr, uint256 tokenId) = testFragmentLiteRefNFT.getAssignedTo(fragmentTokenId); - assertEq(addr, address(testPatchLiteRefNFT)); + vm.prank(_scopeOwner); + _prot.addWhitelist(_scopeName, address(_testFragmentLiteRefNFT)); + + // user 2 does not own either of these + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + + // fragment and target have different owners + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.assign(address(_testFragmentLiteRefNFT), user2FragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + + // fragment and target have different owners + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.startPrank(_userAddress); + _prot.assign(address(_testFragmentLiteRefNFT), user2FragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); + (address addr, uint256 tokenId) = _testFragmentLiteRefNFT.getAssignedTo(fragmentTokenId); + assertEq(addr, address(_testPatchLiteRefNFT)); assertEq(tokenId, patchTokenId); - assertEq(testFragmentLiteRefNFT.ownerOf(fragmentTokenId), userAddress); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragmentTokenId), _userAddress); vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragmentTokenId); - vm.startPrank(userAddress); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragmentTokenId); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentNotAssigned.selector, address(testFragmentLiteRefNFT), fragmentTokenId)); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragmentTokenId); + // not owned by user 2 + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragmentTokenId); + vm.startPrank(_userAddress); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragmentTokenId, 1); + // not currently assigned + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentNotAssigned.selector, address(_testFragmentLiteRefNFT), fragmentTokenId)); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragmentTokenId); } function testDontAssignSomeoneElsesNFT() public { - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); - uint256 fragmentTokenId = testFragmentLiteRefNFT.mint(user2Address); - assertEq(testFragmentLiteRefNFT.ownerOf(fragmentTokenId), user2Address); - //Register artifactNFT to testPatchLiteRefNFT - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, scopeOwner)); - prot.assignNFT(address(testFragmentLiteRefNFT), fragmentTokenId, address(testPatchLiteRefNFT), patchTokenId); + uint256 fragmentTokenId = _testFragmentLiteRefNFT.mint(_user2Address, ""); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragmentTokenId), _user2Address); + //Register artifactNFT to _testPatchLiteRefNFT + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _scopeOwner)); + _prot.assign(address(_testFragmentLiteRefNFT), fragmentTokenId, address(_testPatchLiteRefNFT), patchTokenId); } function testUnassignNFT() public { vm.expectRevert(); // not unassignable - prot.unassignNFT(address(1), 1); + _prot.unassignSingle(address(1), 1); - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - uint256 fragment1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment2 = testFragmentLiteRefNFT.mint(userAddress); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + uint256 fragment1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - //Register testFragmentLiteRefNFT to testFragmentLiteRefNFT to allow recursion - testFragmentLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + //Register _testFragmentLiteRefNFT to _testFragmentLiteRefNFT to allow recursion + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); // Assign Id1 -> Id - prot.assignNFT(address(testFragmentLiteRefNFT), fragment1, address(testPatchLiteRefNFT), patchTokenId); + _prot.assign(address(_testFragmentLiteRefNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); // Assign Id2 -> Id1 - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testFragmentLiteRefNFT), fragment1); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), fragment1); // Now Id2 -> Id1 -> Id, unassign Id2 from Id1 vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment2); - vm.startPrank(scopeOwner); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment2); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment2); + vm.startPrank(_scopeOwner); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment2); // Now Id1 -> Id, unassign Id1 from Id - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment1); + _prot.unassign(address(_testFragmentLiteRefNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId, 0); // Assign Id1 -> Id - prot.assignNFT(address(testFragmentLiteRefNFT), fragment1, address(testPatchLiteRefNFT), patchTokenId); + _prot.assign(address(_testFragmentLiteRefNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); // Assign Id2 -> Id1 - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testFragmentLiteRefNFT), fragment1); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), fragment1); // Now Id2 -> Id1 -> Id, unassign Id1 from Id - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment1); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment1); // Assign Id1 -> Id - prot.assignNFT(address(testFragmentLiteRefNFT), fragment1, address(testPatchLiteRefNFT), patchTokenId); + _prot.assign(address(_testFragmentLiteRefNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); vm.stopPrank(); - vm.startPrank(testBaseNFT.ownerOf(testBaseNFTTokenId)); - // transfer ownership of underlying asset (testBaseNFT) - testBaseNFT.transferFrom(testBaseNFT.ownerOf(testBaseNFTTokenId), address(7), testBaseNFTTokenId); + vm.startPrank(_testBaseNFT.ownerOf(_testBaseNFTTokenId)); + // transfer ownership of underlying asset (_testBaseNFT) + _testBaseNFT.transferFrom(_testBaseNFT.ownerOf(_testBaseNFTTokenId), address(7), _testBaseNFTTokenId); vm.stopPrank(); - vm.startPrank(scopeOwner); + vm.startPrank(_scopeOwner); - testFragmentLiteRefNFT.setGetLiteRefOverride(true, 0); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentUnregistered.selector, address(testFragmentLiteRefNFT))); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment2); - testFragmentLiteRefNFT.setGetLiteRefOverride(false, 0); + _testFragmentLiteRefNFT.setGetLiteRefOverride(true, 0); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentUnregistered.selector, address(_testFragmentLiteRefNFT))); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment2); + _testFragmentLiteRefNFT.setGetLiteRefOverride(false, 0); + + // Revert b/c this isn't the expected assignment given explicitly + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentNotAssignedToTarget.selector, address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), 15000)); + _prot.unassign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), 15000); // Now Id2 -> Id1 -> Id where Id belongs to 7, unassign Id2 from Id1 and check new ownership - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment2); - assertEq(testFragmentLiteRefNFT.ownerOf(fragment2), address(7)); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment2); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragment2), address(7)); - testFragmentLiteRefNFT.setGetAssignedToOverride(true, address(testPatchLiteRefNFT)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.RefNotFoundInScope.selector, scopeName, address(testPatchLiteRefNFT), address(testFragmentLiteRefNFT), fragment2)); - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment2); - testFragmentLiteRefNFT.setGetAssignedToOverride(false, address(testPatchLiteRefNFT)); + _testFragmentLiteRefNFT.setGetAssignedToOverride(true, address(_testPatchLiteRefNFT)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.RefNotFound.selector, address(_testPatchLiteRefNFT), address(_testFragmentLiteRefNFT), fragment2)); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment2); + _testFragmentLiteRefNFT.setGetAssignedToOverride(false, address(_testPatchLiteRefNFT)); vm.stopPrank(); // try to transfer a patch directly - it should be blocked because it is soulbound - assertEq(address(7), testPatchLiteRefNFT.ownerOf(patchTokenId)); // Report as soulbound - vm.startPrank(userAddress); // Prank from underlying owner address - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.SoulboundTransferNotAllowed.selector, address(testPatchLiteRefNFT), patchTokenId)); - testPatchLiteRefNFT.transferFrom(userAddress, address(7), patchTokenId); + assertEq(address(7), _testPatchLiteRefNFT.ownerOf(patchTokenId)); // Report as soulbound + vm.startPrank(_userAddress); // Prank from underlying owner address + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.TransferNotAllowed.selector, address(_testPatchLiteRefNFT), patchTokenId)); + _testPatchLiteRefNFT.transferFrom(_userAddress, address(7), patchTokenId); vm.stopPrank(); } + function testUnassignMultiNFT() public { + vm.expectRevert(); // not unassignable + _prot.unassignMulti(address(1), 1, address(1), 1); + + vm.expectRevert(); // not unassignable + _prot.unassignMulti(address(1), 1, address(1), 1, 0); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + uint256 fragment1 = _testMultiFragmentNFT.mint(_userAddress, ""); + + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); + + _testPatchLiteRefNFT.registerReferenceAddress(address(_testMultiFragmentNFT)); + _prot.assign(address(_testMultiFragmentNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); + _prot.unassign(address(_testMultiFragmentNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentNotAssignedToTarget.selector, address(_testMultiFragmentNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId)); + _prot.unassign(address(_testMultiFragmentNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); + } + function testBatchAssignNFT() public { - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); address[] memory fragmentAddresses = new address[](8); uint256[] memory fragments = new uint256[](8); for (uint8 i = 0; i < 8; i++) { - fragmentAddresses[i] = address(testFragmentLiteRefNFT); - fragments[i] = testFragmentLiteRefNFT.mint(userAddress); + fragmentAddresses[i] = address(_testFragmentLiteRefNFT); + fragments[i] = _testFragmentLiteRefNFT.mint(_userAddress, ""); } - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentUnregistered.selector, address(testFragmentLiteRefNFT))); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); - uint8 refId = testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + // Fragment must be registered + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentUnregistered.selector, address(_testFragmentLiteRefNFT))); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); + uint8 refId = _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); - - vm.prank(scopeOwner); - prot.setScopeRules(scopeName, false, false, true); - vm.startPrank(scopeOwner); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotWhitelisted.selector, scopeName, address(testFragmentLiteRefNFT))); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); + // User may not assign without userAssign enabled + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); + + vm.prank(_scopeOwner); + _prot.setScopeRules(_scopeName, false, false, true); + vm.startPrank(_scopeOwner); + // Whitelist enabled requires whitelisted (both patch and fragment) + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(_testPatchLiteRefNFT))); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); - prot.addWhitelist(scopeName, address(testFragmentLiteRefNFT)); - - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.BadInputLengths.selector)); - prot.batchAssignNFT(new address[](1), fragments, address(testPatchLiteRefNFT), patchTokenId); + _prot.addWhitelist(_scopeName, address(_testPatchLiteRefNFT)); + // Whitelist enabled requires whitelisted (now just fragment) + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotWhitelisted.selector, _scopeName, address(_testFragmentLiteRefNFT))); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); + + _prot.addWhitelist(_scopeName, address(_testFragmentLiteRefNFT)); + // Inputs do not match length + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.BadInputLengths.selector)); + _prot.assignBatch(new address[](1), fragments, address(_testPatchLiteRefNFT), patchTokenId); vm.stopPrank(); - vm.prank(userAddress); - testPatchLiteRefNFT.setFrozen(patchTokenId, true); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.Frozen.selector, address(testPatchLiteRefNFT), patchTokenId)); - vm.prank(scopeOwner); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); - vm.prank(userAddress); - testPatchLiteRefNFT.setFrozen(patchTokenId, false); - - vm.prank(userAddress); - testFragmentLiteRefNFT.setFrozen(fragments[0], true); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.Frozen.selector, address(testFragmentLiteRefNFT), fragments[0])); - vm.prank(scopeOwner); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); - vm.prank(userAddress); - testFragmentLiteRefNFT.setFrozen(fragments[0], false); - - vm.prank(scopeOwner); - testPatchLiteRefNFT.redactReferenceAddress(refId); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentRedacted.selector, address(testFragmentLiteRefNFT))); - vm.prank(scopeOwner); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); - vm.prank(scopeOwner); - testPatchLiteRefNFT.unredactReferenceAddress(refId); + vm.prank(_userAddress); + _testPatchLiteRefNFT.setFrozen(patchTokenId, true); + // It's frozen (patch) + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.Frozen.selector, address(_testPatchLiteRefNFT), patchTokenId)); + vm.prank(_scopeOwner); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); + vm.prank(_userAddress); + _testPatchLiteRefNFT.setFrozen(patchTokenId, false); + + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setFrozen(fragments[0], true); + // It's frozen (fragment) + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.Frozen.selector, address(_testFragmentLiteRefNFT), fragments[0])); + vm.prank(_scopeOwner); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setFrozen(fragments[0], false); + + vm.prank(_scopeOwner); + _testPatchLiteRefNFT.redactReferenceAddress(refId); + // Fragment was redacted + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentRedacted.selector, address(_testFragmentLiteRefNFT))); + vm.prank(_scopeOwner); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); + vm.prank(_scopeOwner); + _testPatchLiteRefNFT.unredactReferenceAddress(refId); address[] memory selfAddr = new address[](1); uint256[] memory selfFrag = new uint256[](1); - selfAddr[0] = address(testPatchLiteRefNFT); + selfAddr[0] = address(_testPatchLiteRefNFT); selfFrag[0] = patchTokenId; - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.SelfAssignmentNotAllowed.selector, selfAddr[0], selfFrag[0])); - vm.prank(scopeOwner); - prot.batchAssignNFT(selfAddr, selfFrag, address(testPatchLiteRefNFT), patchTokenId); - - vm.prank(userAddress); - testFragmentLiteRefNFT.setLocked(fragments[0], true); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.Locked.selector, address(testFragmentLiteRefNFT), fragments[0])); - vm.prank(scopeOwner); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); - vm.prank(userAddress); - testFragmentLiteRefNFT.setLocked(fragments[0], false); + // Self-assignment + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.SelfAssignmentNotAllowed.selector, selfAddr[0], selfFrag[0])); + vm.prank(_scopeOwner); + _prot.assignBatch(selfAddr, selfFrag, address(_testPatchLiteRefNFT), patchTokenId); + + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setLocked(fragments[0], true); + // Fragment is locked + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.Locked.selector, address(_testFragmentLiteRefNFT), fragments[0])); + vm.prank(_scopeOwner); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setLocked(fragments[0], false); // test assigning fragments for another user address[] memory otherUserAddr = new address[](1); uint256[] memory otherUserFrag = new uint256[](1); - otherUserAddr[0] = address(testFragmentLiteRefNFT); - otherUserFrag[0] = testFragmentLiteRefNFT.mint(user2Address); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, scopeOwner)); - vm.prank(scopeOwner); - prot.batchAssignNFT(otherUserAddr, otherUserFrag, address(testPatchLiteRefNFT), patchTokenId); + otherUserAddr[0] = address(_testFragmentLiteRefNFT); + otherUserFrag[0] = _testFragmentLiteRefNFT.mint(_user2Address, ""); + // Target and fragment not same owner + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _scopeOwner)); + vm.prank(_scopeOwner); + _prot.assignBatch(otherUserAddr, otherUserFrag, address(_testPatchLiteRefNFT), patchTokenId); // finally a positive test case - vm.prank(scopeOwner); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); + vm.prank(_scopeOwner); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId, 0); for (uint8 i = 0; i < 8; i++) { - (address addr, uint256 tokenId) = testFragmentLiteRefNFT.getAssignedTo(fragments[i]); - assertEq(addr, address(testPatchLiteRefNFT)); + (address addr, uint256 tokenId) = _testFragmentLiteRefNFT.getAssignedTo(fragments[i]); + assertEq(addr, address(_testPatchLiteRefNFT)); assertEq(tokenId, patchTokenId); - assertEq(testFragmentLiteRefNFT.ownerOf(fragments[i]), userAddress); - testFragmentLiteRefNFT.setTestLockOverride(true); // setup for next test part + assertEq(_testFragmentLiteRefNFT.ownerOf(fragments[i]), _userAddress); + _testFragmentLiteRefNFT.setTestLockOverride(true); // setup for next test part } - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.FragmentAlreadyAssignedInScope.selector, scopeName, fragmentAddresses[0], fragments[0])); - vm.prank(scopeOwner); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.FragmentAlreadyAssigned.selector, fragmentAddresses[0], fragments[0])); + vm.prank(_scopeOwner); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); } function testBatchUserPatchAssignNFT() public { - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - prot.setScopeRules(scopeName, true, true, false); - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, true, true, false); + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); vm.stopPrank(); - vm.startPrank(userAddress); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); + vm.startPrank(_userAddress); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); address[] memory fragmentAddresses = new address[](8); uint256[] memory fragments = new uint256[](8); uint256[] memory user2Fragments = new uint256[](8); for (uint8 i = 0; i < 8; i++) { - fragmentAddresses[i] = address(testFragmentLiteRefNFT); - fragments[i] = testFragmentLiteRefNFT.mint(userAddress); - user2Fragments[i] = testFragmentLiteRefNFT.mint(user2Address); + fragmentAddresses[i] = address(_testFragmentLiteRefNFT); + fragments[i] = _testFragmentLiteRefNFT.mint(_userAddress, ""); + user2Fragments[i] = _testFragmentLiteRefNFT.mint(_user2Address, ""); } vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - prot.batchAssignNFT(fragmentAddresses, user2Fragments, address(testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _prot.assignBatch(fragmentAddresses, user2Fragments, address(_testPatchLiteRefNFT), patchTokenId); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); - prot.batchAssignNFT(fragmentAddresses, user2Fragments, address(testPatchLiteRefNFT), patchTokenId); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.assignBatch(fragmentAddresses, user2Fragments, address(_testPatchLiteRefNFT), patchTokenId); // test assigning fragments for another user address[] memory otherUserAddr = new address[](1); uint256[] memory otherUserFrag = new uint256[](1); - otherUserAddr[0] = address(testFragmentLiteRefNFT); - otherUserFrag[0] = testFragmentLiteRefNFT.mint(user2Address); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, userAddress)); - vm.prank(userAddress); - prot.batchAssignNFT(otherUserAddr, otherUserFrag, address(testPatchLiteRefNFT), patchTokenId); + otherUserAddr[0] = address(_testFragmentLiteRefNFT); + otherUserFrag[0] = _testFragmentLiteRefNFT.mint(_user2Address, ""); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _userAddress)); + vm.prank(_userAddress); + _prot.assignBatch(otherUserAddr, otherUserFrag, address(_testPatchLiteRefNFT), patchTokenId); // finally a positive test case - vm.startPrank(userAddress); - prot.batchAssignNFT(fragmentAddresses, fragments, address(testPatchLiteRefNFT), patchTokenId); + vm.startPrank(_userAddress); + _prot.assignBatch(fragmentAddresses, fragments, address(_testPatchLiteRefNFT), patchTokenId); for (uint8 i = 0; i < 8; i++) { - (address addr, uint256 tokenId) = testFragmentLiteRefNFT.getAssignedTo(fragments[i]); - assertEq(addr, address(testPatchLiteRefNFT)); + (address addr, uint256 tokenId) = _testFragmentLiteRefNFT.getAssignedTo(fragments[i]); + assertEq(addr, address(_testPatchLiteRefNFT)); assertEq(tokenId, patchTokenId); - assertEq(testFragmentLiteRefNFT.ownerOf(fragments[i]), userAddress); + assertEq(_testFragmentLiteRefNFT.ownerOf(fragments[i]), _userAddress); } vm.stopPrank(); } function testTransferLogs() public { - uint256 fragment1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment2 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment3 = testFragmentLiteRefNFT.mint(userAddress); + uint256 fragment1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment3 = _testFragmentLiteRefNFT.mint(_userAddress, ""); - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); - //Register testFragmentLiteRefNFT to testFragmentLiteRefNFT to allow recursion - testFragmentLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + //Register _testFragmentLiteRefNFT to _testFragmentLiteRefNFT to allow recursion + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); // Assign Id2 -> Id1 - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testFragmentLiteRefNFT), fragment1); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), fragment1); // Assign Id3 -> Id2 - prot.assignNFT(address(testFragmentLiteRefNFT), fragment3, address(testFragmentLiteRefNFT), fragment2); + _prot.assign(address(_testFragmentLiteRefNFT), fragment3, address(_testFragmentLiteRefNFT), fragment2); vm.stopPrank(); - vm.expectEmit(true, true, true, true, address(testFragmentLiteRefNFT)); - emit Transfer(userAddress, user2Address, fragment2); - vm.expectEmit(true, true, true, true, address(testFragmentLiteRefNFT)); - emit Transfer(userAddress, user2Address, fragment3); - vm.expectEmit(true, true, true, true, address(testFragmentLiteRefNFT)); - emit Transfer(userAddress, user2Address, fragment1); - vm.prank(userAddress); - testFragmentLiteRefNFT.transferFrom(userAddress, user2Address, fragment1); - assertEq(user2Address, testFragmentLiteRefNFT.ownerOf(fragment1)); - assertEq(user2Address, testFragmentLiteRefNFT.ownerOf(fragment2)); - assertEq(user2Address, testFragmentLiteRefNFT.ownerOf(fragment3)); + vm.expectEmit(true, true, true, true, address(_testFragmentLiteRefNFT)); + emit Transfer(_userAddress, _user2Address, fragment2); + vm.expectEmit(true, true, true, true, address(_testFragmentLiteRefNFT)); + emit Transfer(_userAddress, _user2Address, fragment3); + vm.expectEmit(true, true, true, true, address(_testFragmentLiteRefNFT)); + emit Transfer(_userAddress, _user2Address, fragment1); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.transferFrom(_userAddress, _user2Address, fragment1); + assertEq(_user2Address, _testFragmentLiteRefNFT.ownerOf(fragment1)); + assertEq(_user2Address, _testFragmentLiteRefNFT.ownerOf(fragment2)); + assertEq(_user2Address, _testFragmentLiteRefNFT.ownerOf(fragment3)); } function testUpdateOwnership() public { - uint256 fragment1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment2 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment3 = testFragmentLiteRefNFT.mint(userAddress); + uint256 fragment1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment3 = _testFragmentLiteRefNFT.mint(_userAddress, ""); - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); - //Register testFragmentLiteRefNFT to testFragmentLiteRefNFT to allow recursion - testFragmentLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + //Register _testFragmentLiteRefNFT to _testFragmentLiteRefNFT to allow recursion + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); // Assign Id2 -> Id1 - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testFragmentLiteRefNFT), fragment1); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), fragment1); // Assign Id3 -> Id2 - prot.assignNFT(address(testFragmentLiteRefNFT), fragment3, address(testFragmentLiteRefNFT), fragment2); + _prot.assign(address(_testFragmentLiteRefNFT), fragment3, address(_testFragmentLiteRefNFT), fragment2); vm.stopPrank(); - vm.prank(userAddress); + vm.prank(_userAddress); // This won't actually transfer fragments 2 and 3 - testFragmentLiteRefNFT.transferFrom(userAddress, user2Address, fragment1); - assertEq(user2Address, testFragmentLiteRefNFT.unassignedOwnerOf(fragment1)); - assertEq(userAddress, testFragmentLiteRefNFT.unassignedOwnerOf(fragment2)); - assertEq(userAddress, testFragmentLiteRefNFT.unassignedOwnerOf(fragment3)); - prot.updateOwnershipTree(address(testFragmentLiteRefNFT), fragment1); - assertEq(user2Address, testFragmentLiteRefNFT.unassignedOwnerOf(fragment1)); - assertEq(user2Address, testFragmentLiteRefNFT.unassignedOwnerOf(fragment2)); - assertEq(user2Address, testFragmentLiteRefNFT.unassignedOwnerOf(fragment3)); + _testFragmentLiteRefNFT.transferFrom(_userAddress, _user2Address, fragment1); + assertEq(_user2Address, _testFragmentLiteRefNFT.unassignedOwnerOf(fragment1)); + assertEq(_userAddress, _testFragmentLiteRefNFT.unassignedOwnerOf(fragment2)); + assertEq(_userAddress, _testFragmentLiteRefNFT.unassignedOwnerOf(fragment3)); + _prot.updateOwnershipTree(address(_testFragmentLiteRefNFT), fragment1); + assertEq(_user2Address, _testFragmentLiteRefNFT.unassignedOwnerOf(fragment1)); + assertEq(_user2Address, _testFragmentLiteRefNFT.unassignedOwnerOf(fragment2)); + assertEq(_user2Address, _testFragmentLiteRefNFT.unassignedOwnerOf(fragment3)); // test with patch - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - vm.prank(scopeOwner); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); - vm.prank(userAddress); - testBaseNFT.transferFrom(userAddress, user2Address, testBaseNFTTokenId); - assertEq(user2Address, testPatchLiteRefNFT.ownerOf(patchTokenId)); - assertEq(userAddress, testPatchLiteRefNFT.unpatchedOwnerOf(patchTokenId)); - prot.updateOwnershipTree(address(testPatchLiteRefNFT), patchTokenId); - assertEq(user2Address, testPatchLiteRefNFT.unpatchedOwnerOf(patchTokenId)); + TestPatchNFT patch = new TestPatchNFT(address(_prot)); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + vm.prank(_scopeOwner); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(patch)); + vm.prank(_userAddress); + _testBaseNFT.transferFrom(_userAddress, _user2Address, _testBaseNFTTokenId); + assertEq(_user2Address, patch.ownerOf(patchTokenId)); + assertEq(_userAddress, patch.ownerOfPatch(patchTokenId)); + _prot.updateOwnershipTree(address(patch), patchTokenId); + assertEq(_user2Address, patch.ownerOfPatch(patchTokenId)); } function testLocks() public { - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - uint256 fragment1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment2 = testFragmentLiteRefNFT.mint(userAddress); - testPatchworkNFT.mint(userAddress, 1); - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); - - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - //Register testFragmentLiteRefNFT to testFragmentLiteRefNFT to allow recursion - testFragmentLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + uint256 fragment1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 n = _testPatchworkNFT.mint(_userAddress, ""); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); + + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + //Register _testFragmentLiteRefNFT to _testFragmentLiteRefNFT to allow recursion + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); vm.stopPrank(); // cannot lock a patch - assertEq(false, testPatchLiteRefNFT.locked(patchTokenId)); + assertEq(false, _testPatchLiteRefNFT.locked(patchTokenId)); vm.expectRevert(); - vm.prank(userAddress); - testPatchLiteRefNFT.setLocked(patchTokenId, true); + vm.prank(_userAddress); + _testPatchLiteRefNFT.setLocked(patchTokenId, true); // can lock an unassigned fragment - assertEq(false, testFragmentLiteRefNFT.locked(fragment1)); - vm.prank(userAddress); - testFragmentLiteRefNFT.setLocked(fragment1, true); - assertEq(true, testFragmentLiteRefNFT.locked(fragment1)); - vm.prank(userAddress); - testFragmentLiteRefNFT.setLocked(fragment1, false); + assertEq(false, _testFragmentLiteRefNFT.locked(fragment1)); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setLocked(fragment1, true); + assertEq(true, _testFragmentLiteRefNFT.locked(fragment1)); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setLocked(fragment1, false); // only owner may lock - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - testFragmentLiteRefNFT.setLocked(fragment1, true); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _testFragmentLiteRefNFT.setLocked(fragment1, true); // only owner may lock - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotAuthorized.selector, user2Address)); - vm.prank(user2Address); - testPatchworkNFT.setLocked(1, true); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _user2Address)); + vm.prank(_user2Address); + _testPatchworkNFT.setLocked(n, true); // an assigned fragment is locked implicitly - assertEq(false, testFragmentLiteRefNFT.locked(fragment1)); - vm.prank(scopeOwner); + assertEq(false, _testFragmentLiteRefNFT.locked(fragment1)); + vm.prank(_scopeOwner); // Assign Id1 -> Id - prot.assignNFT(address(testFragmentLiteRefNFT), fragment1, address(testPatchLiteRefNFT), patchTokenId); - assertEq(true, testFragmentLiteRefNFT.locked(fragment1)); + _prot.assign(address(_testFragmentLiteRefNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); + assertEq(true, _testFragmentLiteRefNFT.locked(fragment1)); // cannot lock an assigned fragment vm.expectRevert(); - vm.prank(userAddress); - testFragmentLiteRefNFT.setLocked(fragment1, true); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setLocked(fragment1, true); // cannot assign a locked fragment - vm.startPrank(userAddress); - testFragmentLiteRefNFT.setLocked(fragment2, true); + vm.startPrank(_userAddress); + _testFragmentLiteRefNFT.setLocked(fragment2, true); vm.expectRevert(); - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testPatchLiteRefNFT), patchTokenId); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testPatchLiteRefNFT), patchTokenId); // cannot transfer a locked fragment vm.expectRevert(); - testFragmentLiteRefNFT.transferFrom(userAddress, address(100), fragment2); - testFragmentLiteRefNFT.setLocked(fragment2, false); - testFragmentLiteRefNFT.transferFrom(userAddress, address(100), fragment2); - assertEq(address(100), testFragmentLiteRefNFT.ownerOf(fragment2)); + _testFragmentLiteRefNFT.transferFrom(_userAddress, address(100), fragment2); + _testFragmentLiteRefNFT.setLocked(fragment2, false); + _testFragmentLiteRefNFT.transferFrom(_userAddress, address(100), fragment2); + assertEq(address(100), _testFragmentLiteRefNFT.ownerOf(fragment2)); vm.stopPrank(); } function testFreezes() public { - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - uint256 fragment1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment2 = testFragmentLiteRefNFT.mint(userAddress); - - vm.startPrank(scopeOwner); - prot.claimScope(scopeName); - uint256 patchTokenId = prot.createPatch(address(testBaseNFT), testBaseNFTTokenId, address(testPatchLiteRefNFT)); - - testPatchLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - //Register testFragmentLiteRefNFT to testFragmentLiteRefNFT to allow recursion - testFragmentLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + uint256 fragment1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); + + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + //Register _testFragmentLiteRefNFT to _testFragmentLiteRefNFT to allow recursion + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); vm.stopPrank(); // Freeze the patch, shouldn't allow assignment of a child - vm.prank(userAddress); - testPatchLiteRefNFT.setFrozen(patchTokenId, true); - assertEq(0, testPatchLiteRefNFT.getFreezeNonce(patchTokenId)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.Frozen.selector, address(testPatchLiteRefNFT), patchTokenId)); - vm.prank(scopeOwner); + vm.prank(_userAddress); + _testPatchLiteRefNFT.setFrozen(patchTokenId, true); + assertEq(0, _testPatchLiteRefNFT.getFreezeNonce(patchTokenId)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.Frozen.selector, address(_testPatchLiteRefNFT), patchTokenId)); + vm.prank(_scopeOwner); // Assign Id1 -> Id - prot.assignNFT(address(testFragmentLiteRefNFT), fragment1, address(testPatchLiteRefNFT), patchTokenId); - vm.prank(userAddress); - testPatchLiteRefNFT.setFrozen(patchTokenId, false); - vm.startPrank(scopeOwner); - assertEq(1, testPatchLiteRefNFT.getFreezeNonce(patchTokenId)); + _prot.assign(address(_testFragmentLiteRefNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); + vm.prank(_userAddress); + _testPatchLiteRefNFT.setFrozen(patchTokenId, false); + vm.startPrank(_scopeOwner); + assertEq(1, _testPatchLiteRefNFT.getFreezeNonce(patchTokenId)); // Assign Id1 -> Id - prot.assignNFT(address(testFragmentLiteRefNFT), fragment1, address(testPatchLiteRefNFT), patchTokenId); + _prot.assign(address(_testFragmentLiteRefNFT), fragment1, address(_testPatchLiteRefNFT), patchTokenId); // Assign Id2 -> Id1 - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testFragmentLiteRefNFT), fragment1); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), fragment1); // Lock the patch, shouldn't allow unassignment of any child vm.stopPrank(); - vm.prank(userAddress); - testPatchLiteRefNFT.setFrozen(patchTokenId, true); + vm.prank(_userAddress); + _testPatchLiteRefNFT.setFrozen(patchTokenId, true); + // It will return that the fragment is frozen even though the patch is the root cause, because all assigned to the patch inherit the freeze - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.Frozen.selector, address(testFragmentLiteRefNFT), fragment2)); - vm.prank(scopeOwner); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.Frozen.selector, address(_testFragmentLiteRefNFT), fragment2)); + vm.prank(_scopeOwner); // Now Id2 -> Id1 -> Id, unassign Id2 from Id1 - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment2); - vm.prank(userAddress); - testPatchLiteRefNFT.setFrozen(patchTokenId, false); - assertEq(2, testPatchLiteRefNFT.getFreezeNonce(patchTokenId)); - vm.startPrank(scopeOwner); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment2); + vm.prank(_userAddress); + _testPatchLiteRefNFT.setFrozen(patchTokenId, false); + assertEq(2, _testPatchLiteRefNFT.getFreezeNonce(patchTokenId)); + vm.startPrank(_scopeOwner); // Now Id2 -> Id1 -> Id, unassign Id2 from Id1 - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment2); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment2, 0); vm.stopPrank(); - vm.startPrank(scopeOwner); + vm.startPrank(_scopeOwner); // Unassign Id1 from patch - prot.unassignNFT(address(testFragmentLiteRefNFT), fragment1); + _prot.unassignSingle(address(_testFragmentLiteRefNFT), fragment1); vm.stopPrank(); // Lock the fragment, shouldn't allow assignment to anything - vm.prank(userAddress); - testFragmentLiteRefNFT.setFrozen(fragment1, true); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.Frozen.selector, address(testFragmentLiteRefNFT), fragment1)); - vm.prank(scopeOwner); - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testFragmentLiteRefNFT), fragment1); - vm.prank(userAddress); - testFragmentLiteRefNFT.setFrozen(fragment2, true); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.Frozen.selector, address(testFragmentLiteRefNFT), fragment1)); - vm.prank(scopeOwner); - prot.assignNFT(address(testFragmentLiteRefNFT), fragment2, address(testFragmentLiteRefNFT), fragment1); - vm.startPrank(userAddress); - testFragmentLiteRefNFT.setFrozen(fragment2, false); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setFrozen(fragment1, true); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.Frozen.selector, address(_testFragmentLiteRefNFT), fragment1)); + vm.prank(_scopeOwner); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), fragment1); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.setFrozen(fragment2, true); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.Frozen.selector, address(_testFragmentLiteRefNFT), fragment1)); + vm.prank(_scopeOwner); + _prot.assign(address(_testFragmentLiteRefNFT), fragment2, address(_testFragmentLiteRefNFT), fragment1); + vm.startPrank(_userAddress); + _testFragmentLiteRefNFT.setFrozen(fragment2, false); // test transfers with lock nonce mismatch // first unassign the fragment b/c you can't unassign a locked one - uint256 nonce = testFragmentLiteRefNFT.getFreezeNonce(fragment2); - testFragmentLiteRefNFT.setFrozen(fragment2, true); - testFragmentLiteRefNFT.setFrozen(fragment2, false); // nonce +1 - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotFrozen.selector, address(testFragmentLiteRefNFT), fragment2)); - testFragmentLiteRefNFT.transferFromWithFreezeNonce(userAddress, user2Address, fragment2, nonce+1); - assertEq(false, testFragmentLiteRefNFT.frozen(fragment2)); - testFragmentLiteRefNFT.setFrozen(fragment2, true); - assertEq(true, testFragmentLiteRefNFT.frozen(fragment2)); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.IncorrectNonce.selector, address(testFragmentLiteRefNFT), fragment2, nonce)); - testFragmentLiteRefNFT.transferFromWithFreezeNonce(userAddress, user2Address, fragment2, nonce); + uint256 nonce = _testFragmentLiteRefNFT.getFreezeNonce(fragment2); + _testFragmentLiteRefNFT.setFrozen(fragment2, true); + _testFragmentLiteRefNFT.setFrozen(fragment2, false); // nonce +1 + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotFrozen.selector, address(_testFragmentLiteRefNFT), fragment2)); + _testFragmentLiteRefNFT.transferFromWithFreezeNonce(_userAddress, _user2Address, fragment2, nonce+1); + assertEq(false, _testFragmentLiteRefNFT.frozen(fragment2)); + _testFragmentLiteRefNFT.setFrozen(fragment2, true); + assertEq(true, _testFragmentLiteRefNFT.frozen(fragment2)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.IncorrectNonce.selector, address(_testFragmentLiteRefNFT), fragment2, nonce)); + _testFragmentLiteRefNFT.transferFromWithFreezeNonce(_userAddress, _user2Address, fragment2, nonce); // now success - testFragmentLiteRefNFT.transferFromWithFreezeNonce(userAddress, user2Address, fragment2, nonce+1); - assertEq(user2Address, testFragmentLiteRefNFT.ownerOf(fragment2)); + _testFragmentLiteRefNFT.transferFromWithFreezeNonce(_userAddress, _user2Address, fragment2, nonce+1); + assertEq(_user2Address, _testFragmentLiteRefNFT.ownerOf(fragment2)); } function testSpoofedTransfer1() public { - vm.startPrank(scopeOwner); + vm.startPrank(_scopeOwner); // create a patchworkliteref but manually put in an entry that isn't assigned to it (spoof ownership) - uint256 fragment1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 fragment2 = testFragmentLiteRefNFT.mint(userAddress); - testFragmentLiteRefNFT.registerReferenceAddress(address(testFragmentLiteRefNFT)); - (uint64 ref, ) = testFragmentLiteRefNFT.getLiteReference(address(testFragmentLiteRefNFT), fragment2); - testFragmentLiteRefNFT.addReference(fragment1, ref); + uint256 fragment1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 fragment2 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + (uint64 ref, ) = _testFragmentLiteRefNFT.getLiteReference(address(_testFragmentLiteRefNFT), fragment2); + _testFragmentLiteRefNFT.addReference(fragment1, ref); // Should revert with data integrity error vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.DataIntegrityError.selector, address(testFragmentLiteRefNFT), fragment1, address(0), 0)); - vm.prank(userAddress); - testFragmentLiteRefNFT.transferFrom(userAddress, user2Address, fragment1); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.DataIntegrityError.selector, address(_testFragmentLiteRefNFT), fragment1, address(0), 0)); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.transferFrom(_userAddress, _user2Address, fragment1); } function testSpoofedTransfer2() public { - vm.startPrank(scopeOwner); + vm.startPrank(_scopeOwner); // create a patchworkliteref but manually put in an entry that isn't assigned to it (spoof ownership) - uint256 fragment1 = testFragmentLiteRefNFT.mint(userAddress); - uint256 testBaseNFTTokenId = testBaseNFT.mint(userAddress); - testFragmentLiteRefNFT.registerReferenceAddress(address(testBaseNFT)); - (uint64 ref, ) = testFragmentLiteRefNFT.getLiteReference(address(testBaseNFT), testBaseNFTTokenId); - testFragmentLiteRefNFT.addReference(fragment1, ref); + uint256 fragment1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + _testFragmentLiteRefNFT.registerReferenceAddress(address(_testBaseNFT)); + (uint64 ref, ) = _testFragmentLiteRefNFT.getLiteReference(address(_testBaseNFT), _testBaseNFTTokenId); + _testFragmentLiteRefNFT.addReference(fragment1, ref); // Should revert with data integrity error vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(PatchworkProtocol.NotPatchworkAssignable.selector, address(testBaseNFT))); - vm.prank(userAddress); - testFragmentLiteRefNFT.transferFrom(userAddress, user2Address, fragment1); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotPatchworkAssignable.selector, address(_testBaseNFT))); + vm.prank(_userAddress); + _testFragmentLiteRefNFT.transferFrom(_userAddress, _user2Address, fragment1); } + + function testLiteRefCollision() public { + TestFragmentLiteRefNFT testFrag2 = new TestFragmentLiteRefNFT(address(_prot)); + vm.startPrank(_scopeOwner); + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + _testPatchLiteRefNFT.registerReferenceAddress(address(_testFragmentLiteRefNFT)); + _testFragmentLiteRefNFT.registerReferenceAddress(address(testFrag2)); + uint256 frag1 = _testFragmentLiteRefNFT.mint(_userAddress, ""); + uint256 frag2 = testFrag2.mint(_userAddress, ""); + uint256 _testBaseNFTTokenId = _testBaseNFT.mint(_userAddress); + uint256 patchTokenId = _prot.patch(_userAddress, address(_testBaseNFT), _testBaseNFTTokenId, address(_testPatchLiteRefNFT)); + _prot.assign(address(_testFragmentLiteRefNFT), frag1, address(_testPatchLiteRefNFT), patchTokenId); + // The second assign succeeding combined with the assertion that they are equal ref values means there is no collision in the scope. + _prot.assign(address(testFrag2), frag2, address(_testFragmentLiteRefNFT), frag1); + // LiteRef IDs should match because it is idx1 tokenID 0 for both (0x1. 0x0) + (uint64 lr1,) = _testPatchLiteRefNFT.getLiteReference(address(_testFragmentLiteRefNFT), frag1); + (uint64 lr2,) = _testFragmentLiteRefNFT.getLiteReference(address(testFrag2), frag2); + assertEq(lr1, lr2); + } + + function testBurn() public { + // *burned* can only be called from the patch burning it + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _prot.patchBurned(address(1), 1, address(2)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _prot.patchBurned1155(address(1), 1, address(3), address(2)); + vm.expectRevert(abi.encodeWithSelector(IPatchworkProtocol.NotAuthorized.selector, _defaultUser)); + _prot.patchBurnedAccount(address(1), address(2)); + } } \ No newline at end of file diff --git a/test/PatchworkUtils.t.sol b/test/PatchworkUtils.t.sol index bc44981..01bddd3 100644 --- a/test/PatchworkUtils.t.sol +++ b/test/PatchworkUtils.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -7,7 +7,7 @@ import "forge-std/console.sol"; import "../src/PatchworkUtils.sol"; contract PatchworkUtilsTest is Test { - function testStringConversions() public { + function testStringConversions() public { bytes8 b8; // 9/8 bytes memory ns = bytes("abcdefghi"); @@ -96,4 +96,9 @@ contract PatchworkUtilsTest is Test { } assertEq("", PatchworkUtils.toString32(uint256(b32))); } + + function testByteConversions() public { + assertEq(abi.encodePacked(bytes1(uint8(0)), bytes1(uint8(0))), PatchworkUtils.convertUint16ToBytes(0)); + assertEq(abi.encodePacked(bytes1(uint8(1)), bytes1(uint8(2))), PatchworkUtils.convertUint16ToBytes(258)); + } } \ No newline at end of file diff --git a/test/TestDynamicArrayLiteRefNFT.t.sol b/test/TestDynamicArrayLiteRefNFT.t.sol new file mode 100644 index 0000000..f817226 --- /dev/null +++ b/test/TestDynamicArrayLiteRefNFT.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/PatchworkProtocol.sol"; +import "../src/PatchworkProtocolAssigner.sol"; +import "./nfts/TestDynamicArrayLiteRefNFT.sol"; + +contract PatchworkAccountPatchTest is Test { + + PatchworkProtocol _prot; + + string _scopeName; + address _defaultUser; + address _scopeOwner; + address _patchworkOwner; + address _userAddress; + address _user2Address; + + function setUp() public { + _defaultUser = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + _patchworkOwner = 0xF09CFF10D85E70D5AA94c85ebBEbD288756EFEd5; + _userAddress = 0x10E4017cEd8648A9D5dAc21C82589C03C4835CCc; + _user2Address = address(550001); + _scopeOwner = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + vm.prank(_patchworkOwner); + _prot = new PatchworkProtocol(_patchworkOwner, address(new PatchworkProtocolAssigner(_patchworkOwner))); + + vm.startPrank(_scopeOwner); + _scopeName = "testscope"; + _prot.claimScope(_scopeName); + _prot.setScopeRules(_scopeName, false, false, false); + + vm.stopPrank(); + } + + function testDynamics() public { + TestDynamicArrayLiteRefNFT nft = new TestDynamicArrayLiteRefNFT(address(_prot)); + nft.registerReferenceAddress(address(0x55)); + uint256 m = nft.mint(_userAddress, ""); + assertEq(0, nft.getDynamicReferenceCount(m)); + for (uint256 i = 0; i < 9; i++) { + (uint64 ref,) = nft.getLiteReference(address(0x55), i); + nft.addReference(m, ref); + assertEq(i+1, nft.getDynamicReferenceCount(m)); + } + + (, uint256[] memory tokenIds) = nft.loadDynamicReferencePage(m, 0, 3); + assertEq(tokenIds[0], 0); + assertEq(tokenIds[1], 1); + assertEq(tokenIds[2], 2); + (, tokenIds) = nft.loadDynamicReferencePage(m, 3, 3); + assertEq(tokenIds[0], 3); + assertEq(tokenIds[1], 4); + assertEq(tokenIds[2], 5); + (, tokenIds) = nft.loadDynamicReferencePage(m, 6, 4); + assertEq(tokenIds[0], 6); + assertEq(tokenIds[1], 7); + assertEq(tokenIds[2], 8); + (, tokenIds) = nft.loadDynamicReferencePage(m, 9, 4); + assertEq(tokenIds.length, 0); + (, tokenIds) = nft.loadDynamicReferencePage(m, 10, 4); + assertEq(tokenIds.length, 0); + for (uint256 i = 0; i < 9; i++) { + (uint64 ref,) = nft.getLiteReference(address(0x55), i); + nft.removeReference(m, ref); + assertEq(8-i, nft.getDynamicReferenceCount(m)); + } + } + + function testBatchAdd() public { + TestDynamicArrayLiteRefNFT nft = new TestDynamicArrayLiteRefNFT(address(_prot)); + nft.registerReferenceAddress(address(0x55)); + uint256 m = nft.mint(_userAddress, ""); + assertEq(0, nft.getDynamicReferenceCount(m)); + uint64[] memory refs = new uint64[](11); + for (uint256 i = 0; i < 11; i++) { + (uint64 ref,) = nft.getLiteReference(address(0x55), i); + refs[i] = ref; + } + nft.addReferenceBatch(m, refs); + assertEq(11, nft.getDynamicReferenceCount(m)); + + (, uint256[] memory tokenIds) = nft.loadDynamicReferencePage(m, 0, 3); + assertEq(tokenIds[0], 0); + assertEq(tokenIds[1], 1); + assertEq(tokenIds[2], 2); + (, tokenIds) = nft.loadDynamicReferencePage(m, 3, 3); + assertEq(tokenIds[0], 3); + assertEq(tokenIds[1], 4); + assertEq(tokenIds[2], 5); + (, tokenIds) = nft.loadDynamicReferencePage(m, 6, 5); + assertEq(tokenIds[0], 6); + assertEq(tokenIds[1], 7); + assertEq(tokenIds[2], 8); + assertEq(tokenIds[3], 9); + assertEq(tokenIds[4], 10); + (, tokenIds) = nft.loadDynamicReferencePage(m, 11, 4); + assertEq(tokenIds.length, 0); + (, tokenIds) = nft.loadDynamicReferencePage(m, 13, 4); + assertEq(tokenIds.length, 0); + for (uint256 i = 0; i < 11; i++) { + (uint64 ref,) = nft.getLiteReference(address(0x55), i); + nft.removeReference(m, ref); + assertEq(10-i, nft.getDynamicReferenceCount(m)); + } + } + + function testUnusedFuncs() public { + TestDynamicArrayLiteRefNFT nft = new TestDynamicArrayLiteRefNFT(address(_prot)); + nft.loadAllStaticReferences(0); + } + + +} \ No newline at end of file diff --git a/test/TestPatchLiteRefNFT.t.sol b/test/TestPatchLiteRefNFT.t.sol index 33e13e5..0d0c207 100644 --- a/test/TestPatchLiteRefNFT.t.sol +++ b/test/TestPatchLiteRefNFT.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/sampleNFTs/TestPatchLiteRefNFT.sol"; +import "./nfts/TestPatchLiteRefNFT.sol"; contract TestPatchLiteRefNFTTest is Test { TestPatchLiteRefNFT testNFT; @@ -127,4 +127,9 @@ contract TestPatchLiteRefNFTTest is Test { assertEq(testNFT.loadXP(1), 65535); assertEq(testNFT.loadLevel(1), 255); } + + function testUnusedFuncs() public view { + testNFT.loadDynamicReferencePage(0, 0, 0); + testNFT.getDynamicReferenceCount(0); + } } diff --git a/test/nfts/Test1155PatchNFT.sol b/test/nfts/Test1155PatchNFT.sol new file mode 100644 index 0000000..e7b30fb --- /dev/null +++ b/test/nfts/Test1155PatchNFT.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "../../src/Patchwork1155Patch.sol"; + +contract Test1155PatchNFT is Patchwork1155Patch { + + uint256 _nextTokenId = 0; + + struct Test1155PatchNFTMetadata { + uint256 thing; + } + + constructor(address manager_) Patchwork721("testscope", "Test1155PatchNFT", "TPLR", manager_, msg.sender) { + } + + function schemaURI() pure external returns (string memory) { + return "https://mything/my-nft-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external returns (string memory) { + return string(abi.encodePacked("https://mything/nft-", _tokenId)); + } + + function schema() pure external returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](1); + entries[0] = MetadataSchemaEntry(1, 0, FieldType.UINT256, 0, FieldVisibility.PUBLIC, 2, 0, "thing"); + return MetadataSchema(1, entries); + } + + function mintPatch(address to, PatchTarget memory target) external payable mustBeManager returns (uint256 tokenId){ + if (msg.value > 0) { + revert(); + } + // Just for testing + tokenId = _nextTokenId; + _nextTokenId++; + _storePatch(tokenId, target); + _safeMint(to, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + return tokenId; + } + + function burn(uint256 tokenId) external { + _burnPatch(tokenId); + } + +} + +contract TestReversible1155PatchNFT is PatchworkReversible1155Patch { + + uint256 _nextTokenId = 0; + + struct TestReversible1155PatchNFTMetadata { + uint256 thing; + } + + constructor(address manager_) Patchwork721("testscope", "Test1155PatchNFT", "TPLR", manager_, msg.sender) { + } + + function schemaURI() pure external returns (string memory) { + return "https://mything/my-nft-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external returns (string memory) { + return string(abi.encodePacked("https://mything/nft-", _tokenId)); + } + + function schema() pure external returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](1); + entries[0] = MetadataSchemaEntry(1, 0, FieldType.UINT256, 0, FieldVisibility.PUBLIC, 2, 0, "thing"); + return MetadataSchema(1, entries); + } + + function mintPatch(address to, PatchTarget memory target) external payable mustBeManager returns (uint256 tokenId){ + // Just for testing + if (msg.value > 0) { + revert(); + } + tokenId = _nextTokenId; + _nextTokenId++; + _storePatch(tokenId, target); + _safeMint(to, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + return tokenId; + } + + function burn(uint256 tokenId) external { + _burnPatch(tokenId); + } + +} \ No newline at end of file diff --git a/test/nfts/TestAccountPatchNFT.sol b/test/nfts/TestAccountPatchNFT.sol new file mode 100644 index 0000000..9488560 --- /dev/null +++ b/test/nfts/TestAccountPatchNFT.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "../../src/PatchworkAccountPatch.sol"; + +contract TestAccountPatchNFT is PatchworkReversibleAccountPatch { + + uint256 _nextTokenId = 0; + bool _sameOwnerModel; + + struct TestPatchworkNFTMetadata { + uint256 thing; + } + + constructor(address manager_, bool sameOwnerModel_) Patchwork721("testscope", "TestAccountPatchNFT", "TPLR", manager_, msg.sender) { + _sameOwnerModel = sameOwnerModel_; + } + + function schemaURI() pure external returns (string memory) { + return "https://mything/my-nft-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external returns (string memory) { + return string(abi.encodePacked("https://mything/nft-", _tokenId)); + } + + function schema() pure external returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](1); + entries[0] = MetadataSchemaEntry(1, 0, FieldType.UINT256, 1, FieldVisibility.PUBLIC, 2, 0, "thing"); + return MetadataSchema(1, entries); + } + + function mintPatch(address to, address original) public payable mustBeManager returns (uint256) { + if (msg.value > 0) { + revert(); + } + if (_sameOwnerModel) { + if (to != original) { + revert IPatchworkProtocol.MintNotAllowed(to); + } + } + uint256 tokenId = _nextTokenId; + _nextTokenId++; + _storePatch(tokenId, original); + _mint(to, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + return tokenId; + } + + function burn(uint256 tokenId) public { + // test only + _burnPatch(tokenId); + } + + /** + @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { + _checkTransfer(from, to, tokenId); + super.transferFrom(from, to, tokenId); + } + + /** + @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { + _checkTransfer(from, to, tokenId); + super.safeTransferFrom(from, to, tokenId, data); + } + + function _checkTransfer(address from, address to, uint256 tokenId) internal view { + if (_sameOwnerModel) { + // allow burn only + if (from == address(0)) { + // mint allowed + } else if (to != address(0)) { + revert IPatchworkProtocol.TransferNotAllowed(address(this), tokenId); + } + } + } +} \ No newline at end of file diff --git a/test/nfts/TestBase1155.sol b/test/nfts/TestBase1155.sol new file mode 100644 index 0000000..2846b22 --- /dev/null +++ b/test/nfts/TestBase1155.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + + +contract TestBase1155 is ERC1155 { + + constructor() ERC1155("http://myurl/") { + } + + function mint(address to, uint256 tokenId, uint256 amount) public returns (uint256) { + _mint(to, tokenId, amount, ""); + return tokenId; + } +} \ No newline at end of file diff --git a/src/sampleNFTs/TestBaseNFT.sol b/test/nfts/TestBaseNFT.sol similarity index 94% rename from src/sampleNFTs/TestBaseNFT.sol rename to test/nfts/TestBaseNFT.sol index 5db947d..cfbeb30 100644 --- a/src/sampleNFTs/TestBaseNFT.sol +++ b/test/nfts/TestBaseNFT.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/test/nfts/TestDynamicArrayLiteRefNFT.sol b/test/nfts/TestDynamicArrayLiteRefNFT.sol new file mode 100644 index 0000000..10e00e2 --- /dev/null +++ b/test/nfts/TestDynamicArrayLiteRefNFT.sol @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/* + Prototype - Generated Patchwork Meta contract for Totem NFT. + + Is attached to any normal NFT and is scoped for a specific application. + + Has metadata as defined in totem-metadata.json +*/ + +import "../../src/Patchwork721.sol"; +import "../../src/PatchworkLiteRef.sol"; +import "../../src/interfaces/IPatchworkMintable.sol"; +import "forge-std/console.sol"; + +struct TestDynamicArrayLiteRefNFTMetadata { + uint16 xp; + uint8 level; + uint16 xpLost; + uint16 stakedMade; + uint16 stakedCorrect; + uint8 evolution; + string nickname; +} + +struct DynamicLiteRefs { + uint256[] slots; // 4 per + mapping(uint64 => uint256) idx; +} + +contract TestDynamicArrayLiteRefNFT is Patchwork721, PatchworkLiteRef, IPatchworkMintable { + + uint256 _nextTokenId; + + mapping(uint256 => DynamicLiteRefs) internal _dynamicLiterefStorage; // tokenId => indexed slots + + constructor(address manager_) Patchwork721("testscope", "TestPatchLiteRef", "TPLR", manager_, msg.sender) PatchworkLiteRef() { + } + + // ERC-165 + function supportsInterface(bytes4 interfaceID) public view virtual override(Patchwork721, PatchworkLiteRef) returns (bool) { + return Patchwork721.supportsInterface(interfaceID) || + PatchworkLiteRef.supportsInterface(interfaceID); + } + + function schemaURI() pure external override returns (string memory) { + return "https://mything/my-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external override returns (string memory) {} + + function mint(address to, bytes calldata /* data */) public payable returns (uint256 tokenId) { + if (msg.value > 0) { + revert(); + } + tokenId = _nextTokenId; + _nextTokenId++; + _safeMint(to, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + _dynamicLiterefStorage[tokenId].slots = new uint256[](0); + } + + function mintBatch(address to, bytes calldata data, uint256 quantity) public payable returns (uint256[] memory tokenIds) { + if (msg.value > 0) { + revert(); + } + tokenIds = new uint256[](quantity); + for (uint256 i = 0; i < quantity; i++) { + tokenIds[i] = mint(to, data); + } + } + + /* + Hard coded prototype schema is: + slot 0 offset 0 = artifactIDs (spans 2) - also we need special built-in handling for < 256 bit IDs + slot 2 offset 0 = xp + slot 2 offset 16 = level + slot 2 offset 24 = xpLost + slot 2 offset 40 = stakedMade + slot 2 offset 56 = stakedCorrect + slot 2 offset 72 = evolution + slot 2 offset 80 = nickname + */ + function schema() pure external override returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](8); + entries[0] = MetadataSchemaEntry(0, 0, FieldType.LITEREF, 0, FieldVisibility.PUBLIC, 0, 0, "artifactIDs"); // Dynamic + entries[1] = MetadataSchemaEntry(1, 1, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 0, "xp"); + entries[2] = MetadataSchemaEntry(2, 2, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 0, 16, "level"); + entries[3] = MetadataSchemaEntry(3, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 24, "xpLost"); + entries[4] = MetadataSchemaEntry(4, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 40, "stakedMade"); + entries[5] = MetadataSchemaEntry(5, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 56, "stakedCorrect"); + entries[6] = MetadataSchemaEntry(6, 0, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 0, 72, "evolution"); + entries[7] = MetadataSchemaEntry(7, 0, FieldType.CHAR16, 1, FieldVisibility.PUBLIC, 0, 80, "nickname"); + return MetadataSchema(1, entries); + } + + function packMetadata(TestDynamicArrayLiteRefNFTMetadata memory data) public pure returns (uint256[] memory slots) { + bytes32 nickname; + bytes memory ns = bytes(data.nickname); + + assembly { + nickname := mload(add(ns, 32)) + } + slots = new uint256[](1); + slots[0] = uint256(data.xp) | uint256(data.level) << 16 | uint256(data.xpLost) << 24 | uint256(data.stakedMade) << 40 | uint256(data.stakedCorrect) << 56 | uint256(data.evolution) << 72 | uint256(nickname) >> 128 << 80; + return slots; + } + + function storeMetadata(uint256 _tokenId, TestDynamicArrayLiteRefNFTMetadata memory data) public { + require(_checkTokenWriteAuth(_tokenId), "not authorized"); + _metadataStorage[_tokenId] = packMetadata(data); + } + + function unpackMetadata(uint256[] memory slots) public pure returns (TestDynamicArrayLiteRefNFTMetadata memory data) { + data.xp = uint16(slots[0]); + data.level = uint8(slots[0] >> 16); + data.xpLost = uint16(slots[0] >> 24); + data.stakedMade = uint16(slots[0] >> 40); + data.stakedCorrect = uint16(slots[0] >> 56); + data.evolution = uint8(slots[0] >> 72); + data.nickname = string(abi.encodePacked(bytes16(uint128(slots[0] >> 80)))); + return data; + } + + function loadMetadata(uint256 _tokenId) public view returns (TestDynamicArrayLiteRefNFTMetadata memory data) { + return unpackMetadata(_metadataStorage[_tokenId]); + } + + // Store Only XP + function storeXP(uint256 _tokenId, uint16 xp) public { + require(_checkTokenWriteAuth(_tokenId) || _permissionsAllow[msg.sender] & 0x1 == 1, "not authorized"); + // Slot 2 offset 0: 16 bit value + uint256 cleared = uint256(_metadataStorage[_tokenId][0]) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000; + _metadataStorage[_tokenId][0] = cleared | uint256(xp); + } + + // Load Only XP + function loadXP(uint256 _tokenId) public view returns (uint16) { + return uint16(uint256(_metadataStorage[_tokenId][0])); + } + + // Store Only level + function storeLevel(uint256 _tokenId, uint8 level) public { + require(_checkTokenWriteAuth(_tokenId) || _permissionsAllow[msg.sender] & 0x2 == 2, "not authorized"); + // Slot 2 offset 16: 16 bit value + uint256 cleared = uint256(_metadataStorage[_tokenId][0]) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFF; + _metadataStorage[_tokenId][0] = cleared | (uint256(level) << 16); + } + + // Load Only level + function loadLevel(uint256 _tokenId) public view returns (uint16) { + return uint16(uint256(_metadataStorage[_tokenId][2]) >> 16); + } + + function addReference(uint256 ourTokenId, uint64 liteRef) public override { + require(_checkTokenWriteAuth(ourTokenId), "not authorized"); + // to append: find last slot, if it's not full, add, otherwise start a new slot. + DynamicLiteRefs storage store = _dynamicLiterefStorage[ourTokenId]; + uint256 slotsLen = store.slots.length; + if (slotsLen == 0) { + store.slots.push(uint256(liteRef)); + store.idx[liteRef] = 0; + } else { + uint256 slot = store.slots[slotsLen-1]; + if (slot >= (1 << 192)) { + // new slot (pos 1) + store.slots.push(uint256(liteRef)); + store.idx[liteRef] = slotsLen; + } else { + store.idx[liteRef] = slotsLen-1; + // Reverse search for the next empty subslot + if (slot >= (1 << 128)) { + // pos 4 + store.slots[slotsLen-1] = slot | uint256(liteRef) << 192; + } else if (slot >= (1 << 64)) { + // pos 3 + store.slots[slotsLen-1] = slot | uint256(liteRef) << 128; + } else { + // pos 2 + store.slots[slotsLen-1] = slot | uint256(liteRef) << 64; + } + } + } + } + + function addReferenceBatch(uint256 ourTokenId, uint64[] calldata _liteRefs) public override { + require(_checkTokenWriteAuth(ourTokenId), "not authorized"); + // do in batches of 4 with 1 remainder pass + DynamicLiteRefs storage store = _dynamicLiterefStorage[ourTokenId]; + uint256 slotsLen = store.slots.length; + if (slotsLen > 0) { + revert("already loaded"); + } + uint256 fullBatchCount = _liteRefs.length / 4; + uint256 remainder = _liteRefs.length % 4; + for (uint256 batch = 0; batch < fullBatchCount; batch++) { + uint256 refIdx = batch * 4; + uint256 slot = uint256(_liteRefs[refIdx]) | (uint256(_liteRefs[refIdx+1]) << 64) | (uint256(_liteRefs[refIdx+2]) << 128) | (uint256(_liteRefs[refIdx+3]) << 192); + store.slots.push(slot); + store.idx[_liteRefs[refIdx]] = batch; + store.idx[_liteRefs[refIdx + 1]] = batch; + store.idx[_liteRefs[refIdx + 2]] = batch; + store.idx[_liteRefs[refIdx + 3]] = batch; + } + uint256 rSlot; + for (uint256 i = 0; i < remainder; i++) { + uint256 idx = (fullBatchCount * 4) + i; + rSlot = rSlot | (uint256(_liteRefs[idx]) << (i * 64)); + store.idx[_liteRefs[idx]] = fullBatchCount; + } + store.slots.push(rSlot); + } + + function removeReference(uint256 ourTokenId, uint64 liteRef) public override { + require(_checkTokenWriteAuth(ourTokenId), "not authorized"); + DynamicLiteRefs storage store = _dynamicLiterefStorage[ourTokenId]; + uint256 slotsLen = store.slots.length; + if (slotsLen == 0) { + revert("not found"); + } + + console.log("removing"); + console.logBytes8(bytes8(liteRef)); + for (uint256 i = 0; i < store.slots.length; i++) { + console.logBytes32(bytes32(store.slots[i])); + } + uint256 count = getDynamicReferenceCount(ourTokenId); + if (count == 1) { + if (store.slots[0] == liteRef) { + store.slots.pop(); + delete store.idx[liteRef]; + } else { + revert("not found"); + } + } else { + // remember and remove the last ref + uint256 lastIdx = slotsLen-1; + uint256 slot = store.slots[lastIdx]; + uint64 lastRef; + if (slot >= (1 << 192)) { + // pos 4 + lastRef = uint64(slot >> 192); + store.slots[lastIdx] = slot & 0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + } else if (slot >= (1 << 128)) { + // pos 3 + lastRef = uint64(slot >> 128); + store.slots[lastIdx] = slot & 0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + } else if (slot >= (1 << 64)) { + // pos 2 + lastRef = uint64(slot >> 64); + store.slots[lastIdx] = slot & 0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF; + } else { + // pos 1 + lastRef = uint64(slot); + store.slots.pop(); + } + if (lastRef == liteRef) { + // it was the last ref. No need to replace anything. It's already cleared so just clear the index + delete store.idx[liteRef]; + } else { + // Find the ref and replace it with lastRef then update indexes + uint256 refSlotIdx = store.idx[liteRef]; + slot = store.slots[refSlotIdx]; + if (uint64(slot >> 192) == liteRef) { + slot = slot & 0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + slot = slot | (uint256(lastRef) << 192); + } else if (uint64(slot >> 128) == liteRef) { + slot = slot & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + slot = slot | (uint256(lastRef) << 128); + } else if (uint64(slot >> 64) == liteRef) { + slot = slot & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF; + slot = slot | (uint256(lastRef) << 64); + } else if (uint64(slot) == liteRef) { + slot = slot & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000; + slot = slot | uint256(lastRef); + } else { + revert("storage integrity error"); + } + store.slots[refSlotIdx] = slot; + store.idx[lastRef] = refSlotIdx; + delete store.idx[liteRef]; + } + } + } + + function addReference(uint256 tokenId, uint64 liteRef, uint256 targetMetadataId) public override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + addReference(tokenId, liteRef); + } + + + function removeReference(uint256 tokenId, uint64 liteRef, uint256 targetMetadataId) public override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + removeReference(tokenId, liteRef); + } + + function addReferenceBatch(uint256 tokenId, uint64[] calldata liteRefs, uint256 targetMetadataId) public override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + addReferenceBatch(tokenId, liteRefs); + } + + function loadReferenceAddressAndTokenId(uint256 ourTokenId, uint256 idx) public view returns (address addr, uint256 tokenId) { + uint256[] storage slots = _dynamicLiterefStorage[ourTokenId].slots; + uint slotNumber = idx / 4; // integer division will get the correct slot number + uint shift = (idx % 4) * 64; // the remainder will give the correct shift + uint64 ref = uint64(slots[slotNumber] >> shift); + (addr, tokenId) = getReferenceAddressAndTokenId(ref); + } + + function getDynamicReferenceCount(uint256 tokenId) public view override returns (uint256 count) { + DynamicLiteRefs storage store = _dynamicLiterefStorage[tokenId]; + uint256 slotsLen = store.slots.length; + if (slotsLen == 0) { + return 0; + } else { + uint256 slot = store.slots[slotsLen-1]; + // You could get rid of this conditional stuff if you had a log function + if (slot >= (1 << 192)) { + return slotsLen * 4; + } else { + // Reverse search for the next empty subslot + if (slot >= (1 << 128)) { + // pos 4 + return (slotsLen-1) * 4 + 3; + } else if (slot >= (1 << 64)) { + // pos 3 + return (slotsLen-1) * 4 + 2; + } else { + // pos 2 + return (slotsLen-1) * 4 + 1; + } + } + } + } + + function loadDynamicReferencePage(uint256 tokenId, uint256 offset, uint256 count) public view override returns (address[] memory addresses, uint256[] memory tokenIds) { + uint256 refCount = getDynamicReferenceCount(tokenId); + if (offset >= refCount) { + return (new address[](0), new uint256[](0)); + } + uint256 realCount = refCount - offset; + if (realCount > count) { + realCount = count; + } + addresses = new address[](realCount); + tokenIds = new uint256[](realCount); + uint256[] storage slots = _dynamicLiterefStorage[tokenId].slots; + // start at offset + for (uint256 i = 0; i < realCount; i++) { + uint256 idx = offset + i; + uint slotNumber = idx / 4; // integer division will get the correct slot number + uint shift = (idx % 4) * 64; // the remainder will give the correct shift + uint64 ref = uint64(slots[slotNumber] >> shift); + (address attributeAddress, uint256 attributeTokenId) = getReferenceAddressAndTokenId(ref); + addresses[i] = attributeAddress; + tokenIds[i] = attributeTokenId; + } + } + + function _checkWriteAuth() internal override(Patchwork721, PatchworkLiteRef) view returns (bool allow) { + return Patchwork721._checkWriteAuth(); + } + + function burn(uint256 tokenId) public { + // test only + _burn(tokenId); + } +} \ No newline at end of file diff --git a/src/sampleNFTs/TestFragmentLiteRefNFT.sol b/test/nfts/TestFragmentLiteRefNFT.sol similarity index 67% rename from src/sampleNFTs/TestFragmentLiteRefNFT.sol rename to test/nfts/TestFragmentLiteRefNFT.sol index 326cfa3..3b5aebd 100644 --- a/src/sampleNFTs/TestFragmentLiteRefNFT.sol +++ b/test/nfts/TestFragmentLiteRefNFT.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; /* Prototype - Generated Patchwork Meta contract for Totem NFT. @@ -9,11 +9,9 @@ pragma solidity ^0.8.13; Has metadata as defined in totem-metadata.json */ -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "forge-std/console.sol"; -import "../PatchworkNFTInterface.sol"; -import "../PatchworkNFTBase.sol"; - +import "../../src/PatchworkFragmentSingle.sol"; +import "../../src/PatchworkLiteRef.sol"; +import "../../src/interfaces/IPatchworkMintable.sol"; enum FragmentType { BASE, @@ -29,7 +27,7 @@ struct TestFragmentLiteRefNFTMetadata { string name; } -contract TestFragmentLiteRefNFT is PatchworkFragment, PatchworkLiteRef { +contract TestFragmentLiteRefNFT is PatchworkFragmentSingle, PatchworkLiteRef, IPatchworkMintable { uint256 _nextTokenId; bool _testLockOverride; @@ -38,21 +36,35 @@ contract TestFragmentLiteRefNFT is PatchworkFragment, PatchworkLiteRef { bool _getAssignedToOverrideSet; address _getAssignedToOverride; - constructor (address _manager) PatchworkNFT("testscope", "TestFragmentLiteRef", "TFLR", msg.sender, _manager) { + constructor (address _manager) Patchwork721("testscope", "TestFragmentLiteRef", "TFLR", _manager, msg.sender) { } // ERC-165 - function supportsInterface(bytes4 interfaceID) public view virtual override(PatchworkFragment, PatchworkLiteRef) returns (bool) { + function supportsInterface(bytes4 interfaceID) public view virtual override(PatchworkFragmentSingle, PatchworkLiteRef) returns (bool) { return PatchworkLiteRef.supportsInterface(interfaceID) || - PatchworkFragment.supportsInterface(interfaceID); + PatchworkFragmentSingle.supportsInterface(interfaceID) || + interfaceID == type(IPatchworkMintable).interfaceId; } - function mint(address to) external returns (uint256 tokenId) { + function mint(address to, bytes calldata /* data */) public payable returns (uint256 tokenId) { + if (msg.value > 0) { + revert(); + } tokenId = _nextTokenId; _nextTokenId++; _safeMint(to, tokenId); _metadataStorage[tokenId] = new uint256[](3); } + + function mintBatch(address to, bytes calldata data, uint256 quantity) public payable returns (uint256[] memory tokenIds) { + if (msg.value > 0) { + revert(); + } + tokenIds = new uint256[](quantity); + for (uint256 i = 0; i < quantity; i++) { + tokenIds[i] = mint(to, data); + } + } function schemaURI() pure external returns (string memory) { return "https://mything/my-fragment-metadata.json"; @@ -72,9 +84,9 @@ contract TestFragmentLiteRefNFT is PatchworkFragment, PatchworkLiteRef { function schema() pure external returns (MetadataSchema memory) { MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](8); entries[0] = MetadataSchemaEntry(0, 0, FieldType.UINT64, 8, FieldVisibility.PUBLIC, 0, 0, "artifactIDs"); - entries[1] = MetadataSchemaEntry(1, 0, FieldType.UINT8, 0, FieldVisibility.PUBLIC, 2, 0, "fragmentType"); - entries[2] = MetadataSchemaEntry(2, 0, FieldType.UINT16, 0, FieldVisibility.PUBLIC, 2, 8, "rarity"); - entries[3] = MetadataSchemaEntry(3, 0, FieldType.CHAR16, 0, FieldVisibility.PUBLIC, 2, 16, "name"); + entries[1] = MetadataSchemaEntry(1, 0, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 2, 0, "fragmentType"); + entries[2] = MetadataSchemaEntry(2, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 8, "rarity"); + entries[3] = MetadataSchemaEntry(3, 0, FieldType.CHAR16, 1, FieldVisibility.PUBLIC, 2, 16, "name"); return MetadataSchema(1, entries); } @@ -116,72 +128,93 @@ contract TestFragmentLiteRefNFT is PatchworkFragment, PatchworkLiteRef { return unpackMetadata(_metadataStorage[_tokenId]); } - function addReference(uint256 ourTokenId, uint64 referenceAddress) public override { + function addReference(uint256 ourTokenId, uint64 liteRef) public override { require(_checkTokenWriteAuth(ourTokenId), "not authorized"); uint256[] storage mdStorage = _metadataStorage[ourTokenId]; uint256 slot = mdStorage[0]; uint256 slot2 = mdStorage[1]; if (uint64(slot) == 0) { - mdStorage[0] = slot | referenceAddress; + mdStorage[0] = slot | liteRef; } else if (uint64(slot >> 64) == 0) { - mdStorage[0] = slot | uint256(referenceAddress) << 64; + mdStorage[0] = slot | uint256(liteRef) << 64; } else if (uint64(slot >> 128) == 0) { - mdStorage[0] = slot | uint256(referenceAddress) << 128; + mdStorage[0] = slot | uint256(liteRef) << 128; } else if (uint64(slot >> 192) == 0) { - mdStorage[0] = slot | uint256(referenceAddress) << 192; + mdStorage[0] = slot | uint256(liteRef) << 192; } else if (uint64(slot2) == 0) { - mdStorage[0] = slot2 | referenceAddress; + mdStorage[0] = slot2 | liteRef; } else if (uint64(slot2 >> 64) == 0) { - mdStorage[0] = slot2 | uint256(referenceAddress) << 64; + mdStorage[0] = slot2 | uint256(liteRef) << 64; } else if (uint64(slot2 >> 128) == 0) { - mdStorage[0] = slot2 | uint256(referenceAddress) << 128; + mdStorage[0] = slot2 | uint256(liteRef) << 128; } else if (uint64(slot2 >> 192) == 0) { - mdStorage[0] = slot2 | uint256(referenceAddress) << 192; + mdStorage[0] = slot2 | uint256(liteRef) << 192; } else { revert("No reference slots available"); } } - function batchAddReferences(uint256 ourTokenId, uint64[] calldata /*_referenceAddresses*/) public view override { + function addReference(uint256 tokenId, uint64 liteRef, uint256 targetMetadataId) public override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + addReference(tokenId, liteRef); + } + + function removeReference(uint256 tokenId, uint64 liteRef, uint256 targetMetadataId) public override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + removeReference(tokenId, liteRef); + } + + function addReferenceBatch(uint256 tokenId, uint64[] calldata liteRefs, uint256 targetMetadataId) public view override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + addReferenceBatch(tokenId, liteRefs); + } + + function addReferenceBatch(uint256 ourTokenId, uint64[] calldata /*_liteRefs*/) public view override { require(_checkTokenWriteAuth(ourTokenId), "not authorized"); // TODO bulk insert for fewer stores } - function removeReference(uint256 ourTokenId, uint64 referenceAddress) public override { + function removeReference(uint256 ourTokenId, uint64 liteRef) public override { require(_checkTokenWriteAuth(ourTokenId), "not authorized"); uint256[] storage mdStorage = _metadataStorage[ourTokenId]; uint256 slot = mdStorage[0]; uint256 slot2 = mdStorage[1]; - if (uint64(slot) == referenceAddress) { + if (uint64(slot) == liteRef) { mdStorage[0] = slot & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000; - } else if (uint64(slot >> 64) == referenceAddress) { + } else if (uint64(slot >> 64) == liteRef) { mdStorage[0] = slot & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF; - } else if (uint64(slot >> 128) == referenceAddress) { + } else if (uint64(slot >> 128) == liteRef) { mdStorage[0] = slot & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } else if (uint64(slot >> 192) == referenceAddress) { + } else if (uint64(slot >> 192) == liteRef) { mdStorage[0] = slot & 0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } else if (uint64(slot2) == referenceAddress) { + } else if (uint64(slot2) == liteRef) { mdStorage[0] = slot2 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000; - } else if (uint64(slot2 >> 64) == referenceAddress) { + } else if (uint64(slot2 >> 64) == liteRef) { mdStorage[0] = slot2 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF; - } else if (uint64(slot2 >> 128) == referenceAddress) { + } else if (uint64(slot2 >> 128) == liteRef) { mdStorage[0] = slot2 & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } else if (uint64(slot2 >> 192) == referenceAddress) { + } else if (uint64(slot2 >> 192) == liteRef) { mdStorage[0] = slot2 & 0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; } else { revert("not assigned"); } } - function loadReferenceAddressAndTokenId(uint256 idx) public view returns (address addr, uint256 tokenId) { - uint256[] storage slots = _metadataStorage[tokenId]; + function loadReferenceAddressAndTokenId(uint256 ourTokenId, uint256 idx) public view returns (address addr, uint256 tokenId) { + uint256[] storage slots = _metadataStorage[ourTokenId]; uint slotNumber = idx / 4; uint shift = (idx % 4) * 64; uint64 attributeId = uint64(slots[slotNumber] >> shift); return getReferenceAddressAndTokenId(attributeId); } - function loadAllReferences(uint256 tokenId) public view returns (address[] memory addresses, uint256[] memory tokenIds) { + function loadAllStaticReferences(uint256 tokenId) public view override returns (address[] memory addresses, uint256[] memory tokenIds) { uint256[] storage slots = _metadataStorage[tokenId]; addresses = new address[](8); tokenIds = new uint256[](8); @@ -196,8 +229,8 @@ contract TestFragmentLiteRefNFT is PatchworkFragment, PatchworkLiteRef { return (addresses, tokenIds); } - function _checkWriteAuth() internal override(PatchworkNFT, PatchworkLiteRef) view returns (bool allow) { - return PatchworkNFT._checkWriteAuth(); + function _checkWriteAuth() internal override(Patchwork721, PatchworkLiteRef) view returns (bool allow) { + return Patchwork721._checkWriteAuth(); } // Function for mocking test behaviors - set to true for it to return unlocked always @@ -220,7 +253,7 @@ contract TestFragmentLiteRefNFT is PatchworkFragment, PatchworkLiteRef { } // Testing overrides - function getLiteReference(address addr, uint256 tokenId) public virtual override view returns (uint64 referenceAddress, bool redacted) { + function getLiteReference(address addr, uint256 tokenId) public virtual override view returns (uint64 liteRef, bool redacted) { if (_getLiteRefOverrideSet) { return (_getLiteRefOverride, false); } @@ -239,4 +272,9 @@ contract TestFragmentLiteRefNFT is PatchworkFragment, PatchworkLiteRef { } return super.getAssignedTo(ourTokenId); } + + function setScopeName(string memory scopeName) public { + // For testing only + _scopeName = scopeName; + } } \ No newline at end of file diff --git a/test/nfts/TestFragmentSingleNFT.sol b/test/nfts/TestFragmentSingleNFT.sol new file mode 100644 index 0000000..ef5d717 --- /dev/null +++ b/test/nfts/TestFragmentSingleNFT.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/* + Prototype - Generated Patchwork Meta contract for Totem NFT. + + Is attached to any normal NFT and is scoped for a specific application. + + Has metadata as defined in totem-metadata.json +*/ + +import "../../src/PatchworkPatch.sol"; +import "../../src/PatchworkFragmentSingle.sol"; + +struct TestFragmentSingleNFTMetadata { + uint16 xp; + uint8 level; + uint16 xpLost; + uint16 stakedMade; + uint16 stakedCorrect; + uint8 evolution; + string nickname; +} + +contract TestFragmentSingleNFT is PatchworkFragmentSingle { + + uint256 _nextTokenId; + + constructor(address manager_) Patchwork721("testscope", "TestPatchFragment", "TPLR", manager_, msg.sender) PatchworkFragmentSingle() { + } + + function schemaURI() pure external override returns (string memory) { + return "https://mything/my-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external override returns (string memory) {} + + /* + Hard coded prototype schema is: + slot 0 offset 0 = artifactIDs (spans 2) - also we need special built-in handling for < 256 bit IDs + slot 2 offset 0 = xp + slot 2 offset 16 = level + slot 2 offset 24 = xpLost + slot 2 offset 40 = stakedMade + slot 2 offset 56 = stakedCorrect + slot 2 offset 72 = evolution + slot 2 offset 80 = nickname + */ + function schema() pure external override returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](8); + entries[1] = MetadataSchemaEntry(1, 1, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 0, "xp"); + entries[2] = MetadataSchemaEntry(2, 2, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 2, 16, "level"); + entries[3] = MetadataSchemaEntry(3, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 24, "xpLost"); + entries[4] = MetadataSchemaEntry(4, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 40, "stakedMade"); + entries[5] = MetadataSchemaEntry(5, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 56, "stakedCorrect"); + entries[6] = MetadataSchemaEntry(6, 0, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 2, 72, "evolution"); + entries[7] = MetadataSchemaEntry(7, 0, FieldType.CHAR16, 1, FieldVisibility.PUBLIC, 2, 80, "nickname"); + return MetadataSchema(1, entries); + } + + function packMetadata(TestFragmentSingleNFTMetadata memory data) public pure returns (uint256[] memory slots) { + bytes32 nickname; + bytes memory ns = bytes(data.nickname); + + assembly { + nickname := mload(add(ns, 32)) + } + slots = new uint256[](1); + slots[0] = uint256(data.xp) | uint256(data.level) << 16 | uint256(data.xpLost) << 24 | uint256(data.stakedMade) << 40 | uint256(data.stakedCorrect) << 56 | uint256(data.evolution) << 72 | uint256(nickname) >> 128 << 80; + return slots; + } + + function storeMetadata(uint256 _tokenId, TestFragmentSingleNFTMetadata memory data) public { + require(_checkTokenWriteAuth(_tokenId), "not authorized"); + _metadataStorage[_tokenId] = packMetadata(data); + } + + function unpackMetadata(uint256[] memory slots) public pure returns (TestFragmentSingleNFTMetadata memory data) { + data.xp = uint16(slots[0]); + data.level = uint8(slots[0] >> 16); + data.xpLost = uint16(slots[0] >> 24); + data.stakedMade = uint16(slots[0] >> 40); + data.stakedCorrect = uint16(slots[0] >> 56); + data.evolution = uint8(slots[0] >> 72); + data.nickname = string(abi.encodePacked(bytes16(uint128(slots[0] >> 80)))); + return data; + } + + function loadMetadata(uint256 _tokenId) public view returns (TestFragmentSingleNFTMetadata memory data) { + return unpackMetadata(_metadataStorage[_tokenId]); + } + + function mint(address to, bytes memory /*data*/) external returns (uint256 tokenId){ + // Just for testing + tokenId = _nextTokenId; + _nextTokenId++; + _safeMint(to, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + return tokenId; + } + + function burn(uint256 tokenId) public { + // test only + _burn(tokenId); + } +} \ No newline at end of file diff --git a/test/nfts/TestMultiFragmentNFT.sol b/test/nfts/TestMultiFragmentNFT.sol new file mode 100644 index 0000000..97597e1 --- /dev/null +++ b/test/nfts/TestMultiFragmentNFT.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "../../src/PatchworkFragmentMulti.sol"; +import "../../src/PatchworkLiteRef.sol"; +import "../../src/interfaces/IPatchworkMintable.sol"; + +struct TestMultiFragmentNFTMetadata { + uint8 nothing; +} + +contract TestMultiFragmentNFT is PatchworkFragmentMulti, IPatchworkMintable { + uint256 _nextTokenId; + + constructor (address _manager) Patchwork721("testscope", "TestMultiFragmentNFT", "TFLR", _manager, msg.sender) { + } + + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return PatchworkFragmentMulti.supportsInterface(interfaceID) || + interfaceID == type(IPatchworkMintable).interfaceId; + } + + function mint(address to, bytes calldata /* data */) public payable returns (uint256 tokenId) { + if (msg.value > 0) { + revert(); + } + tokenId = _nextTokenId; + _nextTokenId++; + _safeMint(to, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + } + + function mintBatch(address to, bytes calldata data, uint256 quantity) public payable returns (uint256[] memory tokenIds) { + if (msg.value > 0) { + revert(); + } + tokenIds = new uint256[](quantity); + for (uint256 i = 0; i < quantity; i++) { + tokenIds[i] = mint(to, data); + } + } + + function setScopeName(string memory scopeName) public { + // For testing only + _scopeName = scopeName; + } + + function schemaURI() pure external returns (string memory) { + return "https://mything/my-fragment-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external returns (string memory) { + return string(abi.encodePacked("https://mything/fragment-", _tokenId)); + } + + /* + Hard coded prototype schema is: + slot 0 offset 0 = nothing + */ + function schema() pure external returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](8); + entries[0] = MetadataSchemaEntry(0, 0, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 0, 0, "nothing"); + return MetadataSchema(1, entries); + } + + function packMetadata(TestMultiFragmentNFTMetadata memory data) public pure returns (uint256[] memory slots) { + slots = new uint256[](1); + slots[0] = uint256(data.nothing); + return slots; + } + + function storeMetadata(uint256 _tokenId, TestMultiFragmentNFTMetadata memory data) public { + require(_checkTokenWriteAuth(_tokenId), "not authorized"); + _metadataStorage[_tokenId] = packMetadata(data); + } + + function unpackMetadata(uint256[] memory slots) public pure returns (TestMultiFragmentNFTMetadata memory data) { + data.nothing = uint8(slots[0]); + return data; + } + + function loadMetadata(uint256 _tokenId) public view returns (TestMultiFragmentNFTMetadata memory data) { + return unpackMetadata(_metadataStorage[_tokenId]); + } +} \ No newline at end of file diff --git a/test/nfts/TestPatchFragmentNFT.sol b/test/nfts/TestPatchFragmentNFT.sol new file mode 100644 index 0000000..68474f5 --- /dev/null +++ b/test/nfts/TestPatchFragmentNFT.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/* + Prototype - Generated Patchwork Meta contract for Totem NFT. + + Is attached to any normal NFT and is scoped for a specific application. + + Has metadata as defined in totem-metadata.json +*/ + +import "../../src/PatchworkPatch.sol"; +import "../../src/PatchworkFragmentSingle.sol"; + +struct TestPatchFragmentNFTMetadata { + uint16 xp; + uint8 level; + uint16 xpLost; + uint16 stakedMade; + uint16 stakedCorrect; + uint8 evolution; + string nickname; +} + +contract TestPatchFragmentNFT is PatchworkReversiblePatch, PatchworkFragmentSingle { + + uint256 _nextTokenId; + + constructor(address manager_) Patchwork721("testscope", "TestPatchFragment", "TPLR", manager_, msg.sender) PatchworkFragmentSingle() { + } + + // ERC-165 + function supportsInterface(bytes4 interfaceID) public view virtual override(PatchworkReversiblePatch, PatchworkFragmentSingle) returns (bool) { + return PatchworkFragmentSingle.supportsInterface(interfaceID) || + PatchworkReversiblePatch.supportsInterface(interfaceID); + } + + function schemaURI() pure external override returns (string memory) { + return "https://mything/my-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external override returns (string memory) {} + + function setLocked(uint256 tokenId, bool locked_) public view virtual override(PatchworkPatch, PatchworkFragmentSingle) { + return PatchworkPatch.setLocked(tokenId, locked_); + } + + function locked(uint256 /* tokenId */) public pure virtual override(PatchworkPatch, PatchworkFragmentSingle) returns (bool) { + return false; + } + + function ownerOf(uint256 tokenId) public view virtual override(PatchworkPatch, PatchworkFragmentSingle) returns (address) { + return PatchworkPatch.ownerOf(tokenId); + } + + function updateOwnership(uint256 tokenId) public virtual override(IPatchworkPatch, PatchworkPatch, PatchworkFragmentSingle) { + PatchworkPatch.updateOwnership(tokenId); + } + + /* + Hard coded prototype schema is: + slot 0 offset 0 = artifactIDs (spans 2) - also we need special built-in handling for < 256 bit IDs + slot 2 offset 0 = xp + slot 2 offset 16 = level + slot 2 offset 24 = xpLost + slot 2 offset 40 = stakedMade + slot 2 offset 56 = stakedCorrect + slot 2 offset 72 = evolution + slot 2 offset 80 = nickname + */ + function schema() pure external override returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](8); + entries[1] = MetadataSchemaEntry(1, 1, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 0, "xp"); + entries[2] = MetadataSchemaEntry(2, 2, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 2, 16, "level"); + entries[3] = MetadataSchemaEntry(3, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 24, "xpLost"); + entries[4] = MetadataSchemaEntry(4, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 40, "stakedMade"); + entries[5] = MetadataSchemaEntry(5, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 56, "stakedCorrect"); + entries[6] = MetadataSchemaEntry(6, 0, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 2, 72, "evolution"); + entries[7] = MetadataSchemaEntry(7, 0, FieldType.CHAR16, 1, FieldVisibility.PUBLIC, 2, 80, "nickname"); + return MetadataSchema(1, entries); + } + + function packMetadata(TestPatchFragmentNFTMetadata memory data) public pure returns (uint256[] memory slots) { + bytes32 nickname; + bytes memory ns = bytes(data.nickname); + + assembly { + nickname := mload(add(ns, 32)) + } + slots = new uint256[](1); + slots[0] = uint256(data.xp) | uint256(data.level) << 16 | uint256(data.xpLost) << 24 | uint256(data.stakedMade) << 40 | uint256(data.stakedCorrect) << 56 | uint256(data.evolution) << 72 | uint256(nickname) >> 128 << 80; + return slots; + } + + function storeMetadata(uint256 _tokenId, TestPatchFragmentNFTMetadata memory data) public { + require(_checkTokenWriteAuth(_tokenId), "not authorized"); + _metadataStorage[_tokenId] = packMetadata(data); + } + + function unpackMetadata(uint256[] memory slots) public pure returns (TestPatchFragmentNFTMetadata memory data) { + data.xp = uint16(slots[0]); + data.level = uint8(slots[0] >> 16); + data.xpLost = uint16(slots[0] >> 24); + data.stakedMade = uint16(slots[0] >> 40); + data.stakedCorrect = uint16(slots[0] >> 56); + data.evolution = uint8(slots[0] >> 72); + data.nickname = string(abi.encodePacked(bytes16(uint128(slots[0] >> 80)))); + return data; + } + + function loadMetadata(uint256 _tokenId) public view returns (TestPatchFragmentNFTMetadata memory data) { + return unpackMetadata(_metadataStorage[_tokenId]); + } + + function mintPatch(address originalNFTOwner, PatchTarget memory target) external payable mustBeManager returns (uint256 tokenId){ + if (msg.value > 0) { + revert(); + } + // Just for testing + tokenId = _nextTokenId; + _nextTokenId++; + _storePatch(tokenId, target); + _safeMint(originalNFTOwner, tokenId); + _metadataStorage[tokenId] = new uint256[](3); + return tokenId; + } + + function burn(uint256 tokenId) public { + // test only + _burnPatch(tokenId); + } +} \ No newline at end of file diff --git a/src/sampleNFTs/TestPatchLiteRefNFT.sol b/test/nfts/TestPatchLiteRefNFT.sol similarity index 71% rename from src/sampleNFTs/TestPatchLiteRefNFT.sol rename to test/nfts/TestPatchLiteRefNFT.sol index c099e32..f334463 100644 --- a/src/sampleNFTs/TestPatchLiteRefNFT.sol +++ b/test/nfts/TestPatchLiteRefNFT.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; /* Prototype - Generated Patchwork Meta contract for Totem NFT. @@ -9,11 +9,8 @@ pragma solidity ^0.8.13; Has metadata as defined in totem-metadata.json */ -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "forge-std/console.sol"; -import "../PatchworkNFTInterface.sol"; -import "../PatchworkNFTBase.sol"; - +import "../../src/PatchworkPatch.sol"; +import "../../src/PatchworkLiteRef.sol"; struct TestPatchLiteRefNFTMetadata { uint64[8] artifactIDs; @@ -30,7 +27,7 @@ contract TestPatchLiteRefNFT is PatchworkPatch, PatchworkLiteRef { uint256 _nextTokenId; - constructor(address manager_) PatchworkNFT("testscope", "TestPatchLiteRef", "TPLR", msg.sender, manager_) PatchworkLiteRef() { + constructor(address manager_) Patchwork721("testscope", "TestPatchLiteRef", "TPLR", manager_, msg.sender) PatchworkLiteRef() { } // ERC-165 @@ -45,11 +42,6 @@ contract TestPatchLiteRefNFT is PatchworkPatch, PatchworkLiteRef { function imageURI(uint256 _tokenId) pure external override returns (string memory) {} - function setManager(address manager_) external { - require(_checkWriteAuth()); - _manager = manager_; - } - /* Hard coded prototype schema is: slot 0 offset 0 = artifactIDs (spans 2) - also we need special built-in handling for < 256 bit IDs @@ -64,13 +56,13 @@ contract TestPatchLiteRefNFT is PatchworkPatch, PatchworkLiteRef { function schema() pure external override returns (MetadataSchema memory) { MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](8); entries[0] = MetadataSchemaEntry(0, 0, FieldType.UINT64, 8, FieldVisibility.PUBLIC, 0, 0, "artifactIDs"); - entries[1] = MetadataSchemaEntry(1, 1, FieldType.UINT16, 0, FieldVisibility.PUBLIC, 2, 0, "xp"); - entries[2] = MetadataSchemaEntry(2, 2, FieldType.UINT8, 0, FieldVisibility.PUBLIC, 2, 16, "level"); - entries[3] = MetadataSchemaEntry(3, 0, FieldType.UINT16, 0, FieldVisibility.PUBLIC, 2, 24, "xpLost"); - entries[4] = MetadataSchemaEntry(4, 0, FieldType.UINT16, 0, FieldVisibility.PUBLIC, 2, 40, "stakedMade"); - entries[5] = MetadataSchemaEntry(5, 0, FieldType.UINT16, 0, FieldVisibility.PUBLIC, 2, 56, "stakedCorrect"); - entries[6] = MetadataSchemaEntry(6, 0, FieldType.UINT8, 0, FieldVisibility.PUBLIC, 2, 72, "evolution"); - entries[7] = MetadataSchemaEntry(7, 0, FieldType.CHAR16, 0, FieldVisibility.PUBLIC, 2, 80, "nickname"); + entries[1] = MetadataSchemaEntry(1, 1, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 0, "xp"); + entries[2] = MetadataSchemaEntry(2, 2, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 2, 16, "level"); + entries[3] = MetadataSchemaEntry(3, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 24, "xpLost"); + entries[4] = MetadataSchemaEntry(4, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 40, "stakedMade"); + entries[5] = MetadataSchemaEntry(5, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 2, 56, "stakedCorrect"); + entries[6] = MetadataSchemaEntry(6, 0, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 2, 72, "evolution"); + entries[7] = MetadataSchemaEntry(7, 0, FieldType.CHAR16, 1, FieldVisibility.PUBLIC, 2, 80, "nickname"); return MetadataSchema(1, entries); } @@ -142,82 +134,109 @@ contract TestPatchLiteRefNFT is PatchworkPatch, PatchworkLiteRef { return uint16(uint256(_metadataStorage[_tokenId][2]) >> 16); } - function addReference(uint256 ourTokenId, uint64 referenceAddress) public override { + function addReference(uint256 ourTokenId, uint64 liteRef) public override { require(_checkTokenWriteAuth(ourTokenId), "not authorized"); uint256[] storage mdStorage = _metadataStorage[ourTokenId]; uint256 slot = mdStorage[0]; uint256 slot2 = mdStorage[1]; if (uint64(slot) == 0) { - mdStorage[0] = slot | referenceAddress; + mdStorage[0] = slot | liteRef; } else if (uint64(slot >> 64) == 0) { - mdStorage[0] = slot | uint256(referenceAddress) << 64; + mdStorage[0] = slot | uint256(liteRef) << 64; } else if (uint64(slot >> 128) == 0) { - mdStorage[0] = slot | uint256(referenceAddress) << 128; + mdStorage[0] = slot | uint256(liteRef) << 128; } else if (uint64(slot >> 192) == 0) { - mdStorage[0] = slot | uint256(referenceAddress) << 192; + mdStorage[0] = slot | uint256(liteRef) << 192; } else if (uint64(slot2) == 0) { - mdStorage[0] = slot2 | referenceAddress; + mdStorage[0] = slot2 | liteRef; } else if (uint64(slot2 >> 64) == 0) { - mdStorage[0] = slot2 | uint256(referenceAddress) << 64; + mdStorage[0] = slot2 | uint256(liteRef) << 64; } else if (uint64(slot2 >> 128) == 0) { - mdStorage[0] = slot2 | uint256(referenceAddress) << 128; + mdStorage[0] = slot2 | uint256(liteRef) << 128; } else if (uint64(slot2 >> 192) == 0) { - mdStorage[0] = slot2 | uint256(referenceAddress) << 192; + mdStorage[0] = slot2 | uint256(liteRef) << 192; } else { revert("No reference slots available"); } } - function batchAddReferences(uint256 ourTokenId, uint64[] calldata /*_referenceAddresses*/) public view override { + function addReferenceBatch(uint256 ourTokenId, uint64[] calldata /*_liteRefs*/) public view override { require(_checkTokenWriteAuth(ourTokenId), "not authorized"); // TODO bulk insert for fewer stores } - function mintPatch(address originalNFTOwner, address originalNFTAddress, uint originalNFTTokenId) external returns (uint256 tokenId){ + function mintPatch(address owner, PatchTarget memory target) external payable mustBeManager() returns (uint256 tokenId){ + if (msg.value > 0) { + revert(); + } + // require inherited ownership + if (IERC721(target.addr).ownerOf(target.tokenId) != owner) { + revert IPatchworkProtocol.NotAuthorized(owner); + } // Just for testing tokenId = _nextTokenId; _nextTokenId++; - _storePatch(tokenId, originalNFTAddress, originalNFTTokenId); - _safeMint(originalNFTOwner, tokenId); + _storePatch(tokenId, target); + _safeMint(owner, tokenId); _metadataStorage[tokenId] = new uint256[](3); return tokenId; } - function removeReference(uint256 ourTokenId, uint64 referenceAddress) public override { + function removeReference(uint256 ourTokenId, uint64 liteRef) public override { require(_checkTokenWriteAuth(ourTokenId), "not authorized"); uint256[] storage mdStorage = _metadataStorage[ourTokenId]; uint256 slot = mdStorage[0]; uint256 slot2 = mdStorage[1]; - if (uint64(slot) == referenceAddress) { + if (uint64(slot) == liteRef) { mdStorage[0] = slot & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000; - } else if (uint64(slot >> 64) == referenceAddress) { + } else if (uint64(slot >> 64) == liteRef) { mdStorage[0] = slot & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF; - } else if (uint64(slot >> 128) == referenceAddress) { + } else if (uint64(slot >> 128) == liteRef) { mdStorage[0] = slot & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } else if (uint64(slot >> 192) == referenceAddress) { + } else if (uint64(slot >> 192) == liteRef) { mdStorage[0] = slot & 0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } else if (uint64(slot2) == referenceAddress) { + } else if (uint64(slot2) == liteRef) { mdStorage[0] = slot2 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000; - } else if (uint64(slot2 >> 64) == referenceAddress) { + } else if (uint64(slot2 >> 64) == liteRef) { mdStorage[0] = slot2 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF; - } else if (uint64(slot2 >> 128) == referenceAddress) { + } else if (uint64(slot2 >> 128) == liteRef) { mdStorage[0] = slot2 & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } else if (uint64(slot2 >> 192) == referenceAddress) { + } else if (uint64(slot2 >> 192) == liteRef) { mdStorage[0] = slot2 & 0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; } else { revert("not assigned"); } } - function loadReferenceAddressAndTokenId(uint256 idx) public view returns (address addr, uint256 tokenId) { - uint256[] storage slots = _metadataStorage[tokenId]; + function addReference(uint256 tokenId, uint64 liteRef, uint256 targetMetadataId) public override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + addReference(tokenId, liteRef); + } + + + function removeReference(uint256 tokenId, uint64 liteRef, uint256 /*targetMetadataId*/) public override { + // Work with any given metadataId just for coverage testing + removeReference(tokenId, liteRef); + } + + function addReferenceBatch(uint256 tokenId, uint64[] calldata liteRefs, uint256 targetMetadataId) public view override { + if (targetMetadataId != 0) { + revert("Unsupported metadata ID"); + } + addReferenceBatch(tokenId, liteRefs); + } + + function loadReferenceAddressAndTokenId(uint256 ourTokenId, uint256 idx) public view returns (address addr, uint256 tokenId) { + uint256[] storage slots = _metadataStorage[ourTokenId]; uint slotNumber = idx / 4; uint shift = (idx % 4) * 64; uint64 attributeId = uint64(slots[slotNumber] >> shift); return getReferenceAddressAndTokenId(attributeId); } - function loadAllReferences(uint256 tokenId) public view returns (address[] memory addresses, uint256[] memory tokenIds) { + function loadAllStaticReferences(uint256 tokenId) public view override returns (address[] memory addresses, uint256[] memory tokenIds) { uint256[] storage slots = _metadataStorage[tokenId]; addresses = new address[](8); tokenIds = new uint256[](8); @@ -232,7 +251,12 @@ contract TestPatchLiteRefNFT is PatchworkPatch, PatchworkLiteRef { return (addresses, tokenIds); } - function _checkWriteAuth() internal override(PatchworkNFT, PatchworkLiteRef) view returns (bool allow) { - return PatchworkNFT._checkWriteAuth(); + function _checkWriteAuth() internal override(Patchwork721, PatchworkLiteRef) view returns (bool allow) { + return Patchwork721._checkWriteAuth(); + } + + function burn(uint256 tokenId) public { + // test only + _burnPatch(tokenId); } } \ No newline at end of file diff --git a/test/nfts/TestPatchNFT.sol b/test/nfts/TestPatchNFT.sol new file mode 100644 index 0000000..2633110 --- /dev/null +++ b/test/nfts/TestPatchNFT.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/* + Prototype - Generated Patchwork Meta contract for Totem NFT. + + Is attached to any normal NFT and is scoped for a specific application. + + Has metadata as defined in totem-metadata.json +*/ + +import "../../src/PatchworkPatch.sol"; + +struct TestPatchNFTMetadata { + uint16 xp; + uint8 level; + uint16 xpLost; + uint16 stakedMade; + uint16 stakedCorrect; + uint8 evolution; + string nickname; +} + +contract TestPatchNFT is PatchworkPatch { + + uint256 _nextTokenId; + + constructor(address manager_) Patchwork721("testscope", "TestPatchLiteRef", "TPLR", manager_, msg.sender) { + } + + function schemaURI() pure external override returns (string memory) { + return "https://mything/my-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external override returns (string memory) {} + + function schema() pure external override returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](7); + entries[0] = MetadataSchemaEntry(0, 1, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 0, "xp"); + entries[1] = MetadataSchemaEntry(1, 2, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 0, 16, "level"); + entries[2] = MetadataSchemaEntry(2, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 24, "xpLost"); + entries[3] = MetadataSchemaEntry(3, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 40, "stakedMade"); + entries[4] = MetadataSchemaEntry(4, 0, FieldType.UINT16, 1, FieldVisibility.PUBLIC, 0, 56, "stakedCorrect"); + entries[5] = MetadataSchemaEntry(5, 0, FieldType.UINT8, 1, FieldVisibility.PUBLIC, 0, 72, "evolution"); + entries[6] = MetadataSchemaEntry(6, 0, FieldType.CHAR16, 1, FieldVisibility.PUBLIC, 0, 80, "nickname"); + return MetadataSchema(1, entries); + } + + function packMetadata(TestPatchNFTMetadata memory data) public pure returns (uint256[] memory slots) { + bytes32 nickname; + bytes memory ns = bytes(data.nickname); + + assembly { + nickname := mload(add(ns, 32)) + } + slots = new uint256[](1); + slots[0] = uint256(data.xp) | uint256(data.level) << 16 | uint256(data.xpLost) << 24 | uint256(data.stakedMade) << 40 | uint256(data.stakedCorrect) << 56 | uint256(data.evolution) << 72 | uint256(nickname) >> 128 << 80; + return slots; + } + + function storeMetadata(uint256 _tokenId, TestPatchNFTMetadata memory data) public { + require(_checkTokenWriteAuth(_tokenId), "not authorized"); + _metadataStorage[_tokenId] = packMetadata(data); + } + + function unpackMetadata(uint256[] memory slots) public pure returns (TestPatchNFTMetadata memory data) { + data.xp = uint16(slots[0]); + data.level = uint8(slots[0] >> 16); + data.xpLost = uint16(slots[0] >> 24); + data.stakedMade = uint16(slots[0] >> 40); + data.stakedCorrect = uint16(slots[0] >> 56); + data.evolution = uint8(slots[0] >> 72); + data.nickname = string(abi.encodePacked(bytes16(uint128(slots[0] >> 80)))); + return data; + } + + function loadMetadata(uint256 _tokenId) public view returns (TestPatchNFTMetadata memory data) { + return unpackMetadata(_metadataStorage[_tokenId]); + } + + function mintPatch(address owner, PatchTarget memory target) external payable mustBeManager returns (uint256 tokenId) { + if (msg.value > 0) { + revert(); + } + // require inherited ownership + if (IERC721(target.addr).ownerOf(target.tokenId) != owner) { + revert IPatchworkProtocol.NotAuthorized(owner); + } + // Just for testing + tokenId = _nextTokenId; + _nextTokenId++; + _storePatch(tokenId, target); + _safeMint(owner, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + return tokenId; + } + + function burn(uint256 tokenId) public { + // test only - protocol does not currently support this as you can't mint another patch later + _burnPatch(tokenId); + } +} \ No newline at end of file diff --git a/test/nfts/TestPatchworkNFT.sol b/test/nfts/TestPatchworkNFT.sol new file mode 100644 index 0000000..4831d97 --- /dev/null +++ b/test/nfts/TestPatchworkNFT.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import "../../src/Patchwork721.sol"; +import "../../src/interfaces/IPatchworkMintable.sol"; + +contract TestPatchworkNFT is Patchwork721, IPatchworkMintable { + + uint256 _nextTokenId; + + struct TestPatchworkNFTMetadata { + uint256 thing; + } + + constructor(address manager_) Patchwork721("testscope", "TestPatchworkNFT", "TPLR", manager_, msg.sender) { + } + + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return Patchwork721.supportsInterface(interfaceID) || + interfaceID == type(IPatchworkMintable).interfaceId; + } + + function schemaURI() pure external returns (string memory) { + return "https://mything/my-nft-metadata.json"; + } + + function imageURI(uint256 _tokenId) pure external returns (string memory) { + return string(abi.encodePacked("https://mything/nft-", _tokenId)); + } + + function schema() pure external returns (MetadataSchema memory) { + MetadataSchemaEntry[] memory entries = new MetadataSchemaEntry[](1); + entries[0] = MetadataSchemaEntry(1, 0, FieldType.UINT256, 1, FieldVisibility.PUBLIC, 2, 0, "thing"); + return MetadataSchema(1, entries); + } + + function getScopeName() public view override (Patchwork721, IPatchworkScoped) returns (string memory scopeName) { + return Patchwork721.getScopeName(); + } + + function mint(address to, bytes calldata /* data */) public payable returns (uint256 tokenId) { + if (msg.value > 0) { + revert(); + } + tokenId = _nextTokenId; + _nextTokenId++; + _mint(to, tokenId); + _metadataStorage[tokenId] = new uint256[](1); + } + + function mintBatch(address to, bytes calldata data, uint256 quantity) public payable returns (uint256[] memory tokenIds) { + if (msg.value > 0) { + revert(); + } + tokenIds = new uint256[](quantity); + for (uint256 i = 0; i < quantity; i++) { + tokenIds[i] = mint(to, data); + } + } +} \ No newline at end of file