Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Smart contract to verify an address and for accept an invitation #124

2 changes: 1 addition & 1 deletion packages/addresses/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
"vite": "^3.1.0",
"vite-plugin-dts": "^1.4.1"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

contract InvitationManager {
/* An ECDSA signature. */
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
// Records the timestamp when a particular user gets verified.
mapping(address => uint256) public userVerificationTimestamps;

// Records the timestamp when a particular user accepted an invitation from an inviter.
mapping(address => mapping(address => uint256)) public acceptedInvitationsTimestamp;

// Store invitees for each inviter
mapping(address => address[]) public inviteeLists;

// Total invites accepted by day (using the start timestamp of each day as key)
mapping(uint256 => uint256) public totalInvitesByDay;
andresaiello marked this conversation as resolved.
Show resolved Hide resolved

// Total invites accepted by inviter by day (using the start timestamp of each day as key)
mapping(address => mapping(uint256 => uint256)) public totalInvitesByInviterByDay;

error UserAlreadyVerified();
error UnrecognizedInvitation();
error IndexOutOfBounds();
error CanNotInviteYourself();

event UserVerified(address indexed userAddress, uint256 verifiedAt);
event InvitationAccepted(address indexed inviter, address indexed invitee, uint256 acceptedAt);

function _markAsVerified(address user) internal {
// Check if the user is already verified
if (userVerificationTimestamps[user] > 0) revert UserAlreadyVerified();

userVerificationTimestamps[user] = block.timestamp;
emit UserVerified(user, block.timestamp);
}

function markAsVerified() external {
_markAsVerified(msg.sender);
}

function hasBeenVerified(address userAddress) external view returns (bool) {
return userVerificationTimestamps[userAddress] > 0;
}

function getVerifiedTimestamp(address userAddress) external view returns (uint256) {
return userVerificationTimestamps[userAddress];
}

function _verifySignature(address inviter, Signature calldata signature) private pure {
bytes32 payloadHash = keccak256(abi.encode(inviter));
bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));

address messageSigner = ecrecover(messageHash, signature.v, signature.r, signature.s);
if (inviter != messageSigner) revert UnrecognizedInvitation();
}

function confirmAndAcceptInvitation(address inviter, Signature calldata signature) external {
andresaiello marked this conversation as resolved.
Show resolved Hide resolved
if (inviter == msg.sender) revert CanNotInviteYourself();
if (userVerificationTimestamps[inviter] == 0) revert UnrecognizedInvitation();
_verifySignature(inviter, signature);

acceptedInvitationsTimestamp[inviter][msg.sender] = block.timestamp;
_markAsVerified(msg.sender);

// Add the invitee to the inviter's list
inviteeLists[inviter].push(msg.sender);

uint256 dayStartTimestamp = (block.timestamp / 86400) * 86400; // Normalize to the start of the day

totalInvitesByDay[dayStartTimestamp]++;
totalInvitesByInviterByDay[inviter][dayStartTimestamp]++;

emit InvitationAccepted(inviter, msg.sender, block.timestamp);
}

function getInviteeCount(address inviter) external view returns (uint256) {
return inviteeLists[inviter].length;
}

function getInviteeAtIndex(address inviter, uint256 index) external view returns (address) {
if (index >= inviteeLists[inviter].length) revert IndexOutOfBounds();
return inviteeLists[inviter][index];
}

function getTotalInvitesOnDay(uint256 dayStartTimestamp) external view returns (uint256) {
return totalInvitesByDay[dayStartTimestamp];
}

function getInvitesByInviterOnDay(address inviter, uint256 dayStartTimestamp) external view returns (uint256) {
return totalInvitesByInviterByDay[inviter][dayStartTimestamp];
}
}
26 changes: 26 additions & 0 deletions packages/zevm-app-contracts/scripts/zeta-points/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isNetworkName } from "@zetachain/addresses";
import { saveAddress } from "@zetachain/addresses-tools";
import { ethers, network } from "hardhat";

import { InvitationManager__factory, UserVerificationRegistry__factory } from "../../typechain-types";

const networkName = network.name;

const invitationManager = async () => {
const InvitationManagerFactory = (await ethers.getContractFactory("InvitationManager")) as InvitationManager__factory;

const invitationManager = await InvitationManagerFactory.deploy();
await invitationManager.deployed();
console.log("InvitationManager deployed to:", invitationManager.address);
// saveAddress("invitationManager", invitationManager.address);
};

const main = async () => {
if (!isNetworkName(networkName)) throw new Error("Invalid network name");
await invitationManager();
};

main().catch(error => {
console.error(error);
process.exit(1);
});
106 changes: 106 additions & 0 deletions packages/zevm-app-contracts/test/zeta-points/InvitationManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 } from "../../typechain-types";
import { getInvitationSig } from "./test.helpers";

describe("InvitationManager Contract test", () => {
let invitationManager: InvitationManager,
inviter: SignerWithAddress,
invitee: SignerWithAddress,
addrs: SignerWithAddress[];

beforeEach(async () => {
[inviter, invitee, ...addrs] = await ethers.getSigners();
const InvitationManager = await ethers.getContractFactory("InvitationManager");
//@ts-ignore
invitationManager = await InvitationManager.deploy();
await invitationManager.markAsVerified();
});

describe("True", () => {
it("Should be true", async () => {
expect(true).to.equal(true);
});
});

describe("Invitations test", () => {
it("Should verify an invitation and store it", async () => {
const sig = await getInvitationSig(inviter);

const hasBeenVerifiedBefore = await invitationManager.hasBeenVerified(invitee.address);
await expect(hasBeenVerifiedBefore).to.be.eq(false);

const tx = await invitationManager.connect(invitee).confirmAndAcceptInvitation(inviter.address, sig);
const rec = await tx.wait();

const block = await ethers.provider.getBlock(rec.blockNumber);

const invitation = await invitationManager.acceptedInvitationsTimestamp(inviter.address, invitee.address);
await expect(invitation).to.be.eq(block.timestamp);

const invitationCount = await invitationManager.getInviteeCount(inviter.address);
await expect(invitationCount).to.be.eq(1);

const hasBeenVerifiedAfter = await invitationManager.hasBeenVerified(invitee.address);
await expect(hasBeenVerifiedAfter).to.be.eq(true);
});

it("Should revert if invitation is invalid", async () => {
const sig = await getInvitationSig(inviter);
const tx = invitationManager.connect(invitee).confirmAndAcceptInvitation(addrs[0].address, sig);
await expect(tx).to.be.revertedWith("UnrecognizedInvitation");
});

it("Should revert if inviter has not been verified", async () => {
const sig = await getInvitationSig(addrs[0]);
const tx = invitationManager.connect(invitee).confirmAndAcceptInvitation(addrs[0].address, sig);
await expect(tx).to.be.revertedWith("UnrecognizedInvitation");
});

it("Should revert if invitation is already accepted", async () => {
const sig = await getInvitationSig(inviter);
await invitationManager.connect(invitee).confirmAndAcceptInvitation(inviter.address, sig);
const tx = invitationManager.connect(invitee).confirmAndAcceptInvitation(inviter.address, sig);
await expect(tx).to.be.revertedWith("UserAlreadyVerified");
});

it("Should count only for today if I just accepted", async () => {
const sig = await getInvitationSig(inviter);
const tx = await invitationManager.connect(invitee).confirmAndAcceptInvitation(inviter.address, sig);
const rec = await tx.wait();

const block = await ethers.provider.getBlock(rec.blockNumber);

const invitation = await invitationManager.acceptedInvitationsTimestamp(inviter.address, invitee.address);
await expect(invitation).to.be.eq(block.timestamp);

const invitationCount = await invitationManager.getInviteeCount(inviter.address);
await expect(invitationCount).to.be.eq(1);

const now = block.timestamp;
const todayTimestamp = Math.floor(now / 86400) * 86400;
const invitationCountToday = await invitationManager.getTotalInvitesOnDay(todayTimestamp);
await expect(invitationCountToday).to.be.eq(1);

const invitationByInviterCountToday = await invitationManager.getInvitesByInviterOnDay(
inviter.address,
todayTimestamp
);
await expect(invitationByInviterCountToday).to.be.eq(1);

const yesterdayTimestamp = todayTimestamp - 24 * 60 * 60;
const invitationCountYesterday = await invitationManager.getTotalInvitesOnDay(yesterdayTimestamp);
await expect(invitationCountYesterday).to.be.eq(0);

const invitationByInviterCountYesterday = await invitationManager.getInvitesByInviterOnDay(
inviter.address,
yesterdayTimestamp
);
await expect(invitationByInviterCountYesterday).to.be.eq(0);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 } from "../../typechain-types";

describe("UserVerificationRegistry Contract test", () => {
let invitationManager: InvitationManager, user: SignerWithAddress, addrs: SignerWithAddress[];

beforeEach(async () => {
[user, ...addrs] = await ethers.getSigners();
const InvitationManagerFactory = await ethers.getContractFactory("InvitationManager");
//@ts-ignore
invitationManager = await InvitationManagerFactory.deploy();
});

describe("True", () => {
it("Should be true", async () => {
expect(true).to.equal(true);
});
});

describe("Vereification test", () => {
it("Should be able to verify a wallet", async () => {
const hasBeenVerified = await invitationManager.hasBeenVerified(user.address);
expect(hasBeenVerified).to.be.false;

const tx = await invitationManager.markAsVerified();
const receipt = await tx.wait();
const block = await ethers.provider.getBlock(receipt.blockNumber);

const hasBeenVerifiedAfter = await invitationManager.hasBeenVerified(user.address);
expect(hasBeenVerifiedAfter).to.be.true;

const verification = await invitationManager.getVerifiedTimestamp(user.address);
expect(verification).to.be.eq(block.timestamp);
});
});
});
12 changes: 12 additions & 0 deletions packages/zevm-app-contracts/test/zeta-points/test.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { ethers } from "hardhat";

export const getInvitationSig = async (signer: SignerWithAddress) => {
let payload = ethers.utils.defaultAbiCoder.encode(["address"], [signer.address]);

let payloadHash = ethers.utils.keccak256(payload);

// This adds the message prefix
let signature = await signer.signMessage(ethers.utils.arrayify(payloadHash));
return ethers.utils.splitSignature(signature);
};
Loading