From 7423e07a77e0c923e906ebff0841608ecf543472 Mon Sep 17 00:00:00 2001 From: Tien Ngo Date: Mon, 26 Feb 2024 15:05:56 +0700 Subject: [PATCH] added signature from server side for signedAdd --- contracts/OwnerData.sol | 57 +++++++++++++++------- migrations/252_owner_data.js | 7 ++- test/owner_data.js | 93 +++++++++++++++++++++--------------- 3 files changed, 100 insertions(+), 57 deletions(-) diff --git a/contracts/OwnerData.sol b/contracts/OwnerData.sol index d5c926f..81907ed 100644 --- a/contracts/OwnerData.sol +++ b/contracts/OwnerData.sol @@ -5,8 +5,8 @@ import "@openzeppelin/contracts/utils/Context.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; -contract FFV5 { - function balanceOf(address account, uint256 id) public view returns (uint256) {} +contract IFF { + function ownerOf(uint256 tokenId) public view returns (address) {} } string constant SIGNED_MESSAGE = "Authorize to write your data to the contract"; @@ -17,12 +17,26 @@ contract OwnerData is Context { bytes dataHash; string metadata; } + struct Signature { + bytes ownerSign; + uint256 expiryBlock; + bytes32 r; + bytes32 s; + uint8 v; + } + + address private _trustee; // contractAddress => tokenID => Data[] mapping(address => mapping(uint256 => Data[])) private _tokenData; // contractAddress => tokenID => owner => bool mapping(address => mapping(uint256 => mapping(address => bool))) private _tokenDataOwner; + 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); } @@ -30,11 +44,12 @@ contract OwnerData is Context { function signedAdd( address contractAddress, uint256 tokenID, - bytes memory signature, - Data calldata data + Data calldata data, + Signature calldata signature ) external { - address signer = _verifySignature(signature); - _addData(signer, contractAddress, tokenID, data); + _validateSignature(signature); + address account = _recoverOwnerSignature(signature.ownerSign); + _addData(account, contractAddress, tokenID, data); } @@ -48,12 +63,8 @@ contract OwnerData is Context { uint256 tokenID, Data calldata data ) private { - if (!_isOwner(contractAddress, tokenID, sender)) { - revert NotOwner(sender, contractAddress, tokenID); - } - if (data.owner != sender) { - revert OwnerMismatch(data.owner, sender); - } + 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"); @@ -63,17 +74,29 @@ contract OwnerData is Context { emit DataAdded(contractAddress, tokenID, data); } - function _verifySignature(bytes memory signature) private view returns (address) { + function _validateSignature(Signature calldata signature) private view { + require(signature.expiryBlock >= block.number, "OwnerData: signature expired"); + + bytes32 message = keccak256( + abi.encode(block.chainid, address(this), signature.ownerSign, signature.expiryBlock) + ); + address reqSigner = ECDSA.recover( + ECDSA.toEthSignedMessageHash(message), + signature.v, + signature.r, + signature.s + ); + require(reqSigner == _trustee, "OwnerData: invalid signature"); + } + + 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); } function _isOwner(address contractAddress, uint256 tokenID, address account) private view returns (bool) { - return FFV5(contractAddress).balanceOf(account, tokenID) > 0; + return IFF(contractAddress).ownerOf(tokenID) == account; } event DataAdded(address indexed contractAddress, uint256 indexed tokenID, Data data); - - error NotOwner(address caller, address contractAddress, uint256 tokenID); - error OwnerMismatch(address dataOwner, address caller); } \ No newline at end of file diff --git a/migrations/252_owner_data.js b/migrations/252_owner_data.js index 371a6da..f618480 100644 --- a/migrations/252_owner_data.js +++ b/migrations/252_owner_data.js @@ -1,5 +1,10 @@ var OwnerData = artifacts.require("OwnerData"); +const argv = require("minimist")(process.argv.slice(2), { + string: ["trustee"], +}); module.exports = function (deployer) { - deployer.deploy(OwnerData); + const trustee = + argv.trustee || "0xdB33365a8730de2F7574ff1189fB9D337bF4c36d"; + deployer.deploy(OwnerData, trustee); }; diff --git a/test/owner_data.js b/test/owner_data.js index c300914..9b257ec 100644 --- a/test/owner_data.js +++ b/test/owner_data.js @@ -1,9 +1,8 @@ const OwnerData = artifacts.require("OwnerData"); -const FeralfileExhibitionV5 = artifacts.require("FeralfileExhibitionV5"); +const FeralfileExhibitionV4 = artifacts.require("FeralfileExhibitionV4"); const FeralfileVault = artifacts.require("FeralfileVault"); const CONTRACT_URI = "ipfs://QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; -const TOKEN_BASE_URI = "ipfs://QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"; const COST_RECEIVER = "0x46f2B641d8702f29c45f6D06292dC34Eb9dB1801"; const { bufferToHex } = require("ethereumjs-util"); @@ -16,31 +15,33 @@ contract("OwnerData", async (accounts) => { before(async function () { this.signer = accounts[0]; this.trustee = accounts[1]; + this.signTrustee = accounts[5]; this.vault = await FeralfileVault.new(this.signer); - this.ownerDataContract = await OwnerData.new(); + this.ownerDataContract = await OwnerData.new(this.signTrustee); this.seriesIds = [1, 2]; this.seriesMaxSupply = [100, 1]; this.seriesArtworkMaxSupply = [1, 100]; - this.exhibitionContract = await FeralfileExhibitionV5.new( - TOKEN_BASE_URI, - CONTRACT_URI, + this.exhibitionContract = await FeralfileExhibitionV4.new( + "Feral File V4 Test", + "FFv4", + true, + true, this.signer, this.vault.address, COST_RECEIVER, - true, + CONTRACT_URI, this.seriesIds, - this.seriesMaxSupply, - this.seriesArtworkMaxSupply + this.seriesMaxSupply ); await this.exhibitionContract.mintArtworks([ - [1, 1, accounts[0], 1], - [1, 2, accounts[1], 1], - [1, 3, accounts[2], 1], - [1, 4, accounts[0], 1], - [1, 5, accounts[1], 1], - [1, 6, accounts[2], 1], - [1, 7, "0x23221e5403511CeC833294D2B1B006e9D639A61b", 1], + [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"], ]); await this.exhibitionContract.addTrustee(this.trustee); }); @@ -114,19 +115,14 @@ contract("OwnerData", async (accounts) => { assert.equal(bytesToString(tx1.logs[0].args.data.dataHash), cid1); // Transfer to account 3 - await this.exhibitionContract.safeTransferFrom( + await this.exhibitionContract.transferFrom( accounts[1], accounts[2], 2, - 1, - web3.utils.fromAscii(""), { from: accounts[1] } ); - const acc2Balance = await this.exhibitionContract.balanceOf( - accounts[2], - 2 - ); - assert.equal(acc2Balance, 1); + const acc2Owner = await this.exhibitionContract.ownerOf(2); + assert.equal(acc2Owner, accounts[2]); const tx2 = await this.ownerDataContract.add( this.exhibitionContract.address, @@ -137,19 +133,14 @@ contract("OwnerData", async (accounts) => { assert.equal(bytesToString(tx2.logs[0].args.data.dataHash), cid2); // Transfer to account 4 - await this.exhibitionContract.safeTransferFrom( + await this.exhibitionContract.transferFrom( accounts[2], accounts[4], 2, - 1, - web3.utils.fromAscii(""), { from: accounts[2] } ); - const acc4Balance = await this.exhibitionContract.balanceOf( - accounts[4], - 2 - ); - assert.equal(acc4Balance, 1); + const acc4Owner = await this.exhibitionContract.ownerOf(2); + assert.equal(acc4Owner, accounts[4]); const tx3 = await this.ownerDataContract.add( this.exhibitionContract.address, @@ -185,11 +176,7 @@ contract("OwnerData", async (accounts) => { [accounts[1], cidBytes, "{duration: 1000}"] ); } catch (error) { - assert.ok( - error.message.includes( - "VM Exception while processing transaction: revert" - ) - ); + assert.equal(error.reason, "OwnerData: data owner mismatch"); } }); @@ -226,11 +213,39 @@ contract("OwnerData", async (accounts) => { "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, 7, - signature, - data + data, + signs ); assert.equal(tx.logs[0].event, "DataAdded"); });