diff --git a/hardhat/contracts/Event.sol b/hardhat/contracts/Event.sol index 9c4a20cc..6b44421a 100644 --- a/hardhat/contracts/Event.sol +++ b/hardhat/contracts/Event.sol @@ -4,11 +4,13 @@ pragma solidity ^0.8.4; import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/metatx/MinimalForwarderUpgradeable.sol"; import "./IMintNFT.sol"; import "./IOperationController.sol"; +import "./ERC2771ContextUpgradeable.sol"; import "hardhat/console.sol"; -contract EventManager is OwnableUpgradeable { +contract EventManager is OwnableUpgradeable, ERC2771ContextUpgradeable { struct Group { uint256 groupId; address ownerAddress; @@ -60,15 +62,19 @@ contract EventManager is OwnableUpgradeable { mapping(uint256 => mapping(address => mapping(bytes32 => bool))) private memberRolesByGroupId; mapping(uint256 => address[]) private memberAddressesByGroupId; + //mapping(address => uint256) private userPoints; modifier onlyGroupOwner(uint256 _groupId) { - require(_isGroupOwner(_groupId, msg.sender), "You have no permission"); + require( + _isGroupOwner(_groupId, _msgSender()), + "You have no permission" + ); _; } modifier onlyAdminAccess(uint256 _groupId) { require( - _hasAdminAccess(_groupId, msg.sender), + _hasAdminAccess(_groupId, _msgSender()), "You have no permission" ); _; @@ -76,7 +82,7 @@ contract EventManager is OwnableUpgradeable { modifier onlyCollaboratorAccess(uint256 _groupId) { require( - _hasCollaboratorAccess(_groupId, msg.sender), + _hasCollaboratorAccess(_groupId, _msgSender()), "You have no permission" ); _; @@ -129,6 +135,7 @@ contract EventManager is OwnableUpgradeable { // Currently, reinitializer(3) was executed as constructor. function initialize( + MinimalForwarderUpgradeable _trustedForwarder, address _owner, address _relayerAddr, uint256 _mtxPrice, @@ -141,22 +148,59 @@ contract EventManager is OwnableUpgradeable { _groupIds.increment(); _eventRecordIds.increment(); } + __ERC2771Context_init(address(_trustedForwarder)); relayerAddr = _relayerAddr; mtxPrice = _mtxPrice; maxMintLimit = _maxMintLimit; operationControllerAddr = _operationControllerAddr; } + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (address sender) + { + if (isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + /// @solidity memory-safe-assembly + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return super._msgSender(); + } + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + if (isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return super._msgData(); + } + } + function createGroup(string memory _name) external whenNotPaused { uint256 _newGroupId = _groupIds.current(); _groupIds.increment(); groups.push( - Group({groupId: _newGroupId, ownerAddress: msg.sender, name: _name}) + Group({ + groupId: _newGroupId, + ownerAddress: _msgSender(), + name: _name + }) ); - ownGroupIds[msg.sender].push(_newGroupId); + ownGroupIds[_msgSender()].push(_newGroupId); - emit CreateGroup(msg.sender, _newGroupId); + emit CreateGroup(_msgSender(), _newGroupId); } function getGroups() public view returns (Group[] memory) { @@ -219,17 +263,17 @@ contract EventManager is OwnableUpgradeable { ownGroupIds[_newOwnerAddress].push(_groupId); - for (uint256 i = 0; i < ownGroupIds[msg.sender].length; i++) { - if (ownGroupIds[msg.sender][i] == _groupId) { - ownGroupIds[msg.sender][i] = ownGroupIds[msg.sender][ - ownGroupIds[msg.sender].length - 1 + for (uint256 i = 0; i < ownGroupIds[_msgSender()].length; i++) { + if (ownGroupIds[_msgSender()][i] == _groupId) { + ownGroupIds[_msgSender()][i] = ownGroupIds[_msgSender()][ + ownGroupIds[_msgSender()].length - 1 ]; - ownGroupIds[msg.sender].pop(); + ownGroupIds[_msgSender()].pop(); break; } } - emit TransferGroupOwner(msg.sender, _newOwnerAddress, _groupId); + emit TransferGroupOwner(_msgSender(), _newOwnerAddress, _groupId); } function _isGroupOwner( @@ -248,6 +292,7 @@ contract EventManager is OwnableUpgradeable { bool _useMtx, bool _nonTransferable, bytes32 _secretPhrase, + // uint256 _points, IMintNFT.NFTAttribute[] memory _eventNFTAttributes ) external payable onlyCollaboratorAccess(_groupId) whenNotPaused { require( @@ -291,7 +336,7 @@ contract EventManager is OwnableUpgradeable { eventIdsByGroupId[_groupId].push(_newEventId); groupIdByEventId[_newEventId] = _groupId; - emit CreateEvent(msg.sender, _newEventId); + emit CreateEvent(_msgSender(), _newEventId); } function getEventRecordCount() public view returns (uint256) { diff --git a/hardhat/contracts/IMintNFT.sol b/hardhat/contracts/IMintNFT.sol index 5dbbf0c0..d2c13735 100644 --- a/hardhat/contracts/IMintNFT.sol +++ b/hardhat/contracts/IMintNFT.sol @@ -44,6 +44,16 @@ interface IMintNFT { string memory _secretPhrase ) external; + function getCountOfParticipation(uint256 _groupId, address _address) + external + view + returns (uint256); + + function isHoldingEventNFTByAddress( + address _address, + uint256 _eventId + ) external view returns (bool); + function name() external view returns (string memory); function owner() external view returns (address); diff --git a/hardhat/contracts/MintNFT.sol b/hardhat/contracts/MintNFT.sol index bda2e225..919ab3a5 100644 --- a/hardhat/contracts/MintNFT.sol +++ b/hardhat/contracts/MintNFT.sol @@ -281,6 +281,14 @@ contract MintNFT is isHoldingEventNFT[Hashing.hashingAddressUint256(_addr, _eventId)]; } + function getCountOfParticipation( + uint256 _groupId, + address _address + ) public view returns (uint256) { + bytes32 groupHash = Hashing.hashingAddressUint256(_address, _groupId); + return countOfParticipation[groupHash]; + } + function setEventInfo( uint256 _eventId, uint256 _mintLimit, diff --git a/hardhat/test/EventManager.ts b/hardhat/test/EventManager.ts index fe92030e..ed90b9f6 100644 --- a/hardhat/test/EventManager.ts +++ b/hardhat/test/EventManager.ts @@ -84,6 +84,7 @@ describe("EventManager", function () { const deployedEventManagerContract: any = await upgrades.deployProxy( eventManagerContractFactory, [ + "0xdCb93093424447bF4FE9Df869750950922F1E30B", organizer.address, relayer.address, 250000, @@ -95,6 +96,7 @@ describe("EventManager", function () { } ); eventManager = await deployedEventManagerContract.deployed(); + await eventManager.setMintNFTAddr(mintNFT.address); await mintNFT.setEventManagerAddr(eventManager.address); @@ -102,7 +104,6 @@ describe("EventManager", function () { await generateProof(); publicInputCalldata = _publicInputCalldata; }); - it("Should create", async () => { // does not exist any groups const groupsBeforeCreate = await eventManager.getGroups(); @@ -126,6 +127,7 @@ describe("EventManager", function () { // revert if paused await operationController.connect(organizer).pause(); + await expect( eventManager.createEventRecord( groupsAfterCreate[0].groupId.toNumber(), @@ -485,6 +487,7 @@ describe("EventManager", function () { const deployedEventManagerContract: any = await upgrades.deployProxy( eventManagerContractFactory, [ + "0xdCb93093424447bF4FE9Df869750950922F1E30B", organizer.address, relayer.address, 500000, @@ -540,6 +543,7 @@ describe("EventManager", function () { const deployedEventManagerContract: any = await upgrades.deployProxy( eventManagerContractFactory, [ + "0xdCb93093424447bF4FE9Df869750950922F1E30B", organizer.address, relayer.address, 500000, @@ -615,6 +619,7 @@ describe("EventManager", function () { const deployedEventManagerContract: any = await upgrades.deployProxy( eventManagerContractFactory, [ + "0xdCb93093424447bF4FE9Df869750950922F1E30B", organizer.address, relayer.address, 250000, @@ -709,6 +714,7 @@ describe("EventManager", function () { const deployedEventManagerContract = await upgrades.deployProxy( eventManagerContractFactory, [ + "0xdCb93093424447bF4FE9Df869750950922F1E30B", organizer.address, relayer.address, 250000, @@ -783,6 +789,7 @@ describe("EventManager", function () { const deployedEventManagerContract: any = await upgrades.deployProxy( eventManagerContractFactory, [ + "0xdCb93093424447bF4FE9Df869750950922F1E30B", organizer.address, relayer.address, 250000, diff --git a/hardhat/test/MTX.ts b/hardhat/test/MTX.ts new file mode 100644 index 00000000..fa5325c2 --- /dev/null +++ b/hardhat/test/MTX.ts @@ -0,0 +1,153 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; +import { + EventManager, + MintNFT, + MintRallyForwarder, + OperationController, + SecretPhraseVerifier, +} from "../typechain"; +import { generateProof } from "./helper/secret_phrase"; +import { signMetaTxRequest } from "./helper/signfor_mtx"; + +const attributes = [ + { + metaDataURL: "ipfs://hogehoge/count0.json", + requiredParticipateCount: 0, + }, + { + metaDataURL: "ipfs://hogehoge/count1.json", + requiredParticipateCount: 1, + }, + { + metaDataURL: "ipfs://hogehoge/count5.json", + requiredParticipateCount: 5, + }, +]; + +describe("MTX Event", function () { + let mintNFT: MintNFT; + let secretPhraseVerifier: SecretPhraseVerifier; + let operationController: OperationController; + let eventManager: EventManager; + let mintRallyForwarder: MintRallyForwarder; + let organizer: SignerWithAddress; + let participant1: SignerWithAddress; + let relayer: SignerWithAddress; + + before(async () => { + [organizer, participant1, relayer] = await ethers.getSigners(); + const SecretPhraseVerifierFactory = await ethers.getContractFactory( + "SecretPhraseVerifier" + ); + secretPhraseVerifier = await SecretPhraseVerifierFactory.deploy(); + const OperationControllerFactory = await ethers.getContractFactory( + "OperationController" + ); + const deployedOperationController: any = await upgrades.deployProxy( + OperationControllerFactory, + { initializer: "initialize" } + ); + operationController = deployedOperationController; + await operationController.deployed(); + + const forwarderFactory = await ethers.getContractFactory( + "MintRallyForwarder" + ); + mintRallyForwarder = await forwarderFactory.deploy(); + await mintRallyForwarder.deployed(); + + const MintNFTFactory = await ethers.getContractFactory("MintNFT"); + const deployedMintNFT: any = await upgrades.deployProxy( + MintNFTFactory, + [ + organizer.address, + mintRallyForwarder.address, + secretPhraseVerifier.address, + operationController.address, + ], + { + initializer: "initialize", + } + ); + mintNFT = deployedMintNFT; + await mintNFT.deployed(); + + const eventManagerContractFactory = await ethers.getContractFactory( + "EventManager" + ); + eventManager = (await upgrades.deployProxy( + eventManagerContractFactory, + [ + mintRallyForwarder.address, + organizer.address, + relayer.address, + 250000, + 1000000, + operationController.address, + ], + { + initializer: "initialize", + } + )) as EventManager; + await eventManager.deployed(); + + await eventManager.setMintNFTAddr(mintNFT.address); + await mintNFT.setEventManagerAddr(eventManager.address); + }); + + it("should create group with mtx", async function () { + const { signature, request } = await signMetaTxRequest( + organizer, + mintRallyForwarder, + { + from: organizer.address, + to: eventManager.address, + data: eventManager.interface.encodeFunctionData("createGroup", [ + "mtx group name", + ]), + } + ); + + const tx = await mintRallyForwarder + .connect(relayer) + .execute(request, signature); + await tx.wait(); + + const groups = await eventManager.getGroups(); + expect(groups.length).to.equal(1); + }); + + it("should create event with mtx", async function () { + const { publicInputCalldata } = await generateProof(); + + const { signature, request } = await signMetaTxRequest( + organizer, + mintRallyForwarder, + { + from: organizer.address, + to: eventManager.address, + data: eventManager.interface.encodeFunctionData("createEventRecord", [ + 1, + "mtx event name", + "mtx event description", + "2022-07-30", + 100, + false, + false, + publicInputCalldata[0], + attributes, + ]), + } + ); + + const tx = await mintRallyForwarder + .connect(relayer) + .execute(request, signature); + await tx.wait(); + + const events = await eventManager.getEventRecords(100, 0); + expect(events.length).to.equal(1); + }); +}); diff --git a/hardhat/test/MintNFT.ts b/hardhat/test/MintNFT.ts index 2fa708f7..780affa4 100644 --- a/hardhat/test/MintNFT.ts +++ b/hardhat/test/MintNFT.ts @@ -97,6 +97,7 @@ const deployEventManager = async ( const deployedEventManager: any = await upgrades.deployProxy( EventManager, [ + "0xdCb93093424447bF4FE9Df869750950922F1E30B", deployer.address, relayer.address, 250000, @@ -369,6 +370,12 @@ describe("MintNFT", function () { ); await mintNftTxn2.wait(); expect(await mintNFT.getEventIdOfTokenId(1)).equal(createdEventIds[0]); + expect( + await mintNFT.getCountOfParticipation( + createdGroupId1, + participant1.address + ) + ).equal(1); const { proofCalldata: proofCalldata3 } = await generateProof(); const mintNftTxn3 = await mintNFT @@ -380,6 +387,12 @@ describe("MintNFT", function () { ); await mintNftTxn3.wait(); expect(await mintNFT.getEventIdOfTokenId(2)).equal(createdEventIds[0]); + expect( + await mintNFT.getCountOfParticipation( + createdGroupId1, + participant2.address + ) + ).equal(1); const { proofCalldata: proofCalldata4 } = await generateProof(); const mintNftTxn4 = await mintNFT @@ -391,6 +404,12 @@ describe("MintNFT", function () { ); await mintNftTxn4.wait(); expect(await mintNFT.getEventIdOfTokenId(3)).equal(createdEventIds[1]); + expect( + await mintNFT.getCountOfParticipation( + createdGroupId1, + participant1.address + ) + ).equal(2); const { proofCalldata: proofCalldata5 } = await generateProof(); const mintNftTxn5 = await mintNFT @@ -402,6 +421,12 @@ describe("MintNFT", function () { ); await mintNftTxn5.wait(); expect(await mintNFT.getEventIdOfTokenId(4)).equal(createdEventIds[2]); + expect( + await mintNFT.getCountOfParticipation( + createdGroupId2, + participant1.address + ) + ).equal(1); }); it("get owners of the tokens", async () => { const tokens = [0, 1, 2, 3, 4]; diff --git a/hardhat/test/helper/signfor_mtx.ts b/hardhat/test/helper/signfor_mtx.ts new file mode 100644 index 00000000..ede53b35 --- /dev/null +++ b/hardhat/test/helper/signfor_mtx.ts @@ -0,0 +1,59 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { Contract } from "ethers"; + +const ForwardRequest = [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "data", type: "bytes" }, +]; + +const getMetaTxTypeData = (chainId: number, verifyingContract: string) => { + // Specification of the eth_signTypedData JSON RPC + return { + types: { + ForwardRequest, + }, + domain: { + name: "MintRallyForwarder", + version: "0.0.1", + chainId, + verifyingContract, + }, + primaryType: "ForwardRequest", + }; +}; + +const signTypeData = async (signer: SignerWithAddress, data: any) => { + return await signer._signTypedData(data.domain, data.types, data.message); +}; + +export const buildRequest = async (forwarder: Contract, input: any) => { + // get nonce from forwarder contract + // this nonce is used to prevent replay attack + const nonce = await forwarder.getNonce(input.from); + return { value: 0, gas: 2e6, nonce, ...input }; +}; + +export const buildTypedData = async (forwarder: Contract, request: any) => { + const chainId = 31337; + const typeData = getMetaTxTypeData(chainId, forwarder.address); + return { ...typeData, message: request }; +}; + +export const signMetaTxRequest = async ( + signer: SignerWithAddress, + forwarder: Contract, + input: { + from: string; + to: string; + data: string; + } +) => { + const request = await buildRequest(forwarder, input); + const toSign = await buildTypedData(forwarder, request); + const signature = await signTypeData(signer, toSign); + return { signature, request }; +};