Skip to content

Commit

Permalink
Smart contract to verify an address and for accept an invitation (#124)
Browse files Browse the repository at this point in the history
* SC to verify an address and accept an invitation

* naming

* add test

* add validation

* naming

* add counters

* add test

* update contract to unpersonalized links

* add registration logic to manager

* add validation

* fix test

* rename event

* rename messageSigner

* add index to event and test

* improve test
  • Loading branch information
andresaiello authored Nov 13, 2023
1 parent a0b9862 commit b418a4d
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 1 deletion.
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;

// 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 index, 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 {
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, inviteeLists[inviter].length - 1, 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);
});
137 changes: 137 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,137 @@
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);
});

it("Should emit the right event when invitation is accepted", 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 event = rec.events?.find(e => e.event === "InvitationAccepted");
const block = await ethers.provider.getBlock(rec.blockNumber);

expect(event?.args?.inviter).to.be.eq(inviter.address);
expect(event?.args?.invitee).to.be.eq(invitee.address);
expect(event?.args?.index).to.be.eq(0);
expect(event?.args?.acceptedAt).to.be.eq(block.timestamp);
const inviteeByIndex = await invitationManager.getInviteeAtIndex(inviter.address, event?.args?.index);
expect(inviteeByIndex).to.be.eq(invitee.address);

const tx2 = await invitationManager.connect(addrs[0]).confirmAndAcceptInvitation(inviter.address, sig);
const rec2 = await tx2.wait();
const event2 = rec2.events?.find(e => e.event === "InvitationAccepted");
const block2 = await ethers.provider.getBlock(rec2.blockNumber);

await expect(event2?.args?.inviter).to.be.eq(inviter.address);
await expect(event2?.args?.invitee).to.be.eq(addrs[0].address);
await expect(event2?.args?.index).to.be.eq(1);
await expect(event2?.args?.acceptedAt).to.be.eq(block2.timestamp);
const inviteeByIndex2 = await invitationManager.getInviteeAtIndex(inviter.address, event2?.args?.index);
expect(inviteeByIndex2).to.be.eq(addrs[0].address);
});
});
});
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);
};

0 comments on commit b418a4d

Please sign in to comment.