From 9cda7d9403dcb53743ec88b2b9e4734880224311 Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Wed, 20 Dec 2023 11:06:30 -0700 Subject: [PATCH] Squashed commit of the following: commit 9d15170c27cb65ad4df7d091c29a0a5f1dd8f8cd Author: Lucian Hymer Date: Wed Dec 20 11:04:13 2023 -0700 mob next [ci-skip] [ci skip] [skip ci] lastFile:test/GitcoinIdentityStaking.ts commit 63a0c7b416da81fdee4cd2b9377e06b992672eee Author: Lucian Hymer Date: Wed Dec 20 09:19:10 2023 -0700 mob start [ci-skip] [ci skip] [skip ci] --- contracts/GitcoinIdentityStaking.sol | 1819 +------------------------- test/GitcoinIdentityStaking.ts | 437 ++++--- 2 files changed, 253 insertions(+), 2003 deletions(-) diff --git a/contracts/GitcoinIdentityStaking.sol b/contracts/GitcoinIdentityStaking.sol index de429e4..bd6797f 100644 --- a/contracts/GitcoinIdentityStaking.sol +++ b/contracts/GitcoinIdentityStaking.sol @@ -24,1513 +24,15 @@ contract GitcoinIdentityStaking is using EnumerableSet for EnumerableSet.AddressSet; bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint256 amount; - uint64 unlockTime; - } - - mapping(address => uint256[]) public selfStakeIds; - mapping(address => mapping(address => uint256[])) public communityStakeIds; - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - mapping(uint256 stakeId => Stake) public stakes; - uint256 public stakeCount; - - uint256 public currentBurnRound = 1; - - mapping(uint256 round => uint256 amount) public totalSlashed; - - // Used to permit unfreeze - mapping(uint256 => bool) public slashProofHashes; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashProofHash - ); - - event Burn(uint256 indexed round, uint256 amount); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint256 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - gtc.transferFrom(msg.sender, address(this), amount); - - selfStakeIds[msg.sender].push(stakeId); - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint256 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - communityStakeIds[msg.sender][stakee].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), amount); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash( - address[] calldata accounts, - uint64 slashedPercent, - uint256 slashProofHash - ) external { - uint256 numAccounts = accounts.length; - - for (uint256 i = 0; i < numAccounts; i++) { - address account = accounts[i]; - uint256 numSelfStakes = selfStakeIds[account].length; - for (uint256 j = 0; j < numSelfStakes; j++) { - uint256 stakeId = selfStakeIds[account][j]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - stakes[stakeId].amount -= slashedAmount; - } - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 j = 0; j < numStakedOnByMe; j++) { - address stakee = communityStakeesForAddress[account].at(j); - uint256 numStakes = communityStakeIds[account][stakee].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[account][stakee][k]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - totalSlashed[currentBurnRound] += slashedAmount; - stakes[stakeId].amount -= slashedAmount; - } - } - - uint256 numOthersStakingMe = communityStakersForAddress[account].length(); - for (uint256 j = 0; j < numOthersStakingMe; j++) { - address staker = communityStakersForAddress[account].at(j); - uint256 numStakes = communityStakeIds[staker][account].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[staker][account][k]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - totalSlashed[currentBurnRound] += slashedAmount; - stakes[stakeId].amount -= slashedAmount; - } - } - } - - slashProofHashes[slashProofHash] = true; - - emit SlashEvent(msg.sender, slashedPercent, slashProofHash); - } - - // Burn last round, start next round (locking this round) - // Rounds don't matter, this is just to time the slashing - function burn() external { - // TODO check that threshold has passed since last burn, save this timestamp - - gtc.transfer(address(1), totalSlashed[currentBurnRound - 1]); - - emit Burn(currentBurnRound - 1, totalSlashed[currentBurnRound - 1]); - - currentBurnRound++; - } - - // Pseudocode - // function release(address, amount, proof, slashProofHash) external { - // require(msg.sender has Releaser role) - // require(slashProofHashes[slashProofHash], "Slash proof hash not found"); - // checkProof(proof, slashProofHash); // Probably merkle membership? - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Tracking slashes explicitly in contract -contract GitcoinIdentityStaking2 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint256 amount; - uint64 unlockTime; - } - - struct Slash { - uint256 amount; - uint64 time; - address[] accounts; - } - - mapping(address => uint256[]) public selfStakeIds; - mapping(address => mapping(address => uint256[])) public communityStakeIds; - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - mapping(uint256 stakeId => Stake) public stakes; - mapping(uint256 slashId => Slash) public slashes; - uint256 public stakeCount; - uint256 public slashCount; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashCount - ); - - event Burn(address indexed burner); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint256 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - selfStakeIds[msg.sender].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), amount); - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint256 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - communityStakeIds[msg.sender][stakee].push(stakeId); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - gtc.transferFrom(msg.sender, address(this), amount); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash(address[] calldata accounts, uint64 slashedPercent) external { - uint256 totalSlashed = 0; - uint256 numAccounts = accounts.length; - for (uint256 i = 0; i < numAccounts; i++) { - address account = accounts[i]; - uint256 selfStakeCount = selfStakeIds[account].length; - for (uint256 j = 0; j < selfStakeCount; j++) { - uint256 stakeId = selfStakeIds[account][j]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / 100; - stakes[stakeId].amount -= slashedAmount; - totalSlashed += slashedAmount; - } - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 j = 0; j < numStakedOnByMe; j++) { - address stakee = communityStakeesForAddress[account].at(j); - uint256 numStakes = communityStakeIds[account][stakee].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[account][stakee][k]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - stakes[stakeId].amount -= slashedAmount; - totalSlashed += slashedAmount; - } - } - - uint256 numOthersStakingMe = communityStakersForAddress[account].length(); - for (uint256 j = 0; j < numOthersStakingMe; j++) { - address staker = communityStakersForAddress[account].at(j); - uint256 numStakes = communityStakeIds[staker][account].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[staker][account][k]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - stakes[stakeId].amount -= slashedAmount; - totalSlashed += slashedAmount; - } - } - } - - slashes[slashCount].amount = totalSlashed; - slashes[slashCount].time = uint64(block.timestamp); - slashes[slashCount].accounts = accounts; - - slashCount++; - - emit SlashEvent(msg.sender, slashedPercent, slashCount); - } - - function burn(uint256[] calldata slashIds) external { - uint256 amountToBurn = 0; - - uint256 numIds = slashIds.length; - for (uint256 i = 0; i < numIds; i++) { - uint256 slashId = slashIds[i]; - if (slashes[slashId].time > 0) { - amountToBurn += slashes[slashId].amount; - } - - delete slashes[slashId]; - } - - gtc.transfer(address(1), uint256(amountToBurn)); - - emit Burn(msg.sender); - } - - // Pseudocode - // function release(address, amount, slashId) external { - // require(msg.sender has Releaser role) - // require(slashed[slashId] exists) - // require(slashes[slashId].accounts.contains(address)) - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Track slashes explicitly, move gas usage to burn -contract GitcoinIdentityStaking3 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint256 amount; - uint64 unlockTime; - } - - struct Slash { - uint64 percent; - uint64 time; - address[] accounts; - } - - mapping(address => uint256[]) public selfStakeIds; - mapping(address => mapping(address => uint256[])) public communityStakeIds; - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - mapping(uint256 stakeId => Stake) public stakes; - mapping(uint256 slashId => Slash) public slashes; - uint256 public stakeCount; - uint256 public slashCount; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashCount - ); - - event Burn(address indexed burner); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - // For this one, getting self and community stake totals is - // WAY more complex, all the slashing calculations from the - // `burn` function must be executed for **any** read. But - // if we don't need to ever read this data as part of a - // transaction, that's fine - - function selfStake(uint256 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - selfStakeIds[msg.sender].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), amount); - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint256 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - communityStakeIds[msg.sender][stakee].push(stakeId); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - gtc.transferFrom(msg.sender, address(this), amount); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash(address[] calldata accounts, uint64 slashedPercent) external { - slashes[slashCount].percent = slashedPercent; - slashes[slashCount].time = uint64(block.timestamp); - slashes[slashCount].accounts = accounts; - - emit SlashEvent(msg.sender, slashedPercent, slashCount); - - slashCount++; - } - - function withdraw() external {} - - // This kind of sucks because if there are new community stakers - // after your slash but before the burn, they'll be slashed too - // So we need to store more info or something - function burn(uint256[] calldata slashIds) external { - uint256 amountToBurn = 0; - - uint256 numIds = slashIds.length; - for (uint256 i = 0; i < numIds; i++) { - uint64 slashedPercent = slashes[slashIds[i]].percent; - - for (uint256 j = 0; j < slashes[slashIds[i]].accounts.length; j++) { - address account = slashes[slashIds[i]].accounts[j]; - uint256 selfStakeCount = selfStakeIds[account].length; - for (uint256 k = 0; k < selfStakeCount; k++) { - uint256 stakeId = selfStakeIds[account][k]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - stakes[stakeId].amount -= slashedAmount; - amountToBurn += slashedAmount; - } - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 k = 0; k < numStakedOnByMe; k++) { - address stakee = communityStakeesForAddress[account].at(k); - uint256 numStakes = communityStakeIds[account][stakee].length; - for (uint256 l = 0; l < numStakes; l++) { - uint256 stakeId = communityStakeIds[account][stakee][l]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - stakes[stakeId].amount -= slashedAmount; - amountToBurn += slashedAmount; - } - } - - uint256 numOthersStakingMe = communityStakersForAddress[account] - .length(); - for (uint256 k = 0; k < numOthersStakingMe; k++) { - address staker = communityStakersForAddress[account].at(k); - uint256 numStakes = communityStakeIds[staker][account].length; - for (uint256 l = 0; l < numStakes; l++) { - uint256 stakeId = communityStakeIds[staker][account][l]; - uint256 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - stakes[stakeId].amount -= slashedAmount; - amountToBurn += slashedAmount; - } - } - } - - delete slashes[slashIds[i]]; - } - - gtc.transfer(address(1), amountToBurn); - - emit Burn(msg.sender); - } - - // Pseudocode - // function release(address, amount, slashId) external { - // require(msg.sender has Releaser role) - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Only one stake per stakee:staker, lock period -contract GitcoinIdentityStaking4 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - mapping(address => uint256) public selfStakeAmount; - mapping(address => mapping(address => uint256)) public communityStakeAmounts; - - mapping(address staker => mapping(address stakee => bool)) - public unlockPending; - - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - uint256 public currentBurnRound = 1; - - mapping(uint256 round => uint256 amount) public totalSlashed; - - // Used to permit unfreeze - mapping(uint256 => bool) public slashProofHashes; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashProofHash - ); - - event Burn(uint256 indexed round, uint256 amount); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint256 amount) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockPending[msg.sender][msg.sender] == false, "Unlock pending"); - - gtc.transferFrom(msg.sender, address(this), amount); - - selfStakeAmount[msg.sender] += amount; - - emit SelfStake(msg.sender, amount); - } - - function communityStake(address stakee, uint256 amount) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockPending[msg.sender][stakee] == false, "Unlock pending"); - - communityStakeAmounts[msg.sender][stakee] += amount; - - gtc.transferFrom(msg.sender, address(this), amount); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash( - address[] calldata accounts, - uint64 slashedPercent, - uint256 slashProofHash - ) external { - uint256 numAccounts = accounts.length; - - for (uint256 i = 0; i < numAccounts; i++) { - address account = accounts[i]; - - uint256 selfSlashedAmount = (slashedPercent * selfStakeAmount[account]) / - 100; - totalSlashed[currentBurnRound] += selfSlashedAmount; - selfStakeAmount[account] -= selfSlashedAmount; - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 j = 0; j < numStakedOnByMe; j++) { - address stakee = communityStakeesForAddress[account].at(j); - uint256 slashedAmount = (slashedPercent * - communityStakeAmounts[account][stakee]) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - communityStakeAmounts[account][stakee] -= slashedAmount; - } - - uint256 numOthersStakingMe = communityStakersForAddress[account].length(); - for (uint256 j = 0; j < numOthersStakingMe; j++) { - address staker = communityStakersForAddress[account].at(j); - uint256 slashedAmount = (slashedPercent * - communityStakeAmounts[staker][account]) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - communityStakeAmounts[staker][account] -= slashedAmount; - } - } - - slashProofHashes[slashProofHash] = true; - - emit SlashEvent(msg.sender, slashedPercent, slashProofHash); - } - - // Burn last round, start next round (locking this round) - // Rounds don't matter, this is just to time the slashing - function burn() external { - // TODO check that threshold has passed since last burn, save this timestamp - - gtc.transfer(address(1), totalSlashed[currentBurnRound - 1]); - - currentBurnRound++; - - emit Burn(currentBurnRound - 1, totalSlashed[currentBurnRound - 1]); - } - - // Pseudocode - // function release(address, amount, proof, slashProofHash) external { - // require(msg.sender has Releaser role) - // require(slashProofHashes[slashProofHash], "Slash proof hash not found"); - // checkProof(proof, slashProofHash); // Probably merkle membership? - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Only one stake per stakee:staker -contract GitcoinIdentityStaking5 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint256 amount; - uint64 unlockTime; - } - - mapping(address => Stake) public selfStakeAmount; - mapping(address => mapping(address => Stake)) public communityStakeAmounts; - - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - uint256 public currentBurnRound = 1; - - mapping(uint256 round => uint256 amount) public totalSlashed; - - // Used to permit unfreeze - mapping(uint256 => bool) public slashProofHashes; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashProofHash - ); - - event Burn(uint256 indexed round, uint256 amount); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint256 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - - gtc.transferFrom(msg.sender, address(this), amount); - - selfStakeAmount[msg.sender].amount += amount; - selfStakeAmount[msg.sender].unlockTime = unlockTime; - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint256 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - - communityStakeAmounts[msg.sender][stakee].amount += amount; - communityStakeAmounts[msg.sender][stakee].unlockTime = unlockTime; - - gtc.transferFrom(msg.sender, address(this), amount); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash( - address[] calldata accounts, - uint64 slashedPercent, - uint256 slashProofHash - ) external { - uint256 numAccounts = accounts.length; - - for (uint256 i = 0; i < numAccounts; i++) { - address account = accounts[i]; - - uint256 selfSlashedAmount = (slashedPercent * - selfStakeAmount[account].amount) / 100; - totalSlashed[currentBurnRound] += selfSlashedAmount; - selfStakeAmount[account].amount -= selfSlashedAmount; - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 j = 0; j < numStakedOnByMe; j++) { - address stakee = communityStakeesForAddress[account].at(j); - uint256 slashedAmount = (slashedPercent * - communityStakeAmounts[account][stakee].amount) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - communityStakeAmounts[account][stakee].amount -= slashedAmount; - } - - uint256 numOthersStakingMe = communityStakersForAddress[account].length(); - for (uint256 j = 0; j < numOthersStakingMe; j++) { - address staker = communityStakersForAddress[account].at(j); - uint256 slashedAmount = (slashedPercent * - communityStakeAmounts[staker][account].amount) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - communityStakeAmounts[staker][account].amount -= slashedAmount; - } - } - - slashProofHashes[slashProofHash] = true; - - emit SlashEvent(msg.sender, slashedPercent, slashProofHash); - } - - // Burn last round, start next round (locking this round) - // Rounds don't matter, this is just to time the slashing - function burn() external { - // TODO check that threshold has passed since last burn, save this timestamp - - gtc.transfer(address(1), totalSlashed[currentBurnRound - 1]); - - emit Burn(currentBurnRound - 1, totalSlashed[currentBurnRound - 1]); - - currentBurnRound++; - } - - // Pseudocode - // function release(address, amount, proof, slashProofHash) external { - // require(msg.sender has Releaser role) - // require(slashProofHashes[slashProofHash], "Slash proof hash not found"); - // checkProof(proof, slashProofHash); // Probably merkle membership? - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Only one stake per stakee:staker, store amount as uint192 -contract GitcoinIdentityStaking6 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint192 amount; - uint64 unlockTime; - } - - mapping(address => Stake) public selfStakeAmount; - mapping(address => mapping(address => Stake)) public communityStakeAmounts; - - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - uint256 public currentBurnRound = 1; - - mapping(uint256 round => uint192 amount) public totalSlashed; - - // Used to permit unfreeze - mapping(uint256 => bool) public slashProofHashes; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashProofHash - ); - - event SlashAddresses(address indexed slasher); - - event Burn(uint256 indexed round, uint256 amount); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint192 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - selfStakeAmount[msg.sender].amount += amount; - selfStakeAmount[msg.sender].unlockTime = unlockTime; - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint192 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - - communityStakeAmounts[msg.sender][stakee].amount += amount; - communityStakeAmounts[msg.sender][stakee].unlockTime = unlockTime; - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash( - address[] calldata accounts, - uint64 slashedPercent, - uint256 slashProofHash - ) external { - uint256 numAccounts = accounts.length; - - for (uint256 i = 0; i < numAccounts; i++) { - address account = accounts[i]; - - uint192 selfSlashedAmount = (slashedPercent * - selfStakeAmount[account].amount) / 100; - totalSlashed[currentBurnRound] += selfSlashedAmount; - selfStakeAmount[account].amount -= selfSlashedAmount; - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 j = 0; j < numStakedOnByMe; j++) { - address stakee = communityStakeesForAddress[account].at(j); - uint192 slashedAmount = (slashedPercent * - communityStakeAmounts[account][stakee].amount) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - communityStakeAmounts[account][stakee].amount -= slashedAmount; - } - - uint256 numOthersStakingMe = communityStakersForAddress[account].length(); - for (uint256 j = 0; j < numOthersStakingMe; j++) { - address staker = communityStakersForAddress[account].at(j); - uint192 slashedAmount = (slashedPercent * - communityStakeAmounts[staker][account].amount) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - communityStakeAmounts[staker][account].amount -= slashedAmount; - } - } - - slashProofHashes[slashProofHash] = true; - - emit SlashEvent(msg.sender, slashedPercent, slashProofHash); - } - - // Burn last round, start next round (locking this round) - // Rounds don't matter, this is just to time the slashing - function burn() external { - // TODO check that threshold has passed since last burn, save this timestamp - - gtc.transfer(address(1), uint256(totalSlashed[currentBurnRound - 1])); - - emit Burn( - currentBurnRound - 1, - uint256(totalSlashed[currentBurnRound - 1]) - ); - - currentBurnRound++; - } - - // Pseudocode - // function release(address, amount, proof, slashProofHash) external { - // require(msg.sender has Releaser role) - // require(slashProofHashes[slashProofHash], "Slash proof hash not found"); - // checkProof(proof, slashProofHash); // Probably merkle membership? - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// store amount as uint192 -contract GitcoinIdentityStaking7 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint192 amount; - uint64 unlockTime; - } - - mapping(address => uint256[]) public selfStakeIds; - mapping(address => mapping(address => uint256[])) public communityStakeIds; - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - mapping(uint256 stakeId => Stake) public stakes; - uint256 public stakeCount; - - uint256 public currentBurnRound = 1; - - mapping(uint256 round => uint192 amount) public totalSlashed; - - // Used to permit unfreeze - mapping(uint256 => bool) public slashProofHashes; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashProofHash - ); - - event Burn(uint256 indexed round, uint256 amount); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint192 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - gtc.transferFrom(msg.sender, address(this), amount); - - selfStakeIds[msg.sender].push(stakeId); - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint192 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - communityStakeIds[msg.sender][stakee].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), amount); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash( - address[] calldata accounts, - uint64 slashedPercent, - uint256 slashProofHash - ) external { - uint256 numAccounts = accounts.length; - - for (uint256 i = 0; i < numAccounts; i++) { - address account = accounts[i]; - uint256 numSelfStakes = selfStakeIds[account].length; - for (uint256 j = 0; j < numSelfStakes; j++) { - uint256 stakeId = selfStakeIds[account][j]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - stakes[stakeId].amount -= slashedAmount; - } - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 j = 0; j < numStakedOnByMe; j++) { - address stakee = communityStakeesForAddress[account].at(j); - uint256 numStakes = communityStakeIds[account][stakee].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[account][stakee][k]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - totalSlashed[currentBurnRound] += slashedAmount; - stakes[stakeId].amount -= slashedAmount; - } - } - - uint256 numOthersStakingMe = communityStakersForAddress[account].length(); - for (uint256 j = 0; j < numOthersStakingMe; j++) { - address staker = communityStakersForAddress[account].at(j); - uint256 numStakes = communityStakeIds[staker][account].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[staker][account][k]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - totalSlashed[currentBurnRound] += slashedAmount; - stakes[stakeId].amount -= slashedAmount; - } - } - } - - slashProofHashes[slashProofHash] = true; - - emit SlashEvent(msg.sender, slashedPercent, slashProofHash); - } - - // Burn last round, start next round (locking this round) - // Rounds don't matter, this is just to time the slashing - function burn() external { - // TODO check that threshold has passed since last burn, save this timestamp - - gtc.transfer(address(1), totalSlashed[currentBurnRound - 1]); - - emit Burn(currentBurnRound - 1, totalSlashed[currentBurnRound - 1]); - - currentBurnRound++; - } - - // Pseudocode - // function release(address, amount, proof, slashProofHash) external { - // require(msg.sender has Releaser role) - // require(slashProofHashes[slashProofHash], "Slash proof hash not found"); - // checkProof(proof, slashProofHash); // Probably merkle membership? - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Tracking slashes explicitly in contract -// store amount as uint192 -contract GitcoinIdentityStaking8 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint192 amount; - uint64 unlockTime; - } - - struct Slash { - uint192 amount; - uint64 time; - address[] accounts; - } - - mapping(address => uint256[]) public selfStakeIds; - mapping(address => mapping(address => uint256[])) public communityStakeIds; - mapping(address => EnumerableSet.AddressSet) - private communityStakersForAddress; - mapping(address => EnumerableSet.AddressSet) - private communityStakeesForAddress; - - mapping(uint256 stakeId => Stake) public stakes; - mapping(uint256 slashId => Slash) public slashes; - uint256 public stakeCount; - uint256 public slashCount; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashCount - ); - - event Burn(address indexed burner); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint192 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - selfStakeIds[msg.sender].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint192 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - communityStakeIds[msg.sender][stakee].push(stakeId); - - communityStakeesForAddress[msg.sender].add(stakee); - communityStakersForAddress[stakee].add(msg.sender); - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash(address[] calldata accounts, uint64 slashedPercent) external { - uint192 totalSlashed = 0; - uint256 numAccounts = accounts.length; - for (uint256 i = 0; i < numAccounts; i++) { - address account = accounts[i]; - uint256 selfStakeCount = selfStakeIds[account].length; - for (uint256 j = 0; j < selfStakeCount; j++) { - uint256 stakeId = selfStakeIds[account][j]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / 100; - stakes[stakeId].amount -= slashedAmount; - totalSlashed += slashedAmount; - } - - uint256 numStakedOnByMe = communityStakeesForAddress[account].length(); - for (uint256 j = 0; j < numStakedOnByMe; j++) { - address stakee = communityStakeesForAddress[account].at(j); - uint256 numStakes = communityStakeIds[account][stakee].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[account][stakee][k]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - stakes[stakeId].amount -= slashedAmount; - totalSlashed += slashedAmount; - } - } - - uint256 numOthersStakingMe = communityStakersForAddress[account].length(); - for (uint256 j = 0; j < numOthersStakingMe; j++) { - address staker = communityStakersForAddress[account].at(j); - uint256 numStakes = communityStakeIds[staker][account].length; - for (uint256 k = 0; k < numStakes; k++) { - uint256 stakeId = communityStakeIds[staker][account][k]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / - 100; - stakes[stakeId].amount -= slashedAmount; - totalSlashed += slashedAmount; - } - } - } - - slashes[slashCount].amount = totalSlashed; - slashes[slashCount].time = uint64(block.timestamp); - slashes[slashCount].accounts = accounts; - - slashCount++; - - emit SlashEvent(msg.sender, slashedPercent, slashCount); - } - - function burn(uint256[] calldata slashIds) external { - uint192 amountToBurn = 0; - - uint256 numIds = slashIds.length; - for (uint256 i = 0; i < numIds; i++) { - uint256 slashId = slashIds[i]; - if (slashes[slashId].time > 0) { - amountToBurn += slashes[slashId].amount; - delete slashes[slashId]; - } - } - - gtc.transfer(address(1), uint256(amountToBurn)); - - emit Burn(msg.sender); - } - - // Pseudocode - // function release(address, amount, slashId) external { - // require(msg.sender has Releaser role) - // require(slashed[slashId] exists) - // require(slashes[slashId].accounts.contains(address)) - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Track slashes explicitly, move gas usage to burn -// use uint192 for amount -// use stakeIds for slashing -contract GitcoinIdentityStaking10 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint192 amount; - uint64 unlockTime; - } - - struct Slash { - uint64 percent; - uint64 time; - uint256[] stakeIds; - } - - mapping(address => uint256[]) public selfStakeIds; - mapping(address => mapping(address => uint256[])) public communityStakeIds; - - mapping(uint256 stakeId => Stake) public stakes; - mapping(uint256 slashId => Slash) public slashes; - uint256 public stakeCount; - uint256 public slashCount; - - event SelfStake(address indexed staker, uint192 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint192 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashCount - ); - - event Burn(address indexed burner); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - // For this one, getting self and community stake totals is - // WAY more complex, all the slashing calculations from the - // `burn` function must be executed for **any** read. But - // if we don't need to ever read this data as part of a - // transaction, that's fine - - function selfStake(uint192 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - selfStakeIds[msg.sender].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), amount); - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint192 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - communityStakeIds[msg.sender][stakee].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), amount); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash(uint256[] calldata stakeIds, uint64 slashedPercent) external { - slashes[slashCount].percent = slashedPercent; - slashes[slashCount].time = uint64(block.timestamp); - slashes[slashCount].stakeIds = stakeIds; - - // include hash of stakeIds in event? - emit SlashEvent(msg.sender, slashedPercent, slashCount); - - slashCount++; - } - - function withdraw() external {} - - function burn(uint256[] calldata slashIds) external { - uint192 amountToBurn = 0; - - uint256 numIds = slashIds.length; - for (uint256 i = 0; i < numIds; i++) { - uint64 slashedPercent = slashes[slashIds[i]].percent; - - uint256 numStakes = slashes[slashIds[i]].stakeIds.length; - - for (uint256 j = 0; j < numStakes; j++) { - uint256 stakeId = slashes[slashIds[i]].stakeIds[j]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / 100; - stakes[stakeId].amount -= slashedAmount; - amountToBurn += slashedAmount; - } - - delete slashes[slashIds[i]]; - } - - gtc.transfer(address(1), uint256(amountToBurn)); - - emit Burn(msg.sender); - } - - // Pseudocode - // function release(address, amount, slashId) external { - // require(msg.sender has Releaser role) - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// store amount as uint192 -// slash with stake IDs -contract GitcoinIdentityStaking11 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_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; @@ -1542,19 +44,20 @@ contract GitcoinIdentityStaking11 is mapping(uint256 round => uint192 amount) public totalSlashed; // Used to permit unfreeze - mapping(uint256 => bool) public slashProofHashes; + mapping(bytes32 => bool) public slashProofHashes; event SelfStake(address indexed staker, uint192 amount); + event CommunityStake( address indexed staker, address indexed stakee, uint192 amount ); - event SlashEvent( + event Slash( address indexed slasher, uint64 slashedPercent, - uint256 slashProofHash + bytes32 slashProofHash ); event Burn(uint256 indexed round, uint192 amount); @@ -1607,8 +110,8 @@ contract GitcoinIdentityStaking11 is function slash( uint256[] calldata stakeIds, uint64 slashedPercent, - uint256 slashProofHash - ) external { + bytes32 slashProofHash + ) external onlyRole(SLASHER_ROLE) { uint256 numStakes = stakeIds.length; for (uint256 i = 0; i < numStakes; i++) { @@ -1620,12 +123,12 @@ contract GitcoinIdentityStaking11 is slashProofHashes[slashProofHash] = true; - emit SlashEvent(msg.sender, slashedPercent, slashProofHash); + emit Slash(msg.sender, slashedPercent, slashProofHash); } // Burn last round, start next round (locking this round) // Rounds don't matter, this is just to time the slashing - function burn() external { + function burn() external onlyRole(BURNER_ROLE) { // TODO check that threshold has passed since last burn, save this timestamp gtc.transfer(address(1), totalSlashed[currentBurnRound - 1]); @@ -1635,301 +138,37 @@ contract GitcoinIdentityStaking11 is currentBurnRound++; } - // Pseudocode - // function release(address, amount, proof, slashProofHash) external { - // require(msg.sender has Releaser role) - // require(slashProofHashes[slashProofHash], "Slash proof hash not found"); - // checkProof(proof, slashProofHash); // Probably merkle membership? - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Only one stake per stakee:staker, store amount as uint192 -// slash with staker[], stakee[], percent -contract GitcoinIdentityStaking12 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { + struct SlashMember { + address account; uint192 amount; - uint64 unlockTime; - } - - mapping(address => Stake) public selfStakeAmount; - mapping(address => mapping(address => Stake)) public communityStakeAmounts; - - uint256 public currentBurnRound = 1; - - mapping(uint256 round => uint192 amount) public totalSlashed; - - // Used to permit unfreeze - mapping(uint256 => bool) public slashProofHashes; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashProofHash - ); - - event SlashAddresses(address indexed slasher); - - event Burn(uint256 indexed round, uint256 amount); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint192 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - selfStakeAmount[msg.sender].amount += amount; - selfStakeAmount[msg.sender].unlockTime = unlockTime; - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint192 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - communityStakeAmounts[msg.sender][stakee].amount += amount; - communityStakeAmounts[msg.sender][stakee].unlockTime = unlockTime; - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slashAddresses( - address[] calldata stakers, - address[] calldata stakees, - uint64 percent - ) external { - for (uint256 i = 0; i < stakers.length; i++) { - address staker = stakers[i]; - address stakee = stakees[i]; - - if (stakee == staker) { - uint192 slashedAmount = (percent * selfStakeAmount[staker].amount) / - 100; - totalSlashed[currentBurnRound] += slashedAmount; - selfStakeAmount[staker].amount -= slashedAmount; - } else { - uint192 slashedAmount = (percent * - communityStakeAmounts[staker][stakee].amount) / 100; - totalSlashed[currentBurnRound] += slashedAmount; - communityStakeAmounts[staker][stakee].amount -= slashedAmount; - } - } - - emit SlashAddresses(msg.sender); - } - - // Burn last round, start next round (locking this round) - // Rounds don't matter, this is just to time the slashing - function burn() external { - // TODO check that threshold has passed since last burn, save this timestamp - - gtc.transfer(address(1), uint256(totalSlashed[currentBurnRound - 1])); - - emit Burn( - currentBurnRound - 1, - uint256(totalSlashed[currentBurnRound - 1]) - ); - - currentBurnRound++; } // Pseudocode - // function release(address, amount, proof, slashProofHash) external { - // require(msg.sender has Releaser role) - // require(slashProofHashes[slashProofHash], "Slash proof hash not found"); - // checkProof(proof, slashProofHash); // Probably merkle membership? - // // release - // } - - function _authorizeUpgrade( - address - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} -} - -// Tracking slashes explicitly in contract -// store amount as uint192 -// slash with slash IDs -contract GitcoinIdentityStaking13 is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable -{ - using EnumerableSet for EnumerableSet.AddressSet; - - bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - - error OnlySlasher(); - error OnlyAdmin(); - - struct Stake { - uint192 amount; - uint64 unlockTime; - } - - struct Slash { - uint192 amount; - uint64 time; - uint256[] stakeIds; - } - - mapping(address => uint256[]) public selfStakeIds; - mapping(address => mapping(address => uint256[])) public communityStakeIds; - - mapping(uint256 stakeId => Stake) public stakes; - mapping(uint256 slashId => Slash) public slashes; - uint256 public stakeCount; - uint256 public slashCount; - - event SelfStake(address indexed staker, uint256 amount); - event CommunityStake( - address indexed staker, - address indexed stakee, - uint256 amount - ); - - event SlashEvent( - address indexed slasher, - uint64 slashedPercent, - uint256 slashCount - ); - - event Burn(address indexed burner); - - GTC public gtc; - - function initialize(address gtcAddress) public initializer { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - - __AccessControl_init(); - __Pausable_init(); - - gtc = GTC(gtcAddress); - } - - function selfStake(uint192 amount, uint64 unlockTime) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - selfStakeIds[msg.sender].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - emit SelfStake(msg.sender, amount); - } - - function communityStake( - address stakee, - uint192 amount, - uint64 unlockTime - ) external { - require(amount > 0, "Amount must be greater than 0"); - require(unlockTime > block.timestamp, "Unlock time must be in the future"); - - uint256 stakeId = ++stakeCount; - stakes[stakeId].amount = amount; - stakes[stakeId].unlockTime = unlockTime; - - communityStakeIds[msg.sender][stakee].push(stakeId); - - gtc.transferFrom(msg.sender, address(this), uint256(amount)); - - emit CommunityStake(msg.sender, stakee, amount); - } - - function slash( - uint256[] calldata stakeIds, - uint64 slashedPercent - ) external { - uint192 totalSlashed = 0; - uint256 numStakes = stakeIds.length; - - for (uint256 i = 0; i < numStakes; i++) { - uint256 stakeId = stakeIds[i]; - uint192 slashedAmount = (slashedPercent * stakes[stakeId].amount) / 100; - totalSlashed += slashedAmount; - stakes[stakeId].amount -= slashedAmount; - } + function release( + SlashMember[] calldata slashMembers, + uint256 slashMemberIndex, + uint192 amountToRelease, + bytes32 slashProofHash + ) external onlyRole(RELEASER_ROLE) { + require(slashProofHashes[slashProofHash], "Slash proof hash not found"); + require(keccak256(abi.encode(slashMembers)) == slashProofHash, "Slash proof hash does not match"); - slashes[slashCount].amount = totalSlashed; - slashes[slashCount].time = uint64(block.timestamp); - slashes[slashCount].stakeIds = stakeIds; + SlashMember memory slashMemberToRelease = slashMembers[slashMemberIndex]; - slashCount++; + require(amountToRelease <= slashMemberToRelease.amount, "Amount to release must be less than or equal to amount slashed"); - emit SlashEvent(msg.sender, slashedPercent, slashCount); - } + SlashMember[] memory newSlashMembers = slashMembers; - function burn(uint256[] calldata slashIds) external { - uint192 amountToBurn = 0; + newSlashMembers[slashMemberIndex].amount -= amountToRelease; - uint256 numIds = slashIds.length; - for (uint256 i = 0; i < numIds; i++) { - uint256 slashId = slashIds[i]; - if (slashes[slashId].time > 0) { - amountToBurn += slashes[slashId].amount; - delete slashes[slashId]; - } - } + bytes32 newSlashProofHash = keccak256(abi.encode(newSlashMembers)); - gtc.transfer(address(1), uint256(amountToBurn)); + slashProofHashes[slashProofHash] = false; + slashProofHashes[newSlashProofHash] = true; - emit Burn(msg.sender); + gtc.transfer(slashMemberToRelease.account, amountToRelease); } - // Pseudocode - // function release(address, amount, slashId) external { - // require(msg.sender has Releaser role) - // require(slashed[slashId] exists) - // require(slashes[slashId].accounts.contains(address)) - // // release - // } - function _authorizeUpgrade( address ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} diff --git a/test/GitcoinIdentityStaking.ts b/test/GitcoinIdentityStaking.ts index 3d05cbd..b6aaaf7 100644 --- a/test/GitcoinIdentityStaking.ts +++ b/test/GitcoinIdentityStaking.ts @@ -33,104 +33,104 @@ describe("GitcoinIdentityStaking", function () { .connect(this.owner) .initialize(gtcAddress); - const GitcoinIdentityStaking2 = await ethers.getContractFactory( - "GitcoinIdentityStaking2", - this.owner - ); - this.gitcoinIdentityStaking2 = await GitcoinIdentityStaking2.deploy(); - await this.gitcoinIdentityStaking2 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking3 = await ethers.getContractFactory( - "GitcoinIdentityStaking3", - this.owner - ); - this.gitcoinIdentityStaking3 = await GitcoinIdentityStaking3.deploy(); - await this.gitcoinIdentityStaking3 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking4 = await ethers.getContractFactory( - "GitcoinIdentityStaking4", - this.owner - ); - this.gitcoinIdentityStaking4 = await GitcoinIdentityStaking4.deploy(); - await this.gitcoinIdentityStaking4 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking5 = await ethers.getContractFactory( - "GitcoinIdentityStaking5", - this.owner - ); - this.gitcoinIdentityStaking5 = await GitcoinIdentityStaking5.deploy(); - await this.gitcoinIdentityStaking5 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking6 = await ethers.getContractFactory( - "GitcoinIdentityStaking6", - this.owner - ); - this.gitcoinIdentityStaking6 = await GitcoinIdentityStaking6.deploy(); - await this.gitcoinIdentityStaking6 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking7 = await ethers.getContractFactory( - "GitcoinIdentityStaking7", - this.owner - ); - this.gitcoinIdentityStaking7 = await GitcoinIdentityStaking7.deploy(); - await this.gitcoinIdentityStaking7 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking8 = await ethers.getContractFactory( - "GitcoinIdentityStaking8", - this.owner - ); - this.gitcoinIdentityStaking8 = await GitcoinIdentityStaking8.deploy(); - await this.gitcoinIdentityStaking8 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking10 = await ethers.getContractFactory( - "GitcoinIdentityStaking10", - this.owner - ); - this.gitcoinIdentityStaking10 = await GitcoinIdentityStaking10.deploy(); - await this.gitcoinIdentityStaking10 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking11 = await ethers.getContractFactory( - "GitcoinIdentityStaking11", - this.owner - ); - this.gitcoinIdentityStaking11 = await GitcoinIdentityStaking11.deploy(); - await this.gitcoinIdentityStaking11 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking12 = await ethers.getContractFactory( - "GitcoinIdentityStaking12", - this.owner - ); - this.gitcoinIdentityStaking12 = await GitcoinIdentityStaking12.deploy(); - await this.gitcoinIdentityStaking12 - .connect(this.owner) - .initialize(gtcAddress); - - const GitcoinIdentityStaking13 = await ethers.getContractFactory( - "GitcoinIdentityStaking13", - this.owner - ); - this.gitcoinIdentityStaking13 = await GitcoinIdentityStaking13.deploy(); - await this.gitcoinIdentityStaking13 - .connect(this.owner) - .initialize(gtcAddress); + // const GitcoinIdentityStaking2 = await ethers.getContractFactory( + // "GitcoinIdentityStaking2", + // this.owner + // ); + // this.gitcoinIdentityStaking2 = await GitcoinIdentityStaking2.deploy(); + // await this.gitcoinIdentityStaking2 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking3 = await ethers.getContractFactory( + // "GitcoinIdentityStaking3", + // this.owner + // ); + // this.gitcoinIdentityStaking3 = await GitcoinIdentityStaking3.deploy(); + // await this.gitcoinIdentityStaking3 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking4 = await ethers.getContractFactory( + // "GitcoinIdentityStaking4", + // this.owner + // ); + // this.gitcoinIdentityStaking4 = await GitcoinIdentityStaking4.deploy(); + // await this.gitcoinIdentityStaking4 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking5 = await ethers.getContractFactory( + // "GitcoinIdentityStaking5", + // this.owner + // ); + // this.gitcoinIdentityStaking5 = await GitcoinIdentityStaking5.deploy(); + // await this.gitcoinIdentityStaking5 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking6 = await ethers.getContractFactory( + // "GitcoinIdentityStaking6", + // this.owner + // ); + // this.gitcoinIdentityStaking6 = await GitcoinIdentityStaking6.deploy(); + // await this.gitcoinIdentityStaking6 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking7 = await ethers.getContractFactory( + // "GitcoinIdentityStaking7", + // this.owner + // ); + // this.gitcoinIdentityStaking7 = await GitcoinIdentityStaking7.deploy(); + // await this.gitcoinIdentityStaking7 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking8 = await ethers.getContractFactory( + // "GitcoinIdentityStaking8", + // this.owner + // ); + // this.gitcoinIdentityStaking8 = await GitcoinIdentityStaking8.deploy(); + // await this.gitcoinIdentityStaking8 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking10 = await ethers.getContractFactory( + // "GitcoinIdentityStaking10", + // this.owner + // ); + // this.gitcoinIdentityStaking10 = await GitcoinIdentityStaking10.deploy(); + // await this.gitcoinIdentityStaking10 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking11 = await ethers.getContractFactory( + // "GitcoinIdentityStaking11", + // this.owner + // ); + // this.gitcoinIdentityStaking11 = await GitcoinIdentityStaking11.deploy(); + // await this.gitcoinIdentityStaking11 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking12 = await ethers.getContractFactory( + // "GitcoinIdentityStaking12", + // this.owner + // ); + // this.gitcoinIdentityStaking12 = await GitcoinIdentityStaking12.deploy(); + // await this.gitcoinIdentityStaking12 + // .connect(this.owner) + // .initialize(gtcAddress); + + // const GitcoinIdentityStaking13 = await ethers.getContractFactory( + // "GitcoinIdentityStaking13", + // this.owner + // ); + // this.gitcoinIdentityStaking13 = await GitcoinIdentityStaking13.deploy(); + // await this.gitcoinIdentityStaking13 + // .connect(this.owner) + // .initialize(gtcAddress); for (let i = 0; i < this.userAccounts.length; i++) { await this.gtc @@ -144,84 +144,63 @@ describe("GitcoinIdentityStaking", function () { await Promise.all( [ - // this.gitcoinIdentityStaking, + this.gitcoinIdentityStaking // this.gitcoinIdentityStaking2, // this.gitcoinIdentityStaking3, // this.gitcoinIdentityStaking4 // this.gitcoinIdentityStaking5, - this.gitcoinIdentityStaking6, - this.gitcoinIdentityStaking7, + // this.gitcoinIdentityStaking6, + // this.gitcoinIdentityStaking7, // this.gitcoinIdentityStaking8 // this.gitcoinIdentityStaking10, - this.gitcoinIdentityStaking11, - this.gitcoinIdentityStaking12 + // this.gitcoinIdentityStaking11, + // this.gitcoinIdentityStaking12 // this.gitcoinIdentityStaking13 ].map(async (gitcoinIdentityStaking: any) => { + gitcoinIdentityStaking.grantRole( + await gitcoinIdentityStaking.SLASHER_ROLE(), + this.owner.address + ); + gitcoinIdentityStaking.grantRole( + await gitcoinIdentityStaking.BURNER_ROLE(), + this.owner.address + ); + gitcoinIdentityStaking.grantRole( + await gitcoinIdentityStaking.RELEASER_ROLE(), + this.owner.address + ); + const slashAddresses: { staker: string; stakee: string }[] = []; await Promise.all( userAccounts.map(async (userAccount: any, accountIdx: number) => { - let hasTimelock = true; - try { - gitcoinIdentityStaking["selfStake(uint256)"]; - hasTimelock = false; - } catch {} - - if (hasTimelock) { - for (const func of shuffleArray([ - () => - gitcoinIdentityStaking - .connect(userAccount) - .selfStake(100000, 1703165387), - - () => - gitcoinIdentityStaking - .connect(userAccount) - .communityStake( - this.userAccounts[accountIdx + 1], - 100000, - 1703165387 - ), - - () => - gitcoinIdentityStaking - .connect(userAccount) - .communityStake( - this.userAccounts[ - accountIdx - ? accountIdx - 1 - : this.userAccounts.length - 1 - ], - 100000, - 1703165387 - ) - ])) { - await func(); - } - } else { - for (const func of shuffleArray([ - () => - gitcoinIdentityStaking.connect(userAccount).selfStake(100000), - - () => - gitcoinIdentityStaking - .connect(userAccount) - .communityStake(this.userAccounts[accountIdx + 1], 100000), - - () => - gitcoinIdentityStaking - .connect(userAccount) - .communityStake( - this.userAccounts[ - accountIdx - ? accountIdx - 1 - : this.userAccounts.length - 1 - ], - 100000 - ) - ])) { - await func(); - } + for (const func of shuffleArray([ + () => + gitcoinIdentityStaking + .connect(userAccount) + .selfStake(100000, 1703165387), + + () => + gitcoinIdentityStaking + .connect(userAccount) + .communityStake( + this.userAccounts[accountIdx + 1], + 100000, + 1703165387 + ), + + () => + gitcoinIdentityStaking + .connect(userAccount) + .communityStake( + this.userAccounts[ + accountIdx ? accountIdx - 1 : this.userAccounts.length - 1 + ], + 100000, + 1703165387 + ) + ])) { + await func(); } slashAddresses.push( { @@ -243,55 +222,87 @@ describe("GitcoinIdentityStaking", function () { }) ); - // expect(await gitcoinIdentityStaking.stakeCount()).to.equal( - // userAccounts.length * 3 - // ); - - const addresses = userAccounts - .slice(0, 20) - .map(({ address }: { address: string }) => address); - - const stakeIds = Array.from({ length: 60 }, (_, i) => i); - - for (const slashMethod of [ - ["slash(uint256[],uint64)", [stakeIds, 50]], - ["slash(uint256[],uint64,uint256)", [stakeIds, 50, 123]], - ["slash(address[],uint64)", [addresses, 50]], - ["slash(address[],uint64,uint256)", [addresses, 50, 123]] - ]) { - const [func, args]: any = slashMethod; - try { - gitcoinIdentityStaking[func]; - } catch { - continue; - } - await gitcoinIdentityStaking.connect(this.owner)[func](...args); - } - - let hasBurnArgs = false; - try { - gitcoinIdentityStaking["burn(uint256[])"]; - hasBurnArgs = true; - } catch {} - - if (hasBurnArgs) { - await gitcoinIdentityStaking.connect(this.owner).burn([0, 1]); - } else { - await gitcoinIdentityStaking.connect(this.owner).burn(); - } - - let hasSlashAddresses = false; - try { - gitcoinIdentityStaking.slashAddresses; - hasSlashAddresses = true; - } catch {} - if (hasSlashAddresses) { - await gitcoinIdentityStaking.connect(this.owner).slashAddresses( - slashAddresses.slice(0, 60).map(({ staker }) => staker), - slashAddresses.slice(0, 60).map(({ stakee }) => stakee), - 50 - ); - } + const stakeIds: number[] = []; + let slashMembers: any[][] = []; + + await Promise.all( + userAccounts.slice(0, 60).map(async (userAccount: any) => { + const stakeId = await gitcoinIdentityStaking.selfStakeIds( + userAccount.address, + 0 + ); + const amount = (await gitcoinIdentityStaking.stakes(stakeId))[0]; + slashMembers.push([userAccount.address, amount]); + stakeIds.push(stakeId); + }) + ); + slashMembers = slashMembers.sort((a, b) => (a[0] < b[0] ? -1 : 1)); + + const slashProof = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + [ + { + type: "tuple[]", + name: "SlashMember", + components: [ + { + name: "account", + type: "address", + baseType: "address" + }, + { + name: "amount", + type: "uint192", + baseType: "uint192" + } + ] + } + ], + [slashMembers] + ) + ); + + await gitcoinIdentityStaking + .connect(this.owner) + .slash(stakeIds, 50, slashProof); + + const indexToRelease = 1; + + await gitcoinIdentityStaking + .connect(this.owner) + .release(slashMembers, indexToRelease, 500, slashProof); + + slashMembers[indexToRelease][1] -= BigInt(500); + + const newSlashProof = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + [ + { + type: "tuple[]", + name: "SlashMember", + components: [ + { + name: "account", + type: "address", + baseType: "address" + }, + { + name: "amount", + type: "uint192", + baseType: "uint192" + } + ] + } + ], + [slashMembers] + ) + ); + + await gitcoinIdentityStaking + .connect(this.owner) + .release(slashMembers, 2, 1000, newSlashProof); + + await gitcoinIdentityStaking.connect(this.owner).burn(); }) ); }).timeout(1000000);