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: Create proof of liveness smart contract #190

Merged
Merged
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "hardhat/console.sol";

contract ProofOfLiveness {
uint256 constant PROOF_PERIOD = 24 hours;
uint256 constant LAST_PERIODS_LENGTH = 5;

// Mapping to track the proof history for each user (last 5 proof timestamps)
mapping(address => uint256[LAST_PERIODS_LENGTH]) public proofHistory;

// Custom error for when a user has already proved liveness within the last PROOF_PERIOD
error ProofWithinLast24Hours(uint256 lastProofTime);

// Event to log when liveness is proved
event LivenessProved(address indexed user, uint256 proofTimestamp);

// The function to prove liveness, can only be called once every PROOF_PERIOD
function proveLiveness() external {
uint256 currentTime = block.timestamp;
uint256 lastProofTime = proofHistory[msg.sender][0]; // The most recent proof timestamp is always stored in the first position

// Check if the user has proved liveness within the last PROOF_PERIOD
if (currentTime < lastProofTime + PROOF_PERIOD) {
revert ProofWithinLast24Hours(lastProofTime);
}

// Shift the proof history and add the new timestamp
_updateProofHistory(msg.sender, currentTime);

// Emit an event to track the liveness proof
emit LivenessProved(msg.sender, currentTime);
}
Dismissed Show dismissed Hide dismissed

// Helper function to check if a user can prove liveness (returns true if PROOF_PERIOD has passed)
function canProveLiveness(address user) external view returns (bool) {
uint256 currentTime = block.timestamp;
return currentTime >= proofHistory[user][0] + PROOF_PERIOD;
}

// View function to return the liveness proof status for the last LAST_PERIODS_LENGTH periods (each PROOF_PERIOD long)
function getLastPeriodsStatus(address user) external view returns (bool[LAST_PERIODS_LENGTH] memory) {
uint256 currentTime = block.timestamp;
bool[LAST_PERIODS_LENGTH] memory proofStatus;
Fixed Show fixed Hide fixed

for (uint256 i = 0; i < LAST_PERIODS_LENGTH; i++) {
// Calculate the end of the period (going back i * PROOF_PERIOD)
uint256 periodEnd = currentTime - (i * PROOF_PERIOD);
uint256 periodStart = periodEnd - PROOF_PERIOD - 1;
// If the proof timestamp falls within this period, mark it as true
proofStatus[i] = hasProofedAt(user, periodStart, periodEnd);
}

return proofStatus;
}

function hasProofedAt(address user, uint256 periodStart, uint256 periodEnd) public view returns (bool) {
for (uint256 i = 0; i < LAST_PERIODS_LENGTH; i++) {
if (proofHistory[user][i] >= periodStart && proofHistory[user][i] < periodEnd) {
return true;
}
}
return false;
}
Dismissed Show dismissed Hide dismissed

function getProofHistory(address user) external view returns (uint256[LAST_PERIODS_LENGTH] memory) {
return proofHistory[user];
}

// Internal function to update the user's proof history by shifting timestamps and adding the new proof
function _updateProofHistory(address user, uint256 newProofTimestamp) internal {
// Shift the history to the right
for (uint256 i = LAST_PERIODS_LENGTH - 1; i > 0; i--) {
proofHistory[user][i] = proofHistory[user][i - 1];
}

// Add the new timestamp in the first position
proofHistory[user][0] = newProofTimestamp;
}
}
3 changes: 2 additions & 1 deletion packages/zevm-app-contracts/data/addresses.json
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@
"invitationManager": "0x3649C03C472B698213926543456E9c21081e529d",
"withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E",
"ZetaXP": "0x5c25b6f4D2b7a550a80561d3Bf274C953aC8be7d",
"InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3"
"InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3",
"ProofOfLiveness": "0x981EB6fD19717Faf293Fba0cBD05C6Ac97b8C808"
andresaiello marked this conversation as resolved.
Show resolved Hide resolved
},
"zeta_mainnet": {
"disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d",
33 changes: 33 additions & 0 deletions packages/zevm-app-contracts/scripts/proof-of-liveness/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { isProtocolNetworkName } from "@zetachain/protocol-contracts";
import { ethers, network } from "hardhat";

import { ProofOfLiveness__factory } from "../../typechain-types";
import { saveAddress } from "../address.helpers";
import { verifyContract } from "../explorer.helpers";

const networkName = network.name;

const deployProofOfLiveness = async () => {
if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name");

const ProofOfLivenessFactory = (await ethers.getContractFactory("ProofOfLiveness")) as ProofOfLiveness__factory;
const ProofOfLiveness = await ProofOfLivenessFactory.deploy();

await ProofOfLiveness.deployed();

console.log("ProofOfLiveness deployed to:", ProofOfLiveness.address);

saveAddress("ProofOfLiveness", ProofOfLiveness.address, networkName);

await verifyContract(ProofOfLiveness.address, []);
};

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

main().catch((error) => {
console.error(error);
process.exit(1);
});
andresaiello marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { ethers } from "hardhat";

import { ProofOfLiveness } from "../../typechain-types";

const PROOF_PERIOD = 24 * 60 * 60; // 24 hours in seconds

describe("Proof Of Liveness Contract test", () => {
let proofOfLiveness: ProofOfLiveness,
owner: SignerWithAddress,
signer: SignerWithAddress,
user: SignerWithAddress,
addrs: SignerWithAddress[];

beforeEach(async () => {
[owner, signer, user, ...addrs] = await ethers.getSigners();
const ProofOfLivenessFactory = await ethers.getContractFactory("ProofOfLiveness");

proofOfLiveness = await ProofOfLivenessFactory.deploy();

await proofOfLiveness.deployed();
});

it("Should proof", async () => {
const tx = await proofOfLiveness.proveLiveness();

const receipt = await tx.wait();
const blockTimestamp = (await ethers.provider.getBlock(receipt.blockNumber)).timestamp;

await expect(tx).to.emit(proofOfLiveness, "LivenessProved").withArgs(owner.address, blockTimestamp);
});

it("Should proof 5 times every 24 hours and return correct view values", async () => {
// Prove liveness 5 times
for (let i = 0; i < 5; i++) {
// Call the proveLiveness function
const tx = await proofOfLiveness.proveLiveness();
await tx.wait();

// Increase the time by 24 hours in the EVM
await ethers.provider.send("evm_increaseTime", [PROOF_PERIOD]);
await ethers.provider.send("evm_mine", []); // Mine a new block to apply the time change
}

// Now check the getLastPeriodsStatus for the owner
const periodsStatus = await proofOfLiveness.getLastPeriodsStatus(owner.address);

// We expect that all 5 periods should return true
expect(periodsStatus).to.deep.equal([true, true, true, true, true]);
});

it("Should proof 5 times every 24 hours and return correct view values if one day is missing", async () => {
// Prove liveness 5 times
for (let i = 0; i < 5; i++) {
// Call the proveLiveness function if day is not 3
if (i !== 3) {
const tx = await proofOfLiveness.proveLiveness();
await tx.wait();
}

// Increase the time by 24 hours in the EVM
await ethers.provider.send("evm_increaseTime", [PROOF_PERIOD]);
await ethers.provider.send("evm_mine", []); // Mine a new block to apply the time change
}

// Now check the getLastPeriodsStatus for the owner
const periodsStatus = await proofOfLiveness.getLastPeriodsStatus(owner.address);

// We expect that all 5 periods should return true but 3
expect(periodsStatus).to.deep.equal([true, false, true, true, true]);
});

it("Should proof view return if only one day was proof", async () => {
const tx = await proofOfLiveness.proveLiveness();
await tx.wait();
await ethers.provider.send("evm_mine", []); // Mine a new block to apply the time change

// Now check the getLastPeriodsStatus for the owner
const periodsStatus = await proofOfLiveness.getLastPeriodsStatus(owner.address);

expect(periodsStatus).to.deep.equal([true, false, false, false, false]);
});

it("Should revert if trying to prove twice in less than 24 hours", async () => {
// Prove liveness for the first time
await proofOfLiveness.proveLiveness();

const tx = proofOfLiveness.proveLiveness();

await expect(tx).to.be.revertedWith("ProofWithinLast24Hours");
});
});
Loading