-
Notifications
You must be signed in to change notification settings - Fork 13
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
tim-schultz
wants to merge
18
commits into
main
Choose a base branch
from
1839-merkle-payout
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
bf5fba7
feat(contracts): adding barebones staking contracts
d82d3d9
adding functionality, doing gas tests
0e77ca0
continuing gas tests
c152937
continuing gas tests
6f65797
continuing gas tests
908d49a
fixed test script
27a7d26
continuing gas tests
c9fd033
continuing gas tests
9cda7d9
Squashed commit of the following:
e71e1a1
added thorough tests for stake/slash/release, added slash nonce
e8a0dcc
chore: resolved conflicts
tim-schultz eab4f33
fix tests after merge
68c16be
fix: reset time manipulation after tests
tim-schultz 6d9e960
ownership checks
tim-schultz ec68956
feat: slash using merkle proof
tim-schultz 2e710f5
feat: withdraw based on merkle proof
tim-schultz b7ef2b7
WIP bad list merkle tree
tim-schultz e7623d3
fix proof params
tim-schultz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
// Used to permit unfreeze | ||
mapping(bytes32 => bool) public slashProofHashes; | ||
|
||
mapping(bytes32 => bool) public slashMerkleRoots; | ||
Comment on lines
+67
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) {} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.