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

1839 merkle payout #54

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions badList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format":"standard-v1","tree":["0x1b1fdea6d6a5ee187c3097d3bad61bcd9a27170e4cb6aebb1b24a03e6ad50e69","0x94a013fbf2bc50579e7a03a8aa876ed2653fe1e6d760390da6198a78a7ff5a99","0x3d55e655b268948132c11a40de6c000c4286b1c3bf409e14a8d4e67153f5f23e","0xf3c9e73362f19f1862e5084557b94ed67a049d129f056fb427eb0ba6851128ff","0x5f79db879af91d8a048e0939cbbd392e7992564fa04b5ffff814edeeaeed5e51","0xcc364facb706a7e2b9f4f8c31e68d2aaa1a8ee0cdfca73e1c7933f2fa4050b4e","0xe26c1b79a170caaad55fc0fd199819f5e46aea591ef83df932c638b317f81d40","0xbe7256b8b1584c49d47a9a0267704f22c1582aa2da6964f54a36c4acfd9ecd29","0x462e2414f21c7142c1362f783f76f22c55f7abc23d9f17dfe88b8c677f94cfac","0x656cf1dc90777feed0afa7593b28b5b604c742ced4d13f7bcf8584f0e7d1830d","0x227774b3c57a2f290ef708939678e77b8c0f65c009d32497610b26b021cef146","0x2d27e03c02d8e9f64286fc897e586e3d1685595b27f4a0286f150704932cea2e","0x1ab98fa97241523fdf1f772a633e0107190eda734a4a72574cabc906891b4170","0xf2d8af966361250fc02bba28ca978692434f705725b7b761630173c39ca27fd0","0xf112f7c045fd440dcb057a4bd4be38b3126b47b1659eaf9053d56fc0a51e20bf","0xef5f225309b09c1bcfd94fbdaeaeb34ed15496a9514239fd2b0d1c521bee3a30","0xe2e0f1ced9c27c486eab7fafb37be2ae62ceb3c0e9609f0e41dababe40214ea4","0xaefd3a9fa38f6a6e00be2867e5d5661736fdf1cbb06346466734dad61af7499a","0xabb45fa36a9a98a1b01bbe5a2fe3897acb5d748f2cb46b40147aa2741191c6ac","0xa7b42dd23b5e3fc86ad961c05d69a4ee7eaf5e98158bf6b6ea82b8f4155cdb00","0xa0165f79b9d65c93e44649f5e90dcd3d579f01c6d0a411d7b560b14f5e9ca2ee","0x9e621119d5797394a8ecc07dafe27e8776772a12a4c4c9dd1e600025acbf1be5","0x82da5ad1c7f11d007d4f286cb0a7dc601d14634b879154f0f61f03d1ce4ca0b8","0x6f5229bfb64fd205775623f238dbfc185b433eef86dc91bf672d4932043a9e82","0x6d53293066987d900cdd1d37d4d7956b0d28b4e19538e7a590b34c524ae2d0c3","0x65f5e5ecfb080abe7dafdb614d25e453e6f6ddd10d74d66396535eb833a82f61","0x21beff03bec6f1aeed05639ca75f8350bbb259b79393d3b6c090d00f7c652996"],"values":[{"value":["0x14dC79964da2C08b23698B3D3cc7Ca32193d9955","7"],"treeIndex":17},{"value":["0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f","8"],"treeIndex":19},{"value":["0xa0Ee7A142d267C1f36714E4a8F75612F20a79720","9"],"treeIndex":24},{"value":["0xBcd4042DE499D14e55001CcbB24a551F3b954096","10"],"treeIndex":14},{"value":["0x71bE63f3384f5fb98995898A86B02Fb2426c5788","11"],"treeIndex":16},{"value":["0xFABB0ac9d68B0B445fB7357272Ff202C5651694a","12"],"treeIndex":23},{"value":["0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec","13"],"treeIndex":25},{"value":["0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097","14"],"treeIndex":15},{"value":["0xcd3B766CCDd6AE721141F452C550Ca635964ce71","15"],"treeIndex":18},{"value":["0x2546BcD3c84621e976D8185a91A922aE77ECEc30","16"],"treeIndex":26},{"value":["0xbDA5747bFD65F08deb54cb465eB87D40e51B197E","17"],"treeIndex":22},{"value":["0xdD2FD4581271e230360230F9337D5c0430Bf44C0","18"],"treeIndex":13},{"value":["0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199","19"],"treeIndex":21},{"value":["0x09DB0a93B389bEF724429898f539AEB7ac2Dd55f","20"],"treeIndex":20}],"leafEncoding":["address","uint192"]}
376 changes: 376 additions & 0 deletions contracts/GitcoinIdentityStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,376 @@
// SPDX-License-Identifier: GPL
pragma solidity ^0.8.23;

import {Initializable, AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {GTC} from "./mocks/GTC.sol";

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

import "hardhat/console.sol";

/**
* @title GitcoinIdentityStaking
* @notice This contract is used to stake GTC on self/community identity
*/

// Control
contract GitcoinIdentityStaking is
Initializable,
UUPSUpgradeable,
AccessControlUpgradeable,
PausableUpgradeable
{
using EnumerableSet for EnumerableSet.AddressSet;

error SlashProofHashNotFound();
error SlashProofHashNotValid();
error SlashProofHashAlreadyUsed();
error FundsNotAvailableToRelease();
error MinimumBurnRoundDurationNotMet();
error AmountMustBeGreaterThanZero();
error UnlockTimeMustBeInTheFuture();
error CannotStakeOnSelf();
error FailedTransfer();
error InvalidLockTime();
error StakeIsLocked();
error NotOwnerOfStake();

bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE");
bytes32 public constant RELEASER_ROLE = keccak256("RELEASER_ROLE");

struct Stake {
uint192 amount;
uint64 unlockTime;
}

// TODO func selfStakeIdsLength(address) => uint256
mapping(address => uint256[]) public selfStakeIds;
mapping(address => mapping(address => uint256[])) public communityStakeIds;

mapping(uint256 stakeId => Stake) public stakes;
uint256 public stakeCount;

uint256 public currentSlashRound = 1;

uint64 public burnRoundMinimumDuration = 90 days;

uint256 public lastBurnTimestamp;

address public burnAddress;

mapping(uint256 round => uint192 amount) public totalSlashed;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will probably be needed when tracking the burn operations.


// Used to permit unfreeze
mapping(bytes32 => bool) public slashProofHashes;

mapping(bytes32 => bool) public slashMerkleRoots;
Comment on lines +67 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we could drop these 2 mappings

mapping(bytes32 => bool) public slashUserMerkleRoots;

mapping(bytes32 => uint192) public slashTotals;

bytes32 public slashMerkleRoot;
bytes32 public slashUserMerkleRoot;

event SelfStake(
uint256 indexed id,
address indexed staker,
uint192 amount,
uint64 unlockTime
);

event CommunityStake(
uint256 indexed id,
address indexed staker,
address indexed stakee,
uint192 amount,
uint64 unlockTime
);

event SelfStakeWithdrawn(
uint256 indexed id,
address indexed staker,
uint192 amount
);

event CommunityStakeWithdrawn(
uint256 indexed id,
address indexed staker,
address indexed stakee,
uint192 amount
);

event Slash(
address indexed slasher,
bytes32 slashProofHash,
uint192 slashAmount
);

event Burn(uint256 indexed round, uint192 amount);

GTC public gtc;

function initialize(
address gtcAddress,
address _burnAddress
) public initializer {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);

__AccessControl_init();
__Pausable_init();

gtc = GTC(gtcAddress);
burnAddress = _burnAddress;

lastBurnTimestamp = block.timestamp;
}

function selfStake(uint192 amount, uint64 duration) external {
// revert if amount is 0. Since this value is unsigned integer
if (amount == 0) {
revert AmountMustBeGreaterThanZero();
}

uint64 unlockTime = duration + uint64(block.timestamp);

if (
unlockTime < block.timestamp + 12 weeks ||
unlockTime > block.timestamp + 104 weeks
) {
revert InvalidLockTime();
}

uint256 stakeId = ++stakeCount;
stakes[stakeId].amount = amount;
stakes[stakeId].unlockTime = unlockTime;

selfStakeIds[msg.sender].push(stakeId);

if (!gtc.transferFrom(msg.sender, address(this), amount)) {
revert FailedTransfer();
}

emit SelfStake(stakeId, msg.sender, amount, unlockTime);
}

function ownerOfStake(address staker, uint value) public view returns (bool) {
uint[] memory currentStakes = selfStakeIds[staker];
for (uint i = 0; i < currentStakes.length; i++) {
if (currentStakes[i] == value) {
return true;
}
}
return false;
}

error InvalidWithdrawProof();

function withdrawSelfStake(
uint256 stakeId,
uint192 slashAmnt,
bytes32[] memory slashUserProof
) external {
if (stakes[stakeId].unlockTime < block.timestamp) {
revert StakeIsLocked();
}

bytes32 leaf = keccak256(
bytes.concat(keccak256(abi.encode(msg.sender, stakeId)))
);

console.logBytes32(leaf);
console.logBytes32(slashUserMerkleRoot);
if (!MerkleProof.verify(slashUserProof, slashUserMerkleRoot, leaf)) {
revert InvalidWithdrawProof();
}

// For good users this will always be zero. For bad users this will be the slash amount
uint192 amount = stakes[stakeId].amount - slashAmnt;

gtc.transfer(msg.sender, amount);

delete stakes[stakeId];

emit SelfStakeWithdrawn(stakeId, msg.sender, amount);
}

function communityStake(
address stakee,
uint192 amount,
uint64 duration
) external {
if (stakee == msg.sender) {
revert CannotStakeOnSelf();
}
if (amount == 0) {
revert AmountMustBeGreaterThanZero();
}

uint64 unlockTime = duration + uint64(block.timestamp);

if (
unlockTime < block.timestamp + 12 weeks ||
unlockTime > block.timestamp + 104 weeks
) {
revert InvalidLockTime();
}

uint256 stakeId = ++stakeCount;
stakes[stakeId].amount = amount;
stakes[stakeId].unlockTime = uint64(unlockTime);

communityStakeIds[msg.sender][stakee].push(stakeId);

if (!gtc.transferFrom(msg.sender, address(this), amount)) {
revert FailedTransfer();
}

emit CommunityStake(stakeId, msg.sender, stakee, amount, unlockTime);
}

function ownerOfCommunityStake(
address staker,
address stakee,
uint value
) public view returns (bool) {
uint[] memory currentStakes = communityStakeIds[staker][stakee];
for (uint i = 0; i < currentStakes.length; i++) {
if (currentStakes[i] == value) {
return true;
}
}
return false;
}

function withdrawCommunityStake(address stakee, uint256 stakeId) external {
if (!ownerOfCommunityStake(msg.sender, stakee, stakeId)) {
revert NotOwnerOfStake();
}

if (stakes[stakeId].unlockTime < block.timestamp) {
revert StakeIsLocked();
}

uint192 amount = stakes[stakeId].amount;

delete stakes[stakeId];

gtc.transfer(msg.sender, amount);

emit SelfStakeWithdrawn(stakeId, msg.sender, amount);
}

error InvalidSlashProof();

function slash(
bytes32 currentSlashMerkleRoot,
bytes32 currentSlashUserMerkleRoot,
uint192 slashTotal,
bytes32[] memory slashTotalProof
) external onlyRole(SLASHER_ROLE) {
if (slashMerkleRoots[currentSlashMerkleRoot]) {
revert SlashProofHashAlreadyUsed();
}

if (slashUserMerkleRoots[slashUserMerkleRoot]) {
revert SlashProofHashAlreadyUsed();
}

bytes32 leaf = keccak256(
bytes.concat(keccak256(abi.encode(address(0), slashTotal, uint192(0))))
);
if (!MerkleProof.verify(slashTotalProof, currentSlashMerkleRoot, leaf)) {
revert InvalidSlashProof();
}

slashUserMerkleRoots[slashUserMerkleRoot] = true;
slashMerkleRoots[currentSlashMerkleRoot] = true;
slashTotals[currentSlashMerkleRoot] = slashTotal;
slashMerkleRoot = currentSlashMerkleRoot;
slashUserMerkleRoot = currentSlashUserMerkleRoot;

emit Slash(msg.sender, slashMerkleRoot, slashTotal);
}

// Burn last round and start next round (locking this round)
//
// Rounds don't matter for staking, this is just to
// ensure that slashes are aged before being burned
//
// On each call...
// - the current round contains all the slashes younger than the last
// burn (a minimum of the round mimimum duration, 0-90 days)
// - the previous round contains all the non-released slashes older
// than this (at least 90 days), and so it is burned
// - the current round becomes the previous round, and a new round
// is initiated
// On the very first call, nothing will be burned
function burn() external {
if (block.timestamp - lastBurnTimestamp < burnRoundMinimumDuration) {
revert MinimumBurnRoundDurationNotMet();
}

uint192 amountToBurn = totalSlashed[currentSlashRound - 1];

if (amountToBurn > 0) {
if (!gtc.transfer(burnAddress, amountToBurn)) {
revert FailedTransfer();
}
}

emit Burn(currentSlashRound - 1, amountToBurn);

currentSlashRound++;
lastBurnTimestamp = block.timestamp;
}

struct SlashMember {
address account;
uint192 amount;
}

// The nonce is used in the proof in case we need to
// do the exact same slash multiple times
function release(
SlashMember[] calldata slashMembers,
uint256 slashMemberIndex,
uint192 amountToRelease,
bytes32 slashProofHash,
bytes32 nonce,
bytes32 newNonce
) external onlyRole(RELEASER_ROLE) {
if (!slashProofHashes[slashProofHash]) {
revert SlashProofHashNotFound();
}
if (keccak256(abi.encode(slashMembers, nonce)) != slashProofHash) {
revert SlashProofHashNotValid();
}

SlashMember memory slashMemberToRelease = slashMembers[slashMemberIndex];

if (amountToRelease > slashMemberToRelease.amount) {
revert FundsNotAvailableToRelease();
}

SlashMember[] memory newSlashMembers = slashMembers;

newSlashMembers[slashMemberIndex].amount -= amountToRelease;

bytes32 newSlashProofHash = keccak256(
abi.encode(newSlashMembers, newNonce)
);

slashProofHashes[slashProofHash] = false;
slashProofHashes[newSlashProofHash] = true;

if (!gtc.transfer(slashMemberToRelease.account, amountToRelease)) {
revert FailedTransfer();
}
}

function _authorizeUpgrade(
address
) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
}
Loading