-
Notifications
You must be signed in to change notification settings - Fork 230
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Smart contract to verify an address and for accept an invitation (#124)
* 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
1 parent
a0b9862
commit b418a4d
Showing
6 changed files
with
314 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,4 +22,4 @@ | |
"vite": "^3.1.0", | ||
"vite-plugin-dts": "^1.4.1" | ||
} | ||
} | ||
} |
97 changes: 97 additions & 0 deletions
97
packages/zevm-app-contracts/contracts/zeta-points/InvitationManager.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
137
packages/zevm-app-contracts/test/zeta-points/InvitationManager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
41 changes: 41 additions & 0 deletions
41
packages/zevm-app-contracts/test/zeta-points/UserVerificationRegistry.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
12
packages/zevm-app-contracts/test/zeta-points/test.helpers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |