Skip to content

Commit

Permalink
feat: implement feature to disable invitation (#148)
Browse files Browse the repository at this point in the history
* feat: implement feature to disable invitation

* audit recommendation

* add expiration

* typo

* update addresses

* add pk
  • Loading branch information
andresaiello authored Feb 2, 2024
1 parent d7f76a5 commit f79723b
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 84 deletions.
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
8 changes: 4 additions & 4 deletions packages/zevm-app-contracts/test/zeta-points/test.helpers.ts
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

0 comments on commit f79723b

Please sign in to comment.