From 0d23a3f3dd3808d5eda941f8eff1d8e63cdcf955 Mon Sep 17 00:00:00 2001 From: Tien Ngo Date: Thu, 29 Feb 2024 14:52:21 +0700 Subject: [PATCH] Updated contract support public token and erc721 and erc1155 interfaces --- contracts/OwnerData.sol | 127 ++++++++++++++--------- test/owner_data.js | 216 ++++++++++++++++++++++++++++++++-------- 2 files changed, 254 insertions(+), 89 deletions(-) diff --git a/contracts/OwnerData.sol b/contracts/OwnerData.sol index 81907ed..79b5904 100644 --- a/contracts/OwnerData.sol +++ b/contracts/OwnerData.sol @@ -2,21 +2,27 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +/** + * @title OwnerData + * @dev Manages addition data for ERC721 or ERC1155 token. + */ +contract OwnerData is Context, Ownable { + string private constant SIGNED_MESSAGE = "Authorize to write your data to the contract"; + address private immutable _trustee; -contract IFF { - function ownerOf(uint256 tokenId) public view returns (address) {} -} - -string constant SIGNED_MESSAGE = "Authorize to write your data to the contract"; - -contract OwnerData is Context { struct Data { address owner; bytes dataHash; string metadata; } + struct Signature { bytes ownerSign; uint256 expiryBlock; @@ -25,78 +31,109 @@ contract OwnerData is Context { uint8 v; } - address private _trustee; + struct SignedAddParams { + address contractAddress; + uint256 tokenID; + Data data; + Signature signature; + } // contractAddress => tokenID => Data[] mapping(address => mapping(uint256 => Data[])) private _tokenData; // contractAddress => tokenID => owner => bool mapping(address => mapping(uint256 => mapping(address => bool))) private _tokenDataOwner; + // contractAddress => tokenID => bool + mapping(address => mapping(uint256 => bool)) private _publicTokens; + + event DataAdded(address indexed contractAddress, uint256 indexed tokenID, Data data); constructor(address trustee_) { require(trustee_ != address(0), "OwnerData: Trustee is the zero address"); _trustee = trustee_; } - function add(address contractAddress, uint256 tokenID, Data calldata data) external { - _addData(_msgSender(), contractAddress, tokenID, data); + function add(address contractAddress_, uint256 tokenID_, Data calldata data_) external payable { + require(!_publicTokens[contractAddress_][tokenID_] || msg.value > 0, "OwnerData: Payment required for public token"); + _addData(_msgSender(), contractAddress_, tokenID_, data_); + if (msg.value > 0) { + payable(owner()).transfer(msg.value); + } + } + + function get(address contractAddress_, uint256 tokenID_) external view returns (Data[] memory) { + return _tokenData[contractAddress_][tokenID_]; + } + + function setPublicTokens(address[] memory contractAddresses_, uint256[] memory tokenIDs_, bool isPublic_) external onlyOwner { + require(contractAddresses_.length == tokenIDs_.length, "OwnerData: Arrays length mismatch"); + for (uint256 i = 0; i < contractAddresses_.length; i++) { + _publicTokens[contractAddresses_[i]][tokenIDs_[i]] = isPublic_; + } } - function signedAdd( - address contractAddress, - uint256 tokenID, - Data calldata data, - Signature calldata signature - ) external { - _validateSignature(signature); - address account = _recoverOwnerSignature(signature.ownerSign); - _addData(account, contractAddress, tokenID, data); + function signedAdd(SignedAddParams[] calldata params_) external { + for (uint256 i = 0; i < params_.length; i++) { + _signedAdd(params_[i]); + } } - function get(address contractAddress, uint256 tokenID) external view returns (Data[] memory) { - return _tokenData[contractAddress][tokenID]; + function _signedAdd(SignedAddParams calldata params_) private { + _validateSignature(params_.signature); + if (_publicTokens[params_.contractAddress][params_.tokenID]) { + _addData(params_.data.owner, params_.contractAddress, params_.tokenID, params_.data); + } else { + address account = _recoverOwnerSignature(params_.signature.ownerSign); + _addData(account, params_.contractAddress, params_.tokenID, params_.data); + } } function _addData( - address sender, - address contractAddress, - uint256 tokenID, - Data calldata data + address sender_, + address contractAddress_, + uint256 tokenID_, + Data calldata data_ ) private { - require(_isOwner(contractAddress, tokenID, sender), "OwnerData: sender is not the owner"); - require(data.owner == sender, "OwnerData: data owner mismatch"); - require(data.dataHash.length > 0, "OwnerData: dataHash is empty"); - require(!_tokenDataOwner[contractAddress][tokenID][data.owner], "OwnerData: data already added"); + require(data_.owner == sender_, "OwnerData: data owner and sender mismatch"); + require(data_.dataHash.length > 0, "OwnerData: dataHash is empty"); - _tokenData[contractAddress][tokenID].push(data); - _tokenDataOwner[contractAddress][tokenID][data.owner] = true; + if (!_publicTokens[contractAddress_][tokenID_]) { + require(_isOwner(contractAddress_, tokenID_, data_.owner), "OwnerData: sender is not the owner"); + require(!_tokenDataOwner[contractAddress_][tokenID_][data_.owner], "OwnerData: data already added"); + _tokenDataOwner[contractAddress_][tokenID_][data_.owner] = true; + } - emit DataAdded(contractAddress, tokenID, data); - } + _tokenData[contractAddress_][tokenID_].push(data_); - function _validateSignature(Signature calldata signature) private view { - require(signature.expiryBlock >= block.number, "OwnerData: signature expired"); + emit DataAdded(contractAddress_, tokenID_, data_); + } + function _validateSignature(Signature calldata signature_) private view { + require(block.number < signature_.expiryBlock, "OwnerData: signature expired"); bytes32 message = keccak256( - abi.encode(block.chainid, address(this), signature.ownerSign, signature.expiryBlock) + abi.encode(block.chainid, address(this), signature_.ownerSign, signature_.expiryBlock) ); address reqSigner = ECDSA.recover( ECDSA.toEthSignedMessageHash(message), - signature.v, - signature.r, - signature.s + signature_.v, + signature_.r, + signature_.s ); - require(reqSigner == _trustee, "OwnerData: invalid signature"); + require(reqSigner == _trustee, "OwnerData: Invalid signature"); } - function _recoverOwnerSignature(bytes memory signature) private view returns (address) { + function _recoverOwnerSignature(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); + return ECDSA.recover(ECDSA.toEthSignedMessageHash(message), signature_); } function _isOwner(address contractAddress, uint256 tokenID, address account) private view returns (bool) { - return IFF(contractAddress).ownerOf(tokenID) == account; + if (IERC165(contractAddress).supportsInterface(type(IERC1155).interfaceId)) { + return IERC1155(contractAddress).balanceOf(account, tokenID) > 0; + } + if (IERC165(contractAddress).supportsInterface(type(IERC721).interfaceId)) { + return IERC721(contractAddress).ownerOf(tokenID) == account; + } + return false; } - - event DataAdded(address indexed contractAddress, uint256 indexed tokenID, Data data); } \ No newline at end of file diff --git a/test/owner_data.js b/test/owner_data.js index 9b257ec..5f1e4ba 100644 --- a/test/owner_data.js +++ b/test/owner_data.js @@ -1,9 +1,13 @@ const OwnerData = artifacts.require("OwnerData"); const FeralfileExhibitionV4 = artifacts.require("FeralfileExhibitionV4"); +const FeralFileAirdropV1 = artifacts.require("FeralFileAirdropV1"); const FeralfileVault = artifacts.require("FeralfileVault"); const CONTRACT_URI = "ipfs://QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; const COST_RECEIVER = "0x46f2B641d8702f29c45f6D06292dC34Eb9dB1801"; +const TOKEN_URI = + "https://ipfs.bitmark.com/ipfs/QmNVdQSp1AvZonLwHzTbbZDPLgbpty15RMQrbPEWd4ooTU/{id}"; +const TOKEN_TYPE_FUNGIBLE = 0; const { bufferToHex } = require("ethereumjs-util"); @@ -31,19 +35,40 @@ contract("OwnerData", async (accounts) => { COST_RECEIVER, CONTRACT_URI, this.seriesIds, - this.seriesMaxSupply + this.seriesMaxSupply, ); await this.exhibitionContract.mintArtworks([ - [1, 1, accounts[0]], - [1, 2, accounts[1]], - [1, 3, accounts[2]], - [1, 4, accounts[0]], - [1, 5, accounts[1]], - [1, 6, accounts[2]], - [1, 7, "0x23221e5403511CeC833294D2B1B006e9D639A61b"], + [this.seriesIds[0], 100001, accounts[0]], + [this.seriesIds[0], 100002, accounts[1]], + [this.seriesIds[0], 100003, accounts[2]], + [this.seriesIds[0], 100004, accounts[0]], + [this.seriesIds[0], 100005, accounts[1]], + [this.seriesIds[0], 100006, accounts[2]], + [ + this.seriesIds[0], + 100007, + "0x23221e5403511CeC833294D2B1B006e9D639A61b", + ], + [this.seriesIds[1], 200001, accounts[0]], ]); + await this.exhibitionContract.addTrustee(this.trustee); + await this.ownerDataContract.setPublicTokens( + [this.exhibitionContract.address], + [200001], + true, + ); + + this.fungibleContract = await FeralFileAirdropV1.new( + TOKEN_TYPE_FUNGIBLE, + TOKEN_URI, + CONTRACT_URI, + true, + true, + ); + await this.fungibleContract.mint(999, 10); + await this.fungibleContract.airdrop(999, [accounts[0], accounts[1]]); }); it("test adding data successfully", async function () { @@ -51,8 +76,9 @@ contract("OwnerData", async (accounts) => { const cidBytes = web3.utils.fromAscii(cid); const tx = await this.ownerDataContract.add( this.exhibitionContract.address, - 1, - [accounts[0], cidBytes, "{duration: 1000}"] + 100001, + [accounts[0], cidBytes, "{duration: 1000}"], + { from: accounts[0], value: 0 }, ); const { logs } = tx; assert.equal(logs[0].event, "DataAdded"); @@ -63,12 +89,12 @@ contract("OwnerData", async (accounts) => { it("test getting data", async function () { const data = await this.ownerDataContract.get( this.exhibitionContract.address, - 1 + 100001, ); assert.equal(data[0].owner, accounts[0]); assert.equal( bytesToString(data[0].dataHash), - "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc" + "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc", ); assert.equal(data[0].metadata, "{duration: 1000}"); }); @@ -78,9 +104,9 @@ contract("OwnerData", async (accounts) => { const cidBytes = web3.utils.fromAscii(cid); const tx = await this.ownerDataContract.add( this.exhibitionContract.address, - 3, + 100003, [accounts[2], cidBytes, "{duration: 1000}"], - { from: accounts[2] } + { from: accounts[2], value: 0 }, ); assert.equal(bytesToString(tx.logs[0].args.data.dataHash), cid); @@ -89,9 +115,9 @@ contract("OwnerData", async (accounts) => { try { await this.ownerDataContract.add( this.exhibitionContract.address, - 3, + 100003, [accounts[2], updatedCidBytes, "{duration: 2000}"], - { from: accounts[2] } + { from: accounts[2], value: 0 }, ); } catch (error) { assert.equal(error.reason, "OwnerData: data already added"); @@ -108,9 +134,9 @@ contract("OwnerData", async (accounts) => { const tx1 = await this.ownerDataContract.add( this.exhibitionContract.address, - 2, + 100002, [accounts[1], cidBytes1, "{duration: 1000}"], - { from: accounts[1] } + { from: accounts[1], value: 0 }, ); assert.equal(bytesToString(tx1.logs[0].args.data.dataHash), cid1); @@ -118,17 +144,17 @@ contract("OwnerData", async (accounts) => { await this.exhibitionContract.transferFrom( accounts[1], accounts[2], - 2, - { from: accounts[1] } + 100002, + { from: accounts[1] }, ); - const acc2Owner = await this.exhibitionContract.ownerOf(2); + const acc2Owner = await this.exhibitionContract.ownerOf(100002); assert.equal(acc2Owner, accounts[2]); const tx2 = await this.ownerDataContract.add( this.exhibitionContract.address, - 2, + 100002, [accounts[2], cidBytes2, "{duration: 2000}"], - { from: accounts[2] } + { from: accounts[2], value: 0 }, ); assert.equal(bytesToString(tx2.logs[0].args.data.dataHash), cid2); @@ -136,23 +162,23 @@ contract("OwnerData", async (accounts) => { await this.exhibitionContract.transferFrom( accounts[2], accounts[4], - 2, - { from: accounts[2] } + 100002, + { from: accounts[2] }, ); - const acc4Owner = await this.exhibitionContract.ownerOf(2); + const acc4Owner = await this.exhibitionContract.ownerOf(100002); assert.equal(acc4Owner, accounts[4]); const tx3 = await this.ownerDataContract.add( this.exhibitionContract.address, - 2, + 100002, [accounts[4], cidBytes3, "{duration: 3000}"], - { from: accounts[4] } + { from: accounts[4], value: 0 }, ); assert.equal(bytesToString(tx3.logs[0].args.data.dataHash), cid3); const data = await this.ownerDataContract.get( this.exhibitionContract.address, - 2 + 100002, ); assert.equal(data.length, 3); @@ -172,11 +198,15 @@ contract("OwnerData", async (accounts) => { try { await this.ownerDataContract.add( this.exhibitionContract.address, - 1, - [accounts[1], cidBytes, "{duration: 1000}"] + 100001, + [accounts[1], cidBytes, "{duration: 1000}"], + { from: accounts[0], value: 0 }, ); } catch (error) { - assert.equal(error.reason, "OwnerData: data owner mismatch"); + assert.equal( + error.reason, + "OwnerData: data owner and sender mismatch", + ); } }); @@ -184,16 +214,17 @@ contract("OwnerData", async (accounts) => { const cid = "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; const cidBytes = web3.utils.fromAscii(cid); try { - await this.ownerDataContract.add(accounts[1], 1, [ - accounts[0], - cidBytes, - "{duration: 1000}", - ]); + await this.ownerDataContract.add( + accounts[1], + 100001, + [accounts[0], cidBytes, "{duration: 1000}"], + { from: accounts[0], value: 0 }, + ); } catch (error) { assert.ok( error.message.includes( - "VM Exception while processing transaction: revert" - ) + "VM Exception while processing transaction: revert", + ), ); } }); @@ -222,7 +253,7 @@ contract("OwnerData", async (accounts) => { this.ownerDataContract.address, signature, expiryTime, - ] + ], ); const hash = web3.utils.keccak256(signedParams); @@ -241,12 +272,109 @@ contract("OwnerData", async (accounts) => { web3.utils.toDecimal(v) + 27, ]; - const tx = await this.ownerDataContract.signedAdd( + const tx = await this.ownerDataContract.signedAdd([ + [this.exhibitionContract.address, 100007, data, signs], + ]); + assert.equal(tx.logs[0].event, "DataAdded"); + }); + + it("test adding data for public token", async function () { + const cid = "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; + const cidBytes = web3.utils.fromAscii(cid); + + const tx1 = await this.ownerDataContract.add( this.exhibitionContract.address, - 7, - data, - signs + 200001, + [accounts[0], cidBytes, "{duration: 1000}"], + { from: accounts[0], value: web3.utils.toWei("0.001", "ether") }, ); + assert.equal(tx1.logs[0].event, "DataAdded"); + + const tx2 = await this.ownerDataContract.add( + this.exhibitionContract.address, + 200001, + [accounts[2], cidBytes, "{duration: 1000}"], + { from: accounts[2], value: web3.utils.toWei("0.02", "ether") }, + ); + assert.equal(tx2.logs[0].event, "DataAdded"); + }); + + it("test adding data for public token using signed add function", async function () { + const cid = "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; + const cidBytes = web3.utils.fromAscii(cid); + const data = [ + "0x23221e5403511CeC833294D2B1B006e9D639A61b", + cidBytes, + "{duration: 1000}", + ]; + + const msg = `Authorize to write your data to the 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 expiryTime = (new Date().getTime() / 1000 + 300).toFixed(0); + const chainId = await web3.eth.getChainId(); + const signedParams = web3.eth.abi.encodeParameters( + ["uint", "address", "bytes", "uint256"], + [ + BigInt(chainId).toString(), + this.ownerDataContract.address, + signature, + expiryTime, + ], + ); + + const hash = web3.utils.keccak256(signedParams); + const trusteeSignature = await web3.eth.sign(hash, accounts[5]); + const sig = trusteeSignature.substr(2); + const r = "0x" + sig.slice(0, 64); + const s = "0x" + sig.slice(64, 128); + const v = "0x" + sig.slice(128, 130); + + // sign params + const signs = [ + signature, + expiryTime, + r, + s, + web3.utils.toDecimal(v) + 27, + ]; + + const tx = await this.ownerDataContract.signedAdd([ + [this.exhibitionContract.address, 200001, data, signs], + ]); assert.equal(tx.logs[0].event, "DataAdded"); }); + + it("test add sound for ERC1155 token successfully", async function () { + const cid = "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; + const cidBytes = web3.utils.fromAscii(cid); + const tx = await this.ownerDataContract.add( + this.fungibleContract.address, + 999, + [accounts[0], cidBytes, "{duration: 1000}"], + { from: accounts[0], value: 0 }, + ); + assert.equal(tx.logs[0].event, "DataAdded"); + }); + + it("test add sound for ERC1155 token failed because wrong token owner", async function () { + const cid = "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; + const cidBytes = web3.utils.fromAscii(cid); + try { + const tx = await this.ownerDataContract.add( + this.fungibleContract.address, + 999, + [accounts[0], cidBytes, "{duration: 1000}"], + { from: accounts[2], value: 0 }, + ); + } catch (error) { + assert.equal( + error.reason, + "OwnerData: data owner and sender mismatch", + ); + } + }); });