From 2d8650dc7c30fa8f56a3b0a25ee3c6f5a9958e27 Mon Sep 17 00:00:00 2001 From: stadolf Date: Tue, 24 Sep 2024 15:56:11 +0200 Subject: [PATCH 1/4] repurpose MintAuthorizer to allow signed metadata amendments Signed-off-by: stadolf --- src/IPNFT.sol | 13 +++++++++++++ subgraph/abis/IPNFT.json | 23 +++++++++++++++++++++++ test/IPNFT.t.sol | 22 ++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/src/IPNFT.sol b/src/IPNFT.sol index 597869e3..4005f02d 100644 --- a/src/IPNFT.sol +++ b/src/IPNFT.sol @@ -161,6 +161,18 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser emit ReadAccessGranted(tokenId, reader, until); } + function amendMetadata(uint256 tokenId, string calldata _newTokenURI, bytes calldata authorization) external { + if (ownerOf(tokenId) != _msgSender()) { + revert Unauthorized(); + } + + if (!mintAuthorizer.authorizeMint(_msgSender(), _msgSender(), abi.encode(SignedMintAuthorization(tokenId, _newTokenURI, authorization)))) { + revert Unauthorized(); + } + + _setTokenURI(tokenId, _newTokenURI); + } + /** * @notice check whether `reader` currently is able to access gated content behind `tokenId` * @param reader the address in question @@ -177,6 +189,7 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser require(success, "transfer failed"); } + /// @inheritdoc UUPSUpgradeable function _authorizeUpgrade(address /*newImplementation*/ ) internal diff --git a/subgraph/abis/IPNFT.json b/subgraph/abis/IPNFT.json index d342d424..b03fe6f6 100644 --- a/subgraph/abis/IPNFT.json +++ b/subgraph/abis/IPNFT.json @@ -4,6 +4,29 @@ "inputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "amendMetadata", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_newTokenURI", + "type": "string", + "internalType": "string" + }, + { + "name": "authorization", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "approve", diff --git a/test/IPNFT.t.sol b/test/IPNFT.t.sol index 05dfa23d..85607c27 100644 --- a/test/IPNFT.t.sol +++ b/test/IPNFT.t.sol @@ -24,6 +24,7 @@ contract IPNFTTest is IPNFTMintHelper { event IPNFTMinted(address indexed owner, uint256 indexed tokenId, string tokenURI, string symbol); event SymbolUpdated(uint256 indexed tokenId, string symbol); event ReadAccessGranted(uint256 indexed tokenId, address indexed reader, uint256 until); + event MetadataUpdate(uint256 tokenId); IPNFT internal ipnft; @@ -216,4 +217,25 @@ contract IPNFTTest is IPNFTMintHelper { vm.warp(block.timestamp + 60); assertFalse(ipnft.canRead(bob, tokenId)); } + + function testOwnerCanAmendMetadataAfterSignoff() public { + mintAToken(ipnft, alice); + + vm.startPrank(deployer); + ipnft.setAuthorizer(new SignedMintAuthorizer(deployer)); + vm.stopPrank(); + + bytes32 authMessageHash = ECDSA.toEthSignedMessageHash(keccak256(abi.encodePacked(alice, alice, uint256(1), "ipfs://QmNewUri"))); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(deployerPk, authMessageHash); + bytes memory authorization = abi.encodePacked(r, s, v); + + vm.startPrank(alice); + vm.expectEmit(true, true, false, false); + emit MetadataUpdate(1); + ipnft.amendMetadata(1, "ipfs://QmNewUri", authorization); + assertEq(ipnft.tokenURI(1), "ipfs://QmNewUri"); + vm.stopPrank(); + } + + } From 892714036b0bc322a291dc455d762150fe4b2edb Mon Sep 17 00:00:00 2001 From: stadolf Date: Tue, 24 Sep 2024 16:18:31 +0200 Subject: [PATCH 2/4] subgraph queries new token uri after update events Signed-off-by: stadolf --- subgraph/src/ipnftMapping.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/subgraph/src/ipnftMapping.ts b/subgraph/src/ipnftMapping.ts index b2e82a7e..b61de5ce 100644 --- a/subgraph/src/ipnftMapping.ts +++ b/subgraph/src/ipnftMapping.ts @@ -3,13 +3,16 @@ import { ByteArray, crypto, ethereum, + log, store } from '@graphprotocol/graph-ts' import { IPNFTMinted as IPNFTMintedEvent, Reserved as ReservedEvent, ReadAccessGranted as ReadAccessGrantedEvent, - Transfer as TransferEvent + Transfer as TransferEvent, + MetadataUpdate as MetadataUpdateEvent, + IPNFT as IPNFTContract } from '../generated/IPNFT/IPNFT' import { Ipnft, Reservation, CanRead } from '../generated/schema' @@ -80,3 +83,19 @@ export function handleMint(event: IPNFTMintedEvent): void { store.remove('Reservation', event.params.tokenId.toString()) ipnft.save() } + +export function handleMetadataUpdated(event: MetadataUpdateEvent): void { + let ipnft = Ipnft.load(event.params._tokenId.toString()) + if (!ipnft) { + log.error('ipnft {} not found', [event.params._tokenId.toString()]) + return + } + + //erc4906 is not emitting the new url, we must query it ourselves + let _ipnftContract = IPNFTContract.bind(event.params._event.address); + let newUri = _ipnftContract.tokenURI(event.params._tokenId) + + ipnft.tokenURI = newUri + ipnft.save() +} + From 8d76ce33257c3104eea7e69da8e0a44cc2109723 Mon Sep 17 00:00:00 2001 From: stadolf Date: Wed, 25 Sep 2024 17:06:42 +0200 Subject: [PATCH 3/4] updates docker images, makes test env run here more flexible cli flags, makes local builds fast again here removes dedicated legacy custom foundry cache path adds a dedicated crowdsale fixture path for when it's not needed Signed-off-by: stadolf --- README.md | 9 ++++++--- deploy/001-ipfs-config.sh | 2 +- deploy/inittest.sh | 3 ++- docker-compose.yml | 12 +++++------- foundry.toml | 1 - package.json | 3 ++- setupLocal.sh | 32 +++++++++++++++++++++----------- subgraph/subgraph.yaml | 2 ++ 8 files changed, 39 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index c3b38b27..ea246fe1 100644 --- a/README.md +++ b/README.md @@ -147,11 +147,14 @@ You can place required env vars in your `.env` file and run `source .env` to get - to rollout a new upgrade on a live network without calling the proxy's upgrade function, you can use `forge script script/UpgradeImplementation.s.sol:DeployImplementation` and invoke the upgrade function manually (e.g. from your multisig) - for the "real" thing you'll need to add `-f` and `--private-key` and finally `--broadcast` params . -### Deploy for local development +### Deploying everything locally -#### Quickstart +You need Docker. -- You can use the shell script `./setupLocal.sh` to deploy all contracts and add the optional `-f` or `--fixture` flag to also run the fixture scripts. +#### Automatically + +- `yarn localenv` sets up *everything* +- use `./setupLocal.sh` to deploy all contracts. Add the optional `-f` or `--fixture` flag to also run the fixture scripts to tokenize one IPNFT or `-fx` to create two crowdsale instances. #### Manual diff --git a/deploy/001-ipfs-config.sh b/deploy/001-ipfs-config.sh index cadf1cb0..b15a7264 100755 --- a/deploy/001-ipfs-config.sh +++ b/deploy/001-ipfs-config.sh @@ -18,7 +18,7 @@ ipfs config --json Gateway.HTTPHeaders.Access-Control-Allow-Credentials '["true" # https://web3.storage/docs/reference/peering/ # allows us to also pull w3s / pinata content from our local machine -ipfs config --json Peering.Peers '[{"ID": "bafzbeibhqavlasjc7dvbiopygwncnrtvjd2xmryk5laib7zyjor6kf3avm","Addrs": ["/dns4/elastic.dag.house/tcp/443/wss"]},{"ID": "QmWaik1eJcGHq1ybTWe7sezRfqKNcDRNkeBaLnGwQJz1Cj","Addrs": ["/dnsaddr/fra1-1.hostnodes.pinata.cloud"]},{"ID": "QmNfpLrQQZr5Ns9FAJKpyzgnDL2GgC6xBug1yUZozKFgu4","Addrs": ["/dnsaddr/fra1-2.hostnodes.pinata.cloud"]},{"ID": "QmPo1ygpngghu5it8u4Mr3ym6SEU2Wp2wA66Z91Y1S1g29","Addrs": ["/dnsaddr/fra1-3.hostnodes.pinata.cloud"]},{"ID": "QmRjLSisUCHVpFa5ELVvX3qVPfdxajxWJEHs9kN3EcxAW6","Addrs": ["/dnsaddr/nyc1-1.hostnodes.pinata.cloud"]},{"ID": "QmPySsdmbczdZYBpbi2oq2WMJ8ErbfxtkG8Mo192UHkfGP","Addrs": ["/dnsaddr/nyc1-2.hostnodes.pinata.cloud"]},{"ID": "QmSarArpxemsPESa6FNkmuu9iSE1QWqPX2R3Aw6f5jq4D5","Addrs": ["/dnsaddr/nyc1-3.hostnodes.pinata.cloud"]}]' +ipfs config --json Peering.Peers '[{"ID": "bafzbeibhqavlasjc7dvbiopygwncnrtvjd2xmryk5laib7zyjor6kf3avm","Addrs": ["/dnsaddr/elastic.dag.house"]},{"ID": "QmWaik1eJcGHq1ybTWe7sezRfqKNcDRNkeBaLnGwQJz1Cj","Addrs": ["/dnsaddr/fra1-1.hostnodes.pinata.cloud"]},{"ID": "QmNfpLrQQZr5Ns9FAJKpyzgnDL2GgC6xBug1yUZozKFgu4","Addrs": ["/dnsaddr/fra1-2.hostnodes.pinata.cloud"]},{"ID": "QmPo1ygpngghu5it8u4Mr3ym6SEU2Wp2wA66Z91Y1S1g29","Addrs": ["/dnsaddr/fra1-3.hostnodes.pinata.cloud"]},{"ID": "QmRjLSisUCHVpFa5ELVvX3qVPfdxajxWJEHs9kN3EcxAW6","Addrs": ["/dnsaddr/nyc1-1.hostnodes.pinata.cloud"]},{"ID": "QmPySsdmbczdZYBpbi2oq2WMJ8ErbfxtkG8Mo192UHkfGP","Addrs": ["/dnsaddr/nyc1-2.hostnodes.pinata.cloud"]},{"ID": "QmSarArpxemsPESa6FNkmuu9iSE1QWqPX2R3Aw6f5jq4D5","Addrs": ["/dnsaddr/nyc1-3.hostnodes.pinata.cloud"]}]' #https://github.com/ipfs/kubo/blob/master/docs/config.md#implicit-defaults-of-gatewaypublicgateways #axios is confused with local ipfs subdomains diff --git a/deploy/inittest.sh b/deploy/inittest.sh index 1cb4a98e..a560f3d4 100755 --- a/deploy/inittest.sh +++ b/deploy/inittest.sh @@ -11,7 +11,8 @@ $DC down --remove-orphans sleep 5 $DC up -d $DC ps -./setupLocal.sh -f + +./setupLocal.sh -fx cd subgraph yarn codegen diff --git a/docker-compose.yml b/docker-compose.yml index 259afdeb..c60f2d9d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,15 @@ #https://github.com/graphprotocol/graph-node/blob/master/docker/docker-compose.yml services: anvil: - image: ghcr.io/foundry-rs/foundry:nightly-a470d635cfcdce68609e9dc5762a3584351bacc1 + image: ghcr.io/foundry-rs/foundry:nightly-883bb1c39f56a525657116874e59e80c2b881b10 + platform: linux/amd64 command: - 'anvil --host 0.0.0.0' ports: - '8545:8545' graph-node: - image: graphprotocol/graph-node:845f8fa + image: graphprotocol/graph-node:990ef4d ports: - '8000:8000' - '8001:8001' @@ -31,7 +32,7 @@ services: GRAPH_LOG: debug GRAPH_ALLOW_NON_DETERMINISTIC_IPFS: 1 ipfs: - image: ipfs/kubo:v0.28.0 + image: ipfs/kubo:v0.30.0 ports: - '5001:5001' - '8080:8080' @@ -39,7 +40,7 @@ services: - ./deploy/001-ipfs-config.sh:/container-init.d/001-ipfs-config.sh # - ./data/ipfs:/data/ipfs postgres: - image: postgres + image: postgres:12-alpine ports: - '5432:5432' command: ['postgres', '-cshared_preload_libraries=pg_stat_statements'] @@ -49,6 +50,3 @@ services: POSTGRES_DB: graph-node PGDATA: '/data/postgres' POSTGRES_INITDB_ARGS: '-E UTF8 --locale=C' - - volumes: - - ./data/postgres:/var/lib/postgresql/data diff --git a/foundry.toml b/foundry.toml index 8605f56b..a9eec11e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,6 @@ src = 'src' out = 'out' libs = ['lib'] test = 'test' -cache_path = 'cache_forge' solc_version = "0.8.18" gas_reports = [ "IPNFT", diff --git a/package.json b/package.json index fadfd480..8984d778 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "license": "MIT", "scripts": { "test": "hardhat test --network hardhat", - "clean": "rm -rf out cache_forge" + "clean": "rm -rf out cache_forge", + "localenv": "./deploy/inittest.sh" }, "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.0.0", diff --git a/setupLocal.sh b/setupLocal.sh index f3b1be7e..6ce78c86 100755 --- a/setupLocal.sh +++ b/setupLocal.sh @@ -6,23 +6,28 @@ set -a . ./.env.example set +a -fixture=0 +fixtures=false +extrafixtures=false # Parse command-line options -while [ "$#" -gt 0 ]; do - case $1 in - -f|--fixture) - fixture=1 - ;; +while getopts "fx" opt; do + case ${opt} in + f) + fixtures=true + ;; + x) + extrafixtures=true + ;; *) echo "Unknown option: $1" exit 1 - ;; + ;; esac - shift done -FSC="forge script -f $RPC_URL --broadcast" +shift $((OPTIND -1)) + +FSC="forge script --chain 31337 --rpc-url $RPC_URL --use 0.8.18 --offline --broadcast --revert-strings debug" # Deployments $FSC script/dev/Ipnft.s.sol:DeployIpnftSuite @@ -34,11 +39,16 @@ $FSC script/dev/Tokens.s.sol:DeployFakeTokens $FSC script/dev/CrowdSale.s.sol:DeployCrowdSale # optionally: fixtures -if [ "$fixture" -eq "1" ]; then +if $fixtures; then echo "Running fixture scripts." $FSC script/dev/Ipnft.s.sol:FixtureIpnft - $FSC script/dev/Tokenizer.s.sol:FixtureTokenizer + $FSC script/dev/Tokenizer.s.sol:FixtureTokenizer +fi + +# optionally: extra fixtures +if $extrafixtures; then + echo "Running extra fixture scripts (crowdsales)." $FSC script/dev/CrowdSale.s.sol:FixtureCrowdSale echo "Waiting 15 seconds until claiming plain sale..." diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index e0c804c0..89d2eb5e 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -27,6 +27,8 @@ dataSources: handler: handleTransfer - event: ReadAccessGranted(indexed uint256,indexed address,uint256) handler: handleReadAccess + - event: MetadataUpdate(uint256) + handler: handleMetadataUpdated file: ./src/ipnftMapping.ts - kind: ethereum/contract name: SchmackoSwap From 06417d67a3fd7ce7e4634fabd39ec01781224cc3 Mon Sep 17 00:00:00 2001 From: stadolf Date: Thu, 3 Oct 2024 20:59:02 +0200 Subject: [PATCH 4/4] ensure that the authorizer only lets the caller pass Signed-off-by: stadolf --- src/IPNFT.sol | 3 +-- test/IPNFT.t.sol | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/IPNFT.sol b/src/IPNFT.sol index 4005f02d..76ef64a0 100644 --- a/src/IPNFT.sol +++ b/src/IPNFT.sol @@ -23,7 +23,7 @@ import { IReservable } from "./IReservable.sol"; \▓▓▓▓▓▓\▓▓ \▓▓ \▓▓\▓▓ \▓▓ */ -/// @title IPNFT V2.4 +/// @title IPNFT V2.5 /// @author molecule.to /// @notice IP-NFTs capture intellectual property to be traded and synthesized contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReservable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable { @@ -188,7 +188,6 @@ contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReser (bool success,) = _msgSender().call{ value: address(this).balance }(""); require(success, "transfer failed"); } - /// @inheritdoc UUPSUpgradeable function _authorizeUpgrade(address /*newImplementation*/ ) diff --git a/test/IPNFT.t.sol b/test/IPNFT.t.sol index 85607c27..19facf23 100644 --- a/test/IPNFT.t.sol +++ b/test/IPNFT.t.sol @@ -229,6 +229,11 @@ contract IPNFTTest is IPNFTMintHelper { (uint8 v, bytes32 r, bytes32 s) = vm.sign(deployerPk, authMessageHash); bytes memory authorization = abi.encodePacked(r, s, v); + //the signoff only allows alice to call this + vm.startPrank(charlie); + vm.expectRevert(IPNFT.Unauthorized.selector); + ipnft.amendMetadata(1, "ipfs://QmNewUri", authorization); + vm.startPrank(alice); vm.expectEmit(true, true, false, false); emit MetadataUpdate(1);