diff --git a/contracts/OBSSStorage.sol b/contracts/OBSSStorage.sol index 2e84be8..7e841b1 100644 --- a/contracts/OBSSStorage.sol +++ b/contracts/OBSSStorage.sol @@ -4,15 +4,20 @@ pragma solidity ^0.8.17; import "@openzeppelin/contracts/utils/Counters.sol"; import "@opengsn/contracts/src/ERC2771Recipient.sol"; import "@big-whale-labs/ketl-allow-map-contract/contracts/KetlAllowMap.sol"; -import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "hardhat/console.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /** * @title OBSSStorage * @dev This contract is used to store the data of the OBSS contract */ -contract OBSSStorage is Initializable, Context, ERC2771Recipient { +contract OBSSStorage is + Initializable, + ContextUpgradeable, + OwnableUpgradeable, + ERC2771Recipient +{ using Counters for Counters.Counter; /* State */ @@ -33,9 +38,10 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { mapping(address => Counters.Counter) public lastProfilePostIds; mapping(address => CID) public subscriptions; // Reactions - mapping(bytes32 => mapping(uint256 => Reaction)) public reactions; - mapping(bytes32 => Counters.Counter) public lastReactionIds; - mapping(bytes32 => mapping(address => uint256)) public reactionsUserToId; + mapping(uint256 => mapping(uint256 => Reaction)) public reactions; + mapping(uint256 => Counters.Counter) public lastReactionIds; + mapping(uint256 => mapping(address => uint256)) public reactionsUserToId; + bool public isDataMigrationLocked; // IPFS cid represented in a more efficient way struct CID { @@ -52,6 +58,7 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { } // 1 = upvote, 2 = downvote struct Reaction { + uint256 postId; uint8 reactionType; uint256 value; address reactionOwner; @@ -72,6 +79,14 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { CID postMetadata; } + struct LegacyPost { + Post post; + uint256 feedId; + } + struct LegacyReaction { + Reaction reaction; + } + /* Events */ // Feeds event FeedAdded(uint256 indexed id, CID metadata); @@ -112,6 +127,11 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { _; } + modifier onlyIfLoadingAllowed() { + require(!isDataMigrationLocked, "All legacy data already loaded"); + _; + } + // Constructor function initialize( address _forwarder, @@ -123,6 +143,9 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { founderAllowMap = KetlAllowMap(_founderAllowMap); _setTrustedForwarder(_forwarder); version = _version; + // Set owner + __Ownable_init(); + isDataMigrationLocked = false; } function addAddressToVCAllowMap( @@ -262,27 +285,28 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { if (post.author == address(0)) { revert("Post not found"); } - uint256 oldReactionId = reactionsUserToId[post.metadata.digest][ + uint256 oldReactionId = reactionsUserToId[reactionRequest.postId][ _msgSender() ]; if ( - reactions[post.metadata.digest][oldReactionId].reactionType == + reactions[reactionRequest.postId][oldReactionId].reactionType == reactionRequest.reactionType ) revert("Reaction already added"); if (oldReactionId > 0) { - delete reactions[post.metadata.digest][oldReactionId]; - delete reactionsUserToId[post.metadata.digest][_msgSender()]; + delete reactions[reactionRequest.postId][oldReactionId]; + delete reactionsUserToId[reactionRequest.postId][_msgSender()]; emit ReactionRemoved(_msgSender(), reactionRequest.postId, oldReactionId); } Reaction memory reaction = Reaction( + reactionRequest.postId, reactionRequest.reactionType, msg.value, _msgSender() ); - lastReactionIds[post.metadata.digest].increment(); - uint256 reactionId = lastReactionIds[post.metadata.digest].current(); - reactions[post.metadata.digest][reactionId] = reaction; - reactionsUserToId[post.metadata.digest][_msgSender()] = reactionId; + lastReactionIds[reactionRequest.postId].increment(); + uint256 reactionId = lastReactionIds[reactionRequest.postId].current(); + reactions[reactionRequest.postId][reactionId] = reaction; + reactionsUserToId[reactionRequest.postId][_msgSender()] = reactionId; if (msg.value > 0) { payable(post.author).transfer(msg.value); } @@ -328,12 +352,13 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { } if ( _msgSender() != - reactions[post.metadata.digest][reactionRequest.reactionId].reactionOwner + reactions[reactionRequest.postId][reactionRequest.reactionId] + .reactionOwner ) { revert("You are not the reaction owner"); } - delete reactions[post.metadata.digest][reactionRequest.reactionId]; - delete reactionsUserToId[post.metadata.digest][_msgSender()]; + delete reactions[reactionRequest.postId][reactionRequest.reactionId]; + delete reactionsUserToId[reactionRequest.postId][_msgSender()]; emit ReactionRemoved( _msgSender(), reactionRequest.postId, @@ -371,6 +396,75 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { removeBatchReactions(batchReactionsToRemove); } + function migrateLegacyData( + LegacyPost[] memory legacyPosts, + LegacyReaction[] memory legacyReactions + ) external onlyOwner onlyIfLoadingAllowed { + _addFeedLegacyPostsBatch(legacyPosts); + _addFeedLegacyReactionsBatch(legacyReactions); + } + + function _addFeedLegacyPostsBatch(LegacyPost[] memory legacyPosts) private { + uint256 length = legacyPosts.length; + for (uint8 i = 0; i < length; ) { + LegacyPost memory legacyPost = legacyPosts[i]; + uint256 commentsFeedId = addFeed(legacyPost.post.metadata); + Post memory post = Post( + legacyPost.post.author, + legacyPost.post.metadata, + commentsFeedId, + legacyPost.post.timestamp + ); + uint256 objectId = lastFeedPostIds[legacyPost.feedId].current(); + posts[commentsFeedId] = post; + feedPosts[legacyPost.feedId].push(commentsFeedId); + emit FeedPostAdded(legacyPost.feedId, objectId, post); + lastFeedPostIds[legacyPost.feedId].increment(); + unchecked { + ++i; + } + } + } + + function _addFeedLegacyReactionsBatch( + LegacyReaction[] memory legacyReactions + ) private { + uint256 length = legacyReactions.length; + for (uint8 i = 0; i < length; ) { + LegacyReaction memory legacyReaction = legacyReactions[i]; + Reaction memory reaction = Reaction( + legacyReaction.reaction.postId, + legacyReaction.reaction.reactionType, + legacyReaction.reaction.value, + legacyReaction.reaction.reactionOwner + ); + lastReactionIds[legacyReaction.reaction.postId].increment(); + uint256 reactionId = lastReactionIds[legacyReaction.reaction.postId] + .current(); + reactions[legacyReaction.reaction.postId][reactionId] = reaction; + reactionsUserToId[legacyReaction.reaction.postId][ + legacyReaction.reaction.reactionOwner + ] = reactionId; + if (msg.value > 0) { + payable(legacyReaction.reaction.reactionOwner).transfer(msg.value); + } + emit ReactionAdded( + legacyReaction.reaction.reactionOwner, + legacyReaction.reaction.postId, + legacyReaction.reaction.reactionType, + reactionId, + 0 + ); + unchecked { + ++i; + } + } + } + + function lockDataMigration() external onlyOwner { + isDataMigrationLocked = true; + } + /** * @dev Get the feed posts */ @@ -425,12 +519,12 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { if (post.author == address(0)) { revert("Post not found"); } - uint256 reactionsLength = lastReactionIds[post.metadata.digest].current(); + uint256 reactionsLength = lastReactionIds[postId].current(); uint256 negativeReactions = 0; uint256 positiveReactions = 0; for (uint256 i = 1; i < reactionsLength + 1; ) { - Reaction memory currentReaction = reactions[post.metadata.digest][i]; + Reaction memory currentReaction = reactions[postId][i]; if (currentReaction.reactionType == 1) { positiveReactions += 1; } else if (currentReaction.reactionType == 2) { @@ -447,7 +541,7 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { function _msgSender() internal view - override(Context, ERC2771Recipient) + override(ContextUpgradeable, ERC2771Recipient) returns (address sender) { sender = ERC2771Recipient._msgSender(); @@ -456,7 +550,7 @@ contract OBSSStorage is Initializable, Context, ERC2771Recipient { function _msgData() internal view - override(Context, ERC2771Recipient) + override(ContextUpgradeable, ERC2771Recipient) returns (bytes calldata ret) { return ERC2771Recipient._msgData(); diff --git a/package.json b/package.json index d79fd3a..5eca2c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@big-whale-labs/obss-storage-contract", - "version": "0.1.3", + "version": "0.2.4-0", "description": "Storage contract for OBSS", "repository": { "type": "git", @@ -79,6 +79,7 @@ "dependencies": { "@big-whale-labs/constants": "^0.1.39", "@big-whale-labs/ketl-allow-map-contract": "^0.0.3", + "@big-whale-labs/obss-storage-contract": "^0.1.4", "@openzeppelin/contracts-upgradeable": "^4.8.2", "@openzeppelin/hardhat-upgrades": "^1.22.1", "@zk-kit/incremental-merkle-tree": "^1.0.0", diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 8cccfe5..90193ea 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -1,6 +1,9 @@ +import { BigNumber, utils } from 'ethers' import { GSN_MUMBAI_FORWARDER_CONTRACT_ADDRESS } from '@big-whale-labs/constants' +import { OBSSStorage__factory as LegacyOBSSStorage__factory } from '@big-whale-labs/obss-storage-contract' +import { OBSSStorage } from 'typechain' +import { Provider } from '@ethersproject/providers' import { ethers, run, upgrades } from 'hardhat' -import { utils } from 'ethers' import { version } from '../package.json' import prompt from 'prompt' @@ -8,6 +11,24 @@ const regexes = { ethereumAddress: /^0x[a-fA-F0-9]{40}$/, } +type LegacyData = + | OBSSStorage.LegacyPostStruct[] + | OBSSStorage.LegacyReactionStruct[] + +function getBatchOfData(data: LegacyData, start: number, end: number) { + return data.slice(start, end) +} + +function prepareAllBatches(data: LegacyData) { + const batchStep = 5 + const batches: LegacyData[] = [] + for (let i = 0; i < data.length; i += batchStep) { + const batch = getBatchOfData(data, i, i + batchStep) + batches.push(batch) + } + return batches +} + async function main() { const [deployer] = await ethers.getSigners() @@ -39,6 +60,7 @@ async function main() { }) const provider = ethers.provider + const { chainId } = await provider.getNetwork() const chains = { 1: 'mainnet', @@ -79,10 +101,55 @@ async function main() { ) const adminAddress = await upgrades.erc1967.getAdminAddress(contract.address) + const deployedContract = factory + .attach(contract.address) + .connect(provider) + .connect(deployer) + console.log('OBSSStorage Proxy address:', contract.address) console.log('Implementation address:', implementationAddress) console.log('Admin address:', adminAddress) + console.log('Migrating data...') + + const { legacyPosts, legacyReactions } = await downloadData(provider) + const legacyPostsBatches = prepareAllBatches(legacyPosts) + const legacyReactionsBatches = prepareAllBatches(legacyReactions) + + for (let i = 0; i < legacyPostsBatches.length; i++) { + console.log(`Loading data batch ${i} / ${legacyPostsBatches.length}`) + const tx = await deployedContract.migrateLegacyData( + legacyPostsBatches[i] as OBSSStorage.LegacyPostStruct[], + [] as OBSSStorage.LegacyReactionStruct[] + ) + const receipt = await tx.wait() + console.log( + `Batch ${i} loaded `, + `https://mumbai.polygonscan.com/tx/${receipt.transactionHash}` + ) + } + for (let i = 0; i < legacyReactionsBatches.length; i++) { + console.log(`Loading data batch ${i} / ${legacyReactionsBatches.length}`) + const tx = await deployedContract.migrateLegacyData( + [] as OBSSStorage.LegacyPostStruct[], + legacyReactionsBatches[i] as OBSSStorage.LegacyReactionStruct[] + ) + const receipt = await tx.wait() + console.log( + `Batch ${i} loaded `, + `https://mumbai.polygonscan.com/tx/${receipt.transactionHash}` + ) + } + console.log('Data migration done!') + + console.log('Locking data migration...') + + await deployedContract.lockDataMigration() + const isDataLoadingLocked = await deployedContract.isDataMigrationLocked() + if (isDataLoadingLocked) { + console.log('Data migration locked!') + } + console.log('Wait for 1 minute to make sure blockchain is updated') await new Promise((resolve) => setTimeout(resolve, 15 * 1000)) @@ -110,6 +177,149 @@ async function main() { ) } +const legacyContractAddress = '0x9e7A15E77e5E4f536b8215aaF778e786005D0f8d' + +async function downloadData(provider: Provider) { + const legacyContract = LegacyOBSSStorage__factory.connect( + legacyContractAddress, + provider + ) + + console.log(legacyContract.address) + const totalFeeds = await legacyContract.lastFeedId() + console.log(`Total feeds count: ${totalFeeds.toNumber()}`) + const legacyPosts: OBSSStorage.LegacyPostStruct[] = [] + const legacyReactions: OBSSStorage.LegacyReactionStruct[] = [] + const userToReaction = new Map< + string, + { + postId: BigNumber + reactionType: number + value: BigNumber + reactionOwner: string + } + >() + for (let i = 0; i < totalFeeds.toNumber(); i++) { + const postsInFeed = await legacyContract.lastFeedPostIds(i) + if (postsInFeed.toNumber() === 0) continue + + const feedPosts = await legacyContract.getFeedPosts( + i, + 0, + postsInFeed.toNumber() + ) + + const [first] = feedPosts + + if (first) { + const maxId = Math.max( + ...legacyPosts.map(({ post }) => Number(post.commentsFeedId)), + 0 + ) + for ( + let prevFeed = maxId; + prevFeed < first.commentsFeedId.toNumber(); + prevFeed += 1 + ) { + const metadata = await legacyContract.feeds(prevFeed) + legacyPosts.push({ + post: { + author: '0x0000000000000000000000000000000000000000', + metadata, + commentsFeedId: BigNumber.from(prevFeed), + timestamp: first.timestamp.toNumber(), + }, + feedId: 0, + }) + } + } + + feedPosts.forEach(async (post: OBSSStorage.PostStructOutput) => { + legacyPosts.push({ + post: { + author: post.author, + metadata: { + digest: post.metadata.digest, + hashFunction: post.metadata.hashFunction, + size: post.metadata.size, + }, + commentsFeedId: post.commentsFeedId, + timestamp: post.timestamp.toNumber(), + }, + feedId: i, + }) + // Collect reactions + try { + const lastReactionId = await legacyContract.lastReactionIds( + post.metadata.digest + ) + for ( + let reactionId = 0; + reactionId < lastReactionId.toNumber(); + reactionId++ + ) { + const reaction = await legacyContract.reactions( + post.metadata.digest, + reactionId + ) + + if ( + userToReaction.has( + `${post.commentsFeedId}-${reaction.reactionOwner}` + ) || + reaction.reactionOwner === + '0x0000000000000000000000000000000000000000' + ) + continue + + const currentReactionId = await legacyContract.reactionsUserToId( + post.metadata.digest, + reaction.reactionOwner + ) + + const currentReaction = currentReactionId.eq(reactionId) + ? reaction + : await legacyContract.reactions( + post.metadata.digest, + currentReactionId.toNumber() + ) + + const { reactionType, value, reactionOwner } = currentReaction + + userToReaction.set( + `${post.commentsFeedId}-${reaction.reactionOwner}`, + { + postId: post.commentsFeedId, + reactionType, + value, + reactionOwner, + } + ) + } + } catch (error) { + console.log(error) + } + }) + } + + const reactions = Array.from(userToReaction.values()).map((reaction) => ({ + reaction, + })) + + legacyReactions.push(...reactions) + + const sorted = legacyPosts.sort( + (a, b) => + (a.post.commentsFeedId as BigNumber).toNumber() - + (b.post.commentsFeedId as BigNumber).toNumber() + ) + + return { + legacyPosts: sorted, + legacyReactions, + } +} + main().catch((error) => { console.error(error) process.exitCode = 1 diff --git a/test/OBSSStorage.ts b/test/OBSSStorage.ts index 5745447..9ac34ce 100644 --- a/test/OBSSStorage.ts +++ b/test/OBSSStorage.ts @@ -2,6 +2,8 @@ import { MOCK_CID, getFakeAllowMapContract, getFeedPostsBatch, + getLegacyFeedPostsBatch, + getLegacyReactionsBatch, getReactionsBatch, getRemoveReactionsBatch, zeroAddress, @@ -122,5 +124,24 @@ describe('OBSSStorage contract tests', () => { ) ) }) + it('successfully load the legacy data', async function () { + const legacyPosts = getLegacyFeedPostsBatch() + const legacyReactions = getLegacyReactionsBatch() + + expect( + await this.contract.migrateLegacyData(legacyPosts, legacyReactions) + ) + }) + it('should lock data loading after calling `lockDataLoading`', async function () { + const legacyPosts = getLegacyFeedPostsBatch() + const legacyReactions = getLegacyReactionsBatch() + + await this.contract.migrateLegacyData(legacyPosts, legacyReactions) + // Lock data loading + await this.contract.lockDataMigration() + await expect( + this.contract.migrateLegacyData(legacyPosts, legacyReactions) + ).to.be.revertedWith('All legacy data already loaded') + }) }) }) diff --git a/test/utils.ts b/test/utils.ts index fff8bdc..49bf330 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,5 +1,6 @@ import { BigNumber } from 'ethers' import { IncrementalMerkleTree } from '@zk-kit/incremental-merkle-tree' +import { OBSSStorage } from 'typechain' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { buildPoseidon } from 'circomlibjs' import { randomBytes } from 'ethers/lib/utils' @@ -116,6 +117,29 @@ export function getFeedPostsBatch(length = 10) { return posts } +export function getLegacyFeedPostsBatch(length = 10) { + const posts: OBSSStorage.LegacyPostStruct[] = [] + + for (let i = 0; i < length; i++) { + posts.push({ + post: { + author: `0x000000000000000000000000000000000000000${i}`, + metadata: { + digest: generateRandomBytes32(), + hashFunction: BigNumber.from(0), + size: BigNumber.from(0), + }, + commentsFeedId: i + 1, + timestamp: 1000000 * i, + }, + feedId: 0, + }) + posts[i].post.metadata.digest = generateRandomBytes32() + } + + return posts +} + export function getReactionsBatch(length = 10) { const reactions: { postId: number @@ -129,6 +153,23 @@ export function getReactionsBatch(length = 10) { return reactions } +export function getLegacyReactionsBatch(length = 10) { + const reactions: OBSSStorage.LegacyReactionStruct[] = [] + + for (let i = 0; i < length; i++) { + reactions.push({ + reaction: { + postId: i + 1, + reactionType: 1, + value: 0, + reactionOwner: `0x000000000000000000000000000000000000000${i}`, + }, + }) + } + + return reactions +} + export function getRemoveReactionsBatch(length = 10) { const reactions: { postId: number diff --git a/yarn.lock b/yarn.lock index b8ef75a..b246c04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,13 @@ __metadata: languageName: node linkType: hard +"@big-whale-labs/constants@npm:^0.1.39": + version: 0.1.75 + resolution: "@big-whale-labs/constants@npm:0.1.75" + checksum: 2d01855825f42937761912f878e8e24e4453de809f1d91dea429ef96b8b22c458cc109176066de6d199c7da82b2e7e2823cb652d1d8584ab39fa68d3e7086b67 + languageName: node + linkType: hard + "@big-whale-labs/constants@npm:^0.1.58": version: 0.1.58 resolution: "@big-whale-labs/constants@npm:0.1.58" @@ -46,12 +53,26 @@ __metadata: languageName: node linkType: hard +"@big-whale-labs/obss-storage-contract@npm:^0.1.4": + version: 0.1.4 + resolution: "@big-whale-labs/obss-storage-contract@npm:0.1.4" + dependencies: + "@big-whale-labs/constants": ^0.1.39 + "@big-whale-labs/ketl-allow-map-contract": ^0.0.3 + "@zk-kit/incremental-merkle-tree": ^1.0.0 + "@zk-kit/incremental-merkle-tree.sol": ^1.3.3 + circomlibjs: ^0.1.7 + checksum: 96a96b3056e623455e780898457c426a34a4a2fdba4396bbeb2ff736e797af6b09252069d3fbf6058941b1799773304677241cc25ad62d9f52072ef525ab258c + languageName: node + linkType: hard + "@big-whale-labs/obss-storage-contract@workspace:.": version: 0.0.0-use.local resolution: "@big-whale-labs/obss-storage-contract@workspace:." dependencies: "@big-whale-labs/constants": ^0.1.58 "@big-whale-labs/ketl-allow-map-contract": ^0.0.3 + "@big-whale-labs/obss-storage-contract": ^0.1.4 "@big-whale-labs/versioned-contract": ^1.0.2 "@ethersproject/providers": ^5.7.2 "@nomiclabs/hardhat-ethers": ^2.2.1