From 764cb2d26dc19b8d52001fcc699acc2ce2421d03 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Tue, 5 Nov 2024 16:08:26 -0300 Subject: [PATCH] feat: new enrollment contract --- .vscode/settings.json | 2 +- .../zeta-points/InvitationManager.sol | 4 +- .../zeta-points/InvitationManagerV2.sol | 66 +++++++ .../zevm-app-contracts/data/addresses.json | 5 +- .../zeta-points/deploy-invitationV2.ts | 33 ++++ .../test/zeta-points/InvitationManagerV2.ts | 175 ++++++++++++++++++ .../zeta-points/invitationManager.helpers.ts | 40 ++++ 7 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 packages/zevm-app-contracts/contracts/zeta-points/InvitationManagerV2.sol create mode 100644 packages/zevm-app-contracts/scripts/zeta-points/deploy-invitationV2.ts create mode 100644 packages/zevm-app-contracts/test/zeta-points/InvitationManagerV2.ts create mode 100644 packages/zevm-app-contracts/test/zeta-points/invitationManager.helpers.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a8e2505..e6e32cd1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "statusBarItem.hoverBackground": "#71C87D" }, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "[solidity]": { "editor.tabSize": 4, diff --git a/packages/zevm-app-contracts/contracts/zeta-points/InvitationManager.sol b/packages/zevm-app-contracts/contracts/zeta-points/InvitationManager.sol index aad0d270..cfe703cb 100644 --- a/packages/zevm-app-contracts/contracts/zeta-points/InvitationManager.sol +++ b/packages/zevm-app-contracts/contracts/zeta-points/InvitationManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.7; +pragma solidity ^0.8.20; contract InvitationManager { /* An ECDSA signature. */ @@ -69,7 +69,7 @@ contract InvitationManager { return userVerificationTimestamps[userAddress]; } - function _verifySignature(address inviter, uint256 expiration, Signature calldata signature) private pure { + function _verifySignature(address inviter, uint256 expiration, Signature calldata signature) internal pure { bytes32 payloadHash = keccak256(abi.encode(inviter, expiration)); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash)); diff --git a/packages/zevm-app-contracts/contracts/zeta-points/InvitationManagerV2.sol b/packages/zevm-app-contracts/contracts/zeta-points/InvitationManagerV2.sol new file mode 100644 index 00000000..410eb756 --- /dev/null +++ b/packages/zevm-app-contracts/contracts/zeta-points/InvitationManagerV2.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +import "./InvitationManager.sol"; + +contract InvitationManagerV2 is EIP712 { + bytes32 private constant VERIFY_TYPEHASH = keccak256("Verify(address to,uint256 signatureExpiration)"); + + InvitationManager public immutable invitationManager; + + struct VerifyData { + address to; + bytes signature; + uint256 signatureExpiration; + } + + // Records the timestamp when a particular user gets verified. + mapping(address => uint256) public userVerificationTimestamps; + + error UserAlreadyVerified(); + error SignatureExpired(); + error InvalidSigner(); + + event UserVerified(address indexed userAddress, uint256 verifiedAt, uint256 unix_timestamp); + + constructor(InvitationManager _invitationManager) EIP712("InvitationManagerV2", "1") { + invitationManager = _invitationManager; + } + + function _markAsVerified(address user) internal { + // Check if the user is already verified + if (hasBeenVerified(user)) revert UserAlreadyVerified(); + + userVerificationTimestamps[user] = block.timestamp; + emit UserVerified(user, block.timestamp, block.timestamp); + } + + function markAsVerified() external { + _markAsVerified(msg.sender); + } + + function hasBeenVerified(address userAddress) public view returns (bool) { + if (userVerificationTimestamps[userAddress] > 0) return true; + if (address(invitationManager) != address(0) && invitationManager.hasBeenVerified(userAddress)) return true; + return false; + } + + function _verify(VerifyData memory claimData) private view { + bytes32 structHash = keccak256(abi.encode(VERIFY_TYPEHASH, claimData.to, claimData.signatureExpiration)); + bytes32 constructedHash = _hashTypedDataV4(structHash); + + if (!SignatureChecker.isValidSignatureNow(claimData.to, constructedHash, claimData.signature)) { + revert InvalidSigner(); + } + + if (block.timestamp > claimData.signatureExpiration) revert SignatureExpired(); + } + + function markAsVerifiedWithSignature(VerifyData memory data) external { + _verify(data); + _markAsVerified(data.to); + } +} diff --git a/packages/zevm-app-contracts/data/addresses.json b/packages/zevm-app-contracts/data/addresses.json index ba5e0238..2e3e46a5 100644 --- a/packages/zevm-app-contracts/data/addresses.json +++ b/packages/zevm-app-contracts/data/addresses.json @@ -1,7 +1,7 @@ { "zevm": { "zeta_testnet": { - "disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d", + "disperse": "0x049893Bd0fC4923FC1B1136Ef2ac996C55D4942C", "rewardDistributorFactory": "0xB9dc665610CF5109cE23aBBdaAc315B41FA094c1", "zetaSwap": "0xA8168Dc495Ed61E70f5c1941e2860050AB902cEF", "zetaSwapBtcInbound": "0x358E2cfC0E16444Ba7D3164Bbeeb6bEA7472c559", @@ -11,7 +11,8 @@ "InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3", "ProofOfLiveness": "0x981EB6fD19717Faf293Fba0cBD05C6Ac97b8C808", "TimelockController": "0x44139C2150c11c25f517B8a8F974b59C82aEe709", - "ZetaXPGov": "0x854032d484aE21acC34F36324E55A8080F21Af12" + "ZetaXPGov": "0x854032d484aE21acC34F36324E55A8080F21Af12", + "invitationManagerV2": "0xb80f6360194Dd6B47B80bd8730b3dBb05a39e723" }, "zeta_mainnet": { "disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d", diff --git a/packages/zevm-app-contracts/scripts/zeta-points/deploy-invitationV2.ts b/packages/zevm-app-contracts/scripts/zeta-points/deploy-invitationV2.ts new file mode 100644 index 00000000..70e9258d --- /dev/null +++ b/packages/zevm-app-contracts/scripts/zeta-points/deploy-invitationV2.ts @@ -0,0 +1,33 @@ +import { isProtocolNetworkName } from "@zetachain/protocol-contracts"; +import { ethers, network } from "hardhat"; + +import { InvitationManagerV2__factory } from "../../typechain-types"; +import { getZEVMAppAddress, saveAddress } from "../address.helpers"; +import { verifyContract } from "../explorer.helpers"; + +const networkName = network.name; + +const invitationManager = async () => { + if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name"); + + const invitationManagerV1 = getZEVMAppAddress("invitationManager", networkName); + + const InvitationManagerFactory = (await ethers.getContractFactory( + "InvitationManagerV2" + )) as InvitationManagerV2__factory; + const invitationManager = await InvitationManagerFactory.deploy(invitationManagerV1); + await invitationManager.deployed(); + console.log("InvitationManagerV2 deployed to:", invitationManager.address); + saveAddress("invitationManagerV2", invitationManager.address, networkName); + await verifyContract(invitationManager.address, [invitationManagerV1]); +}; + +const main = async () => { + if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name"); + await invitationManager(); +}; + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/zevm-app-contracts/test/zeta-points/InvitationManagerV2.ts b/packages/zevm-app-contracts/test/zeta-points/InvitationManagerV2.ts new file mode 100644 index 00000000..d34dea40 --- /dev/null +++ b/packages/zevm-app-contracts/test/zeta-points/InvitationManagerV2.ts @@ -0,0 +1,175 @@ +import { expect, use } from "chai"; +import { solidity } from "ethereum-waffle"; +use(solidity); +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { ethers } from "hardhat"; + +import { InvitationManager, InvitationManagerV2 } from "../../typechain-types"; +import { EnrollmentSigned, getEnrollmentSignature } from "./invitationManager.helpers"; + +const HARDHAT_CHAIN_ID = 1337; + +describe("InvitationManagerV2 Contract test", () => { + let invitationManager: InvitationManager, + invitationManagerV2: InvitationManagerV2, + signer: SignerWithAddress, + user: SignerWithAddress, + addrs: SignerWithAddress[]; + + beforeEach(async () => { + [signer, user, ...addrs] = await ethers.getSigners(); + const InvitationManager = await ethers.getContractFactory("InvitationManager"); + //@ts-ignore + invitationManager = await InvitationManager.deploy(); + + const InvitationManagerV2 = await ethers.getContractFactory("InvitationManagerV2"); + //@ts-ignore + invitationManagerV2 = await InvitationManagerV2.deploy(invitationManager.address); + }); + + const getTomorrowTimestamp = async () => { + const block = await ethers.provider.getBlock("latest"); + const now = block.timestamp; + const tomorrow = now + 24 * 60 * 60; + return tomorrow; + }; + + describe("True", () => { + it("Should be true", async () => { + expect(true).to.equal(true); + }); + }); + + describe("Invitations test", () => { + it("Should do enrollment", async () => { + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(false); + } + + await invitationManagerV2.connect(user).markAsVerified(); + + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(true); + } + }); + + it("Should execute enrollement with from other", async () => { + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(false); + } + + const signatureExpiration = await getTomorrowTimestamp(); + const signature = await getEnrollmentSignature( + HARDHAT_CHAIN_ID, + invitationManagerV2.address, + user, + signatureExpiration, + user.address + ); + const enrollementParams: EnrollmentSigned = { + signature, + signatureExpiration, + to: user.address, + } as EnrollmentSigned; + + await invitationManagerV2.markAsVerifiedWithSignature(enrollementParams); + + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(true); + } + }); + + it("Should check if user was enroll in previus version", async () => { + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(false); + } + + await invitationManager.connect(user).markAsVerified(); + + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(true); + } + }); + + it("Should fail if try to enroll somebody else", async () => { + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(false); + } + + const signatureExpiration = await getTomorrowTimestamp(); + const signature = await getEnrollmentSignature( + HARDHAT_CHAIN_ID, + invitationManagerV2.address, + user, + signatureExpiration, + user.address + ); + const enrollementParams: EnrollmentSigned = { + signature, + signatureExpiration, + to: signer.address, + } as EnrollmentSigned; + + const tx = invitationManagerV2.markAsVerifiedWithSignature(enrollementParams); + await expect(tx).to.be.revertedWith("InvalidSigner"); + }); + + it("Should fail if try to enroll and was already enrolled", async () => { + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(false); + } + + const signatureExpiration = await getTomorrowTimestamp(); + const signature = await getEnrollmentSignature( + HARDHAT_CHAIN_ID, + invitationManagerV2.address, + user, + signatureExpiration, + user.address + ); + const enrollementParams: EnrollmentSigned = { + signature, + signatureExpiration, + to: user.address, + } as EnrollmentSigned; + + await invitationManagerV2.markAsVerifiedWithSignature(enrollementParams); + + const tx = invitationManagerV2.markAsVerifiedWithSignature(enrollementParams); + await expect(tx).to.be.revertedWith("UserAlreadyVerified"); + }); + + it("Should fail if try to enroll and was enrolled with previus contract", async () => { + { + const hasBeenVerifiedBefore = await invitationManagerV2.hasBeenVerified(user.address); + await expect(hasBeenVerifiedBefore).to.be.eq(false); + } + + await invitationManager.connect(user).markAsVerified(); + const signatureExpiration = await getTomorrowTimestamp(); + const signature = await getEnrollmentSignature( + HARDHAT_CHAIN_ID, + invitationManagerV2.address, + user, + signatureExpiration, + user.address + ); + const enrollementParams: EnrollmentSigned = { + signature, + signatureExpiration, + to: user.address, + } as EnrollmentSigned; + + const tx = invitationManagerV2.markAsVerifiedWithSignature(enrollementParams); + await expect(tx).to.be.revertedWith("UserAlreadyVerified"); + }); + }); +}); diff --git a/packages/zevm-app-contracts/test/zeta-points/invitationManager.helpers.ts b/packages/zevm-app-contracts/test/zeta-points/invitationManager.helpers.ts new file mode 100644 index 00000000..3edbbd2d --- /dev/null +++ b/packages/zevm-app-contracts/test/zeta-points/invitationManager.helpers.ts @@ -0,0 +1,40 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +export interface Enrollment { + to: string; +} + +export interface EnrollmentSigned extends Enrollment { + signature: string; + signatureExpiration: number; +} + +export const getEnrollmentSignature = async ( + chainId: number, + verifyingContract: string, + signer: SignerWithAddress, + signatureExpiration: number, + to: string +) => { + const domain = { + chainId: chainId, + name: "InvitationManagerV2", + verifyingContract: verifyingContract, + version: "1", + }; + + const types = { + Verify: [ + { name: "to", type: "address" }, + { name: "signatureExpiration", type: "uint256" }, + ], + }; + + const value = { + signatureExpiration, + to, + }; + // Signing the data + const signature = await signer._signTypedData(domain, types, value); + return signature; +};