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

feat: implement feature to disable invitation #148

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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
},
"devDependencies": {
"@changesets/cli": "^2.23.1",
"@nomicfoundation/hardhat-verify": "2.0.3",
"@nomiclabs/hardhat-ethers": "^2.0.5",
"@nomiclabs/hardhat-etherscan": "3.0.3",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@typechain/ethers-v5": "^10.0.0",
"@typechain/hardhat": "^6.0.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/zeta-app-contracts/data/addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"bsc_mainnet": {
"multiChainSwap": "",
"multiChainValue": "",
"multiChainValue": "0x33e5fCFfe910B99DB46c259804fCA1317EA0Aa89",
"zetaTokenConsumerUniV2": "",
"zetaTokenConsumerUniV3": ""
},
Expand All @@ -26,7 +26,7 @@
},
"eth_mainnet": {
"multiChainSwap": "",
"multiChainValue": "",
"multiChainValue": "0x910966E1C0Bc9FD74f499723c19Ff9799fE258a5",
"zetaTokenConsumerUniV2": "",
"zetaTokenConsumerUniV3": ""
},
Expand All @@ -44,7 +44,7 @@
},
"zeta_testnet": {
"multiChainSwap": "",
"multiChainValue": "0x82aC45D07dEe4DBDe050e838beF345347DEd99a8",
"multiChainValue": "0x36Cfb6dCd6926dFb749dc8E4b28efc73f3e6FAe3",
"zetaTokenConsumerUniV2": "",
"zetaTokenConsumerUniV3": ""
}
Expand Down
5 changes: 4 additions & 1 deletion packages/zeta-app-contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import "@nomiclabs/hardhat-etherscan";
import "@nomicfoundation/hardhat-verify";
import "@nomiclabs/hardhat-waffle";
import "@typechain/hardhat";
import "hardhat-gas-reporter";
Expand All @@ -11,11 +11,14 @@ import type { HardhatUserConfig } from "hardhat/types";

dotenv.config();

const PRIVATE_KEYS = process.env.PRIVATE_KEY !== undefined ? [`0x${process.env.PRIVATE_KEY}`] : [];

const config: HardhatUserConfig = {
//@ts-ignore
etherscan: {
apiKey: {
// BSC
bsc: process.env.BSCSCAN_API_KEY || "",
bscTestnet: process.env.BSCSCAN_API_KEY || "",
// ETH
goerli: process.env.ETHERSCAN_API_KEY || "",
Expand Down
9 changes: 6 additions & 3 deletions packages/zevm-app-contracts/contracts/disperse/Disperse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
pragma solidity 0.8.7;

import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract Disperse {
using SafeERC20 for IERC20;

bool private locked;

event FundsDispersed(address indexed token, address indexed from, address indexed recipient, uint256 value);
Expand Down Expand Up @@ -38,9 +41,9 @@ contract Disperse {
) external noReentrancy {
uint256 total = 0;
for (uint256 i = 0; i < recipients.length; i++) total += values[i];
require(token.transferFrom(msg.sender, address(this), total));
token.safeTransferFrom(msg.sender, address(this), total);
for (uint256 i = 0; i < recipients.length; i++) {
require(token.transfer(recipients[i], values[i]));
token.safeTransfer(recipients[i], values[i]);
emit FundsDispersed(address(token), msg.sender, recipients[i], values[i]);
}
}
Expand All @@ -51,7 +54,7 @@ contract Disperse {
uint256[] calldata values
) external noReentrancy {
for (uint256 i = 0; i < recipients.length; i++) {
require(token.transferFrom(msg.sender, recipients[i], values[i]));
token.safeTransferFrom(msg.sender, recipients[i], values[i]);
emit FundsDispersed(address(token), msg.sender, recipients[i], values[i]);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ contract InvitationManager {
bytes32 r;
bytes32 s;
}

// Indicate if invitation is still available. The default value is true.
mapping(address => bool) public invitationEnabled;

// Records the timestamp when a particular user gets verified.
mapping(address => uint256) public userVerificationTimestamps;

Expand All @@ -24,23 +28,37 @@ contract InvitationManager {
mapping(address => mapping(uint256 => uint256)) public totalInvitesByInviterByDay;

error UserAlreadyVerified();
error UserNotVerified();
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);
event UserVerified(address indexed userAddress, uint256 verifiedAt, uint256 unix_timestamp);
event InvitationAccepted(
address indexed inviter,
address indexed invitee,
uint256 index,
uint256 expiration,
uint256 acceptedAt,
uint256 unix_timestamp
);

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);
emit UserVerified(user, block.timestamp, block.timestamp);
}

function markAsVerified() external {
_markAsVerified(msg.sender);
invitationEnabled[msg.sender] = true;
}

function updateInvitationStatus(bool value) external {
if (userVerificationTimestamps[msg.sender] == 0) revert UserNotVerified();
invitationEnabled[msg.sender] = value;
}

function hasBeenVerified(address userAddress) external view returns (bool) {
Expand All @@ -51,18 +69,21 @@ contract InvitationManager {
return userVerificationTimestamps[userAddress];
}

function _verifySignature(address inviter, Signature calldata signature) private pure {
bytes32 payloadHash = keccak256(abi.encode(inviter));
function _verifySignature(address inviter, uint256 expiration, Signature calldata signature) private pure {
bytes32 payloadHash = keccak256(abi.encode(inviter, expiration));
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 {
function confirmAndAcceptInvitation(address inviter, uint256 expiration, Signature calldata signature) external {
if (inviter == msg.sender) revert CanNotInviteYourself();
if (userVerificationTimestamps[inviter] == 0) revert UnrecognizedInvitation();
_verifySignature(inviter, signature);
if (!invitationEnabled[inviter]) revert UnrecognizedInvitation();

_verifySignature(inviter, expiration, signature);

if (expiration < block.timestamp) revert UnrecognizedInvitation();

acceptedInvitationsTimestamp[inviter][msg.sender] = block.timestamp;
_markAsVerified(msg.sender);
Expand All @@ -75,7 +96,14 @@ contract InvitationManager {
totalInvitesByDay[dayStartTimestamp]++;
totalInvitesByInviterByDay[inviter][dayStartTimestamp]++;

emit InvitationAccepted(inviter, msg.sender, inviteeLists[inviter].length - 1, block.timestamp);
emit InvitationAccepted(
inviter,
msg.sender,
inviteeLists[inviter].length - 1,
expiration,
block.timestamp,
block.timestamp
);
}

function getInviteeCount(address inviter) external view returns (uint256) {
Expand Down
10 changes: 5 additions & 5 deletions packages/zevm-app-contracts/data/addresses.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"zevm": {
"zeta_testnet": {
"disperse": "0xf394dc01879E39f19eDA533EFD10C82eEee5B2b1",
"rewardDistributorFactory": "0x667e4C493d40015256BDC89E3ba750B2F90359E1",
"zetaSwap": "0x44D1F1f9289DBA1Cf5824bd667184cEBE020aA1c",
"zetaSwapBtcInbound": "0x008b393933D5CA2457Df570CA5D628380FFf6da4",
"invitationManager": "0xF4cF881A3d23936e3710ef2Cbbe93f71C4389918"
"disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d",
"rewardDistributorFactory": "0xB9dc665610CF5109cE23aBBdaAc315B41FA094c1",
"zetaSwap": "0xA8168Dc495Ed61E70f5c1941e2860050AB902cEF",
"zetaSwapBtcInbound": "0x358E2cfC0E16444Ba7D3164Bbeeb6bEA7472c559",
"invitationManager": "0x3649C03C472B698213926543456E9c21081e529d"
}
}
}
2 changes: 1 addition & 1 deletion packages/zevm-app-contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import "@nomiclabs/hardhat-etherscan";
import "@nomicfoundation/hardhat-verify";
import "@nomiclabs/hardhat-waffle";
import "@typechain/hardhat";
import "hardhat-gas-reporter";
Expand Down
16 changes: 11 additions & 5 deletions packages/zevm-app-contracts/test/Disperse.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { parseUnits } from "@ethersproject/units";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { parseEther } from "ethers/lib/utils";
import { ethers, network } from "hardhat";

import { Disperse, Disperse__factory, MockZRC20, MockZRC20__factory } from "../typechain-types";
Expand All @@ -23,18 +24,23 @@ describe("Disperse tests", () => {

describe("Disperse", () => {
it("Should disperse ETH", async () => {
const amount = parseUnits("10");
const count = 500;
const amount = parseEther("0.01");
const balance0 = await ethers.provider.getBalance(accounts[0].address);
const balance1 = await ethers.provider.getBalance(accounts[1].address);
await disperseContract.disperseEther([accounts[0].address, accounts[1].address], [amount, amount.mul(2)], {
value: amount.mul(3),

const bigArrayAddress = new Array(count).fill(accounts[0].address);
const bigArrayAmount = new Array(count).fill(amount);

await disperseContract.disperseEther(bigArrayAddress, bigArrayAmount, {
value: amount.mul(count),
});

const balance0After = await ethers.provider.getBalance(accounts[0].address);
const balance1After = await ethers.provider.getBalance(accounts[1].address);

expect(balance0After.sub(balance0)).to.be.eq(amount);
expect(balance1After.sub(balance1)).to.be.eq(amount.mul(2));
expect(balance0After.sub(balance0)).to.be.eq(amount.mul(count));
expect(balance1After.sub(balance1)).to.be.eq(0);
});

it("Should disperse ETH with surplus", async () => {
Expand Down
62 changes: 47 additions & 15 deletions packages/zevm-app-contracts/test/zeta-points/InvitationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ describe("InvitationManager Contract test", () => {
await invitationManager.markAsVerified();
});

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);
Expand All @@ -29,12 +36,16 @@ describe("InvitationManager Contract test", () => {

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

const sig = await getInvitationSig(inviter, expirationDate);

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 tx = await invitationManager
.connect(invitee)
.confirmAndAcceptInvitation(inviter.address, expirationDate, sig);
const rec = await tx.wait();

const block = await ethers.provider.getBlock(rec.blockNumber);
Expand All @@ -50,38 +61,54 @@ describe("InvitationManager Contract test", () => {
});

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

it("Should revert if invitation is expired", async () => {
const expirationDate = await getTomorrowTimestamp();
const yesterdayTimestamp = expirationDate - 24 * 60 * 60;
const sig = await getInvitationSig(inviter, expirationDate);
const tx = invitationManager
.connect(invitee)
.confirmAndAcceptInvitation(inviter.address, yesterdayTimestamp, 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);
const expirationDate = await getTomorrowTimestamp();
const sig = await getInvitationSig(addrs[0], expirationDate);
const tx = invitationManager.connect(invitee).confirmAndAcceptInvitation(addrs[0].address, expirationDate, 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);
const expirationDate = await getTomorrowTimestamp();
const sig = await getInvitationSig(inviter, expirationDate);
await invitationManager.connect(invitee).confirmAndAcceptInvitation(inviter.address, expirationDate, sig);
const tx = invitationManager.connect(invitee).confirmAndAcceptInvitation(inviter.address, expirationDate, 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 expirationDate = await getTomorrowTimestamp();
const sig = await getInvitationSig(inviter, expirationDate);
const tx = await invitationManager
.connect(invitee)
.confirmAndAcceptInvitation(inviter.address, expirationDate, sig);
const rec = await tx.wait();

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

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);
Expand All @@ -104,12 +131,15 @@ describe("InvitationManager Contract test", () => {
});

it("Should emit the right event when invitation is accepted", async () => {
const sig = await getInvitationSig(inviter);
const expirationDate = await getTomorrowTimestamp();
const sig = await getInvitationSig(inviter, expirationDate);

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 tx = await invitationManager
.connect(invitee)
.confirmAndAcceptInvitation(inviter.address, expirationDate, sig);
const rec = await tx.wait();
const event = rec.events?.find((e) => e.event === "InvitationAccepted");
const block = await ethers.provider.getBlock(rec.blockNumber);
Expand All @@ -121,7 +151,9 @@ describe("InvitationManager Contract test", () => {
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 tx2 = await invitationManager
.connect(addrs[0])
.confirmAndAcceptInvitation(inviter.address, expirationDate, sig);
const rec2 = await tx2.wait();
const event2 = rec2.events?.find((e) => e.event === "InvitationAccepted");
const block2 = await ethers.provider.getBlock(rec2.blockNumber);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +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]);
export const getInvitationSig = async (signer: SignerWithAddress, expirationDate: number) => {
const payload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [signer.address, expirationDate]);

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

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