diff --git a/contracts/OwnerData.sol b/contracts/OwnerData.sol index bcddf3b..c33077d 100644 --- a/contracts/OwnerData.sol +++ b/contracts/OwnerData.sol @@ -1,77 +1,94 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; -contract FFV4 { - function ownerOf(uint256 tokenId) public view returns (address) {} +contract FF { + function ownerOf(uint256 tokenId) public view returns (address) {} } -contract OwnerData is Context { - struct Data { - address owner; // Address of the owner who submitted the data - bytes dataHash; // Hash of the actual data stored off-chain (e.g., IPFS CID) - string metadata; // Any additional metadata associated with the data - } - - // Mapping to store contract => tokenID => data[] - mapping(address => mapping(uint256 => Data[])) private tokenData; - - constructor() {} +string constant SIGNED_MESSAGE = "Feral File is requesting authorization to write your sound piece to contract"; - function add(address contractAddress, uint256 tokenID, Data calldata data) external { - // check ownership - address tokenOwner = FFV4(contractAddress).ownerOf(tokenID); +contract OwnerData is Context { + struct Data { + address owner; + bytes dataHash; + string metadata; + } - require(data.owner == _msgSender() && tokenOwner == _msgSender(), "OwnerData: owner mismatch"); + mapping(address => mapping(uint256 => Data[])) private _tokenData; - bool exists = false; - - for (uint i = 0; i < tokenData[contractAddress][tokenID].length; i++) { - if (tokenData[contractAddress][tokenID][i].owner == data.owner) { - // update existing data - tokenData[contractAddress][tokenID][i] = data; - exists = true; - break; - } - } + function add(address contractAddress, uint256 tokenID, Data calldata data) external { + require(_isOwner(contractAddress, tokenID, _msgSender()), "OwnerData: caller is not the owner"); + require(data.owner == _msgSender(), "OwnerData: data owner mismatch"); + _updateData(contractAddress, tokenID, data); + } - if (!exists) { - tokenData[contractAddress][tokenID].push(data); + function signedAdd( + address contractAddress, + uint256 tokenID, + bytes memory signature, + Data calldata data + ) external { + address signer = _verifySignature(signature); + require(_isOwner(contractAddress, tokenID, signer), "OwnerData: signer is not the owner"); + require(data.owner == signer, "OwnerData: data owner mismatch"); + _updateData(contractAddress, tokenID, data); } - - emit DataAdded(contractAddress, tokenID, data); - } - function remove(address contractAddress, uint256 tokenID) external { - // check ownership - address tokenOwner = FFV4(contractAddress).ownerOf(tokenID); + function remove(address contractAddress, uint256 tokenID) external { + require(_isOwner(contractAddress, tokenID, _msgSender()), "OwnerData: caller is not the owner"); + _removeData(contractAddress, tokenID); + } - require(tokenOwner == _msgSender(), "OwnerData: owner mismatch"); + function get(address contractAddress, uint256 tokenID) external view returns (Data[] memory) { + return _tokenData[contractAddress][tokenID]; + } - uint index; - for (uint i = 0; i < tokenData[contractAddress][tokenID].length; i++) { - if (tokenData[contractAddress][tokenID][i].owner == _msgSender()) { - index = i; - break; - } + function _updateData( + address contractAddress, + uint256 tokenID, + Data calldata data + ) private { + Data[] storage datas = _tokenData[contractAddress][tokenID]; + bool exists = false; + for (uint256 i = 0; i < datas.length; ++i) { + if (datas[i].owner == data.owner) { + datas[i] = data; + exists = true; + break; + } + } + if (!exists) { + datas.push(data); + } + emit DataAdded(contractAddress, tokenID, data); } - require(index >= 0 && index < tokenData[contractAddress][tokenID].length, "OwnerData: data not found"); + function _removeData(address contractAddress, uint256 tokenID) private { + Data[] storage datas = _tokenData[contractAddress][tokenID]; + for (uint256 i = 0; i < datas.length; ++i) { + if (datas[i].owner == _msgSender()) { + datas[i] = datas[datas.length - 1]; + datas.pop(); + emit DataRemoved(contractAddress, tokenID); + return; + } + } + revert("OwnerData: data not found"); + } - // remove data from array - for (uint j = index; j < tokenData[contractAddress][tokenID].length - 1; j++) { - tokenData[contractAddress][tokenID][j] = tokenData[contractAddress][tokenID][j + 1]; + function _verifySignature(bytes memory signature) private view returns (address) { + bytes memory message = abi.encodePacked(SIGNED_MESSAGE, " ", Strings.toHexString(address(this)), "."); + return ECDSA.recover(ECDSA.toEthSignedMessageHash(message), signature); } - tokenData[contractAddress][tokenID].pop(); - - // emit event - emit DataRemoved(contractAddress, tokenID); - } - function get(address contractAddress, uint256 tokenID) external view returns (Data[] memory) { - return tokenData[contractAddress][tokenID]; - } + function _isOwner(address contractAddress, uint256 tokenID, address owner) private view returns (bool) { + return FF(contractAddress).ownerOf(tokenID) == owner; + } - event DataAdded(address contractAddress, uint256 tokenID, Data data); - event DataRemoved(address contractAddress, uint256 tokenID); + event DataAdded(address indexed contractAddress, uint256 indexed tokenID, Data data); + event DataRemoved(address indexed contractAddress, uint256 indexed tokenID); } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ea91ed9..6229cf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@openzeppelin/contracts": "4.9.0", "@truffle/hdwallet-provider": "^2.0.3", - "axios": "^0.27.2" + "axios": "^0.27.2", + "ethereumjs-util": "^7.1.5" }, "devDependencies": { "@openzeppelin/test-helpers": "^0.5.16", diff --git a/package.json b/package.json index 1998cef..b94e6ee 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "dependencies": { "@openzeppelin/contracts": "4.9.0", "@truffle/hdwallet-provider": "^2.0.3", - "axios": "^0.27.2" + "axios": "^0.27.2", + "ethereumjs-util": "^7.1.5" }, "devDependencies": { "@openzeppelin/test-helpers": "^0.5.16", diff --git a/test/owner_data.js b/test/owner_data.js index ce9d4dc..ab74995 100644 --- a/test/owner_data.js +++ b/test/owner_data.js @@ -4,6 +4,8 @@ const FeralfileVault = artifacts.require("FeralfileVault"); const CONTRACT_URI = "ipfs://QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; +const { bufferToHex } = require("ethereumjs-util"); + const bytesToString = (bytes) => { return web3.utils.toAscii(bytes).replace(/\u0000/g, ""); }; @@ -33,6 +35,7 @@ contract("OwnerData", async (accounts) => { [1, 4, accounts[0]], [1, 5, accounts[1]], [1, 6, accounts[2]], + [1, 7, "0x23221e5403511CeC833294D2B1B006e9D639A61b"], ]); }); @@ -252,7 +255,7 @@ contract("OwnerData", async (accounts) => { [accounts[1], cidBytes, "{duration: 1000}"] ); } catch (error) { - assert.equal(error.reason, "OwnerData: owner mismatch"); + assert.equal(error.reason, "OwnerData: data owner mismatch"); } }); @@ -273,4 +276,28 @@ contract("OwnerData", async (accounts) => { ); } }); + + it("test adding with signed add function", async function () { + const cid = "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; + const cidBytes = web3.utils.fromAscii(cid); + const data = [ + "0x23221e5403511CeC833294D2B1B006e9D639A61b", + cidBytes, + "{duration: 1000}", + ]; + + const msg = `Feral File is requesting authorization to write your sound piece to contract ${this.ownerDataContract.address.toLowerCase()}.`; + const msgHash = bufferToHex(Buffer.from(msg, "utf-8")); + const privateKey = + "0x5cd8bcda59dd3a9988bd20bdbdea7225a4a57949d12b9a527caf3ff819941d7f"; + const { signature } = await web3.eth.accounts.sign(msgHash, privateKey); + + const tx = await this.ownerDataContract.signedAdd( + this.exhibitionContract.address, + 1, + signature, + data + ); + assert.equal(tx.logs[0].event, "DataAdded"); + }); });