Skip to content

Commit

Permalink
feat: Create proof of liveness smart contract (#190)
Browse files Browse the repository at this point in the history
* Create proof of liveness smart contract

* refactor to constants

* add test

* remove unused code

* deploy to mainnet
  • Loading branch information
andresaiello authored Sep 26, 2024
1 parent 9e35108 commit b7ae18a
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 2 deletions.
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);
}

// 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;

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;
}

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;
}
}
6 changes: 4 additions & 2 deletions packages/zevm-app-contracts/data/addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"invitationManager": "0x3649C03C472B698213926543456E9c21081e529d",
"withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E",
"ZetaXP": "0x5c25b6f4D2b7a550a80561d3Bf274C953aC8be7d",
"InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3"
"InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3",
"ProofOfLiveness": "0x981EB6fD19717Faf293Fba0cBD05C6Ac97b8C808"
},
"zeta_mainnet": {
"disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d",
Expand All @@ -18,7 +19,8 @@
"invitationManager": "0x3649C03C472B698213926543456E9c21081e529d",
"withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E",
"ZetaXP": "0x9A4e8bB5FFD8088ecF1DdE823e97Be8080BD38cb",
"InstantRewards": "0x018412ec1D5bBb864eAe0A4BECaa683052890238"
"InstantRewards": "0x018412ec1D5bBb864eAe0A4BECaa683052890238",
"ProofOfLiveness": "0x327c9837B183e69C522a30E6f91A42c86e057432"
}
}
}
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);
});
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");
});
});

0 comments on commit b7ae18a

Please sign in to comment.