Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: new enrollment contract #196

Merged
merged 1 commit into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"statusBarItem.hoverBackground": "#71C87D"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"[solidity]": {
"editor.tabSize": 4,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;
pragma solidity ^0.8.20;

contract InvitationManager {
/* An ECDSA signature. */
Expand Down Expand Up @@ -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));

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
5 changes: 3 additions & 2 deletions packages/zevm-app-contracts/data/addresses.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"zevm": {
"zeta_testnet": {
"disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d",
"disperse": "0x049893Bd0fC4923FC1B1136Ef2ac996C55D4942C",
"rewardDistributorFactory": "0xB9dc665610CF5109cE23aBBdaAc315B41FA094c1",
"zetaSwap": "0xA8168Dc495Ed61E70f5c1941e2860050AB902cEF",
"zetaSwapBtcInbound": "0x358E2cfC0E16444Ba7D3164Bbeeb6bEA7472c559",
Expand All @@ -11,7 +11,8 @@
"InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3",
"ProofOfLiveness": "0x981EB6fD19717Faf293Fba0cBD05C6Ac97b8C808",
"TimelockController": "0x44139C2150c11c25f517B8a8F974b59C82aEe709",
"ZetaXPGov": "0x854032d484aE21acC34F36324E55A8080F21Af12"
"ZetaXPGov": "0x854032d484aE21acC34F36324E55A8080F21Af12",
"invitationManagerV2": "0xb80f6360194Dd6B47B80bd8730b3dBb05a39e723"
},
"zeta_mainnet": {
"disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d",
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
175 changes: 175 additions & 0 deletions packages/zevm-app-contracts/test/zeta-points/InvitationManagerV2.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
};
Loading