Only this pageAll pages
Powered by GitBook
1 of 14

StakedCelo

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Deposit and Withdrawal Flows

These are the flows of how CELO is deposited and withdrawn from the protocol, detailing specific contract functions that need to be called.

Deposit flow

  • Call Manager.deposit, setting msg.value to the amount of CELO one wants to deposit. stCELO is minted to the user, and Manager schedules votes according to the voting strategy.

At some point, Account.activateAndVote should be called for each validator group that has had votes scheduled recently. Note that this does not need to be called for every deposit call, but ideally should be called before the epoch during which the deposit was made ends. This is because voting CELO doesn't start receiving epoch rewards until the next epoch after it was used for voting. The function can be called by any address, whether or not it had previously deposited into the protocol (in particular, there could be a bot that calls it once a day per validator group).

Withdrawal flow

  • Call Manager.withdraw. stCELO is burned from the user, and Manager schedules withdrawals according to the voting strategy. The following steps are necessary to unlock Account's CELO from the LockedGold contract and actually distribute them to the user.

  • Call Account.withdraw for each group that was scheduled to be withdrawn from in the previous step (can be called by anyone, e.g. a bot). Some CELO might be available for immediate withdrawal, if it hadn't yet been locked and used for voting, and will be transferred to the user. For the rest of the withdrawal amount, it will be unvoted from the specified group and the LockedGold unlocking process will begin.

After the 3 day unlocking period has passed, Account.finishPendingWithdrawal should be called (can be called by anyone, e.g. a bot), specifying the pending withdrawal that was created in the previous step. This will finalize the LockedGold withdrawal and return the remaining CELO to the user.

Voting for Validator Groups

An account can vote for up to 10 different validator groups (based on the maxNumGroupsVotedFor parameter of the Elections core smart contract). For this implementation of StakedCelo, consequently the protocol can only vote for up to 10 validators. Currently, votes are only cast for 8 validator groups.

The following principles guide the selection of the initial set of validators:

  1. Efficiency: For holders of stCELO: Vote for validators who support Celo most effectively. For the community: Ship fast and iterate towards better (and more complex) solutions later on.

  2. Neutrality: Cast votes in a comprehensible way, based on objective measures which are important for the protocol to work, or for the Celo ecosystem to function. Besides these core caveats, aim to stay neutral in the selection of individual validator groups.

  3. Credibility: Our approach should be verifiable by anyone based on on-chain data.

Based on these principles, establish rules to select the validator groups to vote for with StakedCelo:

A) Create a shortlist of eligible validator groups

i) Start with all validator groups of Celo as returned by the CLI (celocli validator:list)

ii) Of those, select all which have an uptime score of >98% [need for high uptime to receive maximum rewards]

iii) Of those, select the lower two tertiles of validator groups regarding number of votes received (specifically this means excluding validator groups with over 3,750,000 votes) [support the decentralization of Celo]

iv) Of those, select all which have had zero slashing incidents in the past [slashing incidents decrease rewards]

v) Of those, select all which have at least one validator elected [active validators needed to receive rewards]

vi) Of those, select all which have >2,000,000 votes as open capacity [voting only possible for 8 validators and a minimum of 16M votes to be cast by protocol should be ensured]

This results in the shortlist shared below. Note: The list was created according to these rules on July 28 and shared on the on that day.

B) Randomly select a subset of the shortlist

i) Commit to a blocknumber (and share the hash prior to its mining) and use the randomness contract to generate a seed with that block as input for a random selection algorithm

ii) Use to select 8 validator groups randomly from the shortlist and then vote for those. We do select less than the maximum number of 10, such that we have two free slots available under normal conditions and retain some flexibility around changing votes.

Appendix A: Shortlist

Addresses of validator groups part of the shortlist, selected as described above:

0x0f66619058BB9675f3d394FCc2cE236a29901571

0x15Ed3f6b79F5Fb9Ef1D99D37314Dd626b3005F0b

0x2AF540161Cbeb58FdC99b159c76E390598860510

0x2c2B0f71d59B546B2CAfd222696589c13C3c325C

0x34649AdA2cB44D851a2103Feaa8922DedDABfc1c

0x3D451dd723797b3DE938C5B22412032B6452591A

0x3DcF2ED8dC84a63FfD2bfDc3CDb2fA0B1aeAfE5c

0x5402172E972b31Fc9F0383F53f45823Ab5037379

0x602B65795BCc64b2fb329AC004236E194f077158

0x614B7654ba0cC6000ABe526779911b70C1F7125A

0x69cd274C6AcBc08F664eD7B2D54aaB6615BC1d70

0x70FC0b021dFdBb9A106D1Ed8F35f59D3f23eCb7B

0x71dcC67baEECd9308e341359f70B782D9C3565b8

0x81AE1C73A326325216E25ff1af9EA3871195036E

0x81cef0668e15639D0b101bdc3067699309D73BED

0x82f8Bcf96f24BA60Ef041D192c7CE04C907E2fb8

0x8493BD3De67AC341D4cC11531f96a1A2cdBf29aD

0x8580dB53C3ebC56230662B771ceF2707E92Ef83A

0x89d5bd54C43dDd10905A030DE6Ff02EbB6c51654

0x95aE59515915D6c493a846aE022F93726652b50A

0xAcdf897493A6000dbe256791E8A2beCbb405FD4F

0xB33e9e01E561a1Da60f7Cb42508500e571afb6Eb

0xb35Be22BccB0dB9dC62967dcF15fEB97C20f854e

0xc24baeac0Fd189637112B7e33d22FfF2730aF993

0xc64DF5Be250264bf2888591D87cdeB13BFADC501

0xC7d5409fEe80B3Ac37Dbc111664dC511a5982469

0xc8A81D473992c7c6D3F469A8263F24914625709d

0xd25c6a9FEf4744E8d4F90Bf6bdFAF7686909d799

0xD72Ed2e3db984bAC3bB351FE652200dE527eFfcf

0xE075ba4b1dCAF75513118d7aA08A057c658842c9

0xE141831c2c1198d79B9Ff61cD97C3bAca7F071E0

Appendix B: Selected validator groups

to block commitment has.

Block 13816816 was selected, leading to randomness eeebfcc9bf72d7966eb5413ff5f83270e5dca2f67231f566c95fbccd7216ad38

which results with the following selection of validator groups:

0xc24baeac0Fd189637112B7e33d22FfF2730aF993

0xb35Be22BccB0dB9dC62967dcF15fEB97C20f854e

0x3D451dd723797b3DE938C5B22412032B6452591A

0x15Ed3f6b79F5Fb9Ef1D99D37314Dd626b3005F0b

0x34649AdA2cB44D851a2103Feaa8922DedDABfc1c

0xc8A81D473992c7c6D3F469A8263F24914625709d

0x81AE1C73A326325216E25ff1af9EA3871195036E

0xD72Ed2e3db984bAC3bB351FE652200dE527eFfcf

Celo forum
this script
Link

Protocol Upgrades

StakedCelo is upgradeable through a 3 out of 5 multisig. Any upgrade initiated by the multisig require to wait for a 7 day timelock to pass before coming into effect, giving users ample time to withdraw funds before a code change should they wish to do so. The holders of the multisig are from from the Celo developer community.

stCELO vs rstCELO

stCELO is an ERC-20 compatible non-rebasing token, which is intended to further encourage active participation of users in the protocol, dApps, and wider Celo ecosystem. Non-rebasing means that the amount of tokens a user has in their wallet stays constant over time if that user takes no actions. Consequently, one stCELO is increasing in value over time (measured in CELO), as epoch rewards are being accrued in the protocol*. This may be helpful for DeFi protocols, for example bridging protocols, that do not natively support rebasing tokens.

However, to provide maximal flexibility for developers, StakedCelo also provides a rebasing token, called rstCELO, which wraps around stCELO. The core feature of rstCELO is that it is backed by exactly one CELO, so when withdrawing rstCELO from the StakedCelo protocol one CELO should be received for each rstCELO. This can be a useful property for certain third-party protocols that want to integrate StakedCelo, for example for a decentralized exchange that is optimized for 1:1 exchange rates. rstCELO is simply a wrapped form of stCELO and the two tokens can therefore be redeemed for one another atomically and instantly via a smart contract call at any time.

Contracts

The key contracts of the StakedCelo protocol are Account.sol and Manger.sol as well as the underlying token contracts RebasedStakedCelo.sol and StakedCelo.sol. Each of these contracts is described in more detail in the following chapters.

Introduction

StakedCelo is a Celo-native open source liquid staking protocol developed by cLabs to encourage the active participation of users in the protocol. It allows anyone to stake CELO, thus supporting the network and receiving the Epoch Rewards associated with staking, and at the same time to keep these assets liquid so that they can be used to participate in and engage across other applications in the ecosystem.

In a nutshell, the protocol works as follows:

  • Users deposit CELO into the protocol, which are then staked to support active participation and consensus of the Celo blockchain

  • As a receipt for the deposited CELO, users receive stCELO in return. These stCELO are freely moveable on chain (in contrast to staked CELO which are locked) and can be exchanged anytime for a respective amount of CELO via the protocol

  • The CELO received by the protocol are used to vote for validator groups and receive

  • The rewards accrue in the protocol and are shared proportionally among all holders of stCELO for their active participation and engagement in the protocol

  • The rewards are reflected in an increasing value of stCELO over time, meaning the amount of CELO backing one stCELO is increasing constantly with the Epoch rewards

These core features characterize StakedCelo:

  • stCELO is following the ERC-20 standard and is non-rebasing

  • A rebasing version is available and called rstCELO (a wrapped version of stCELO) - this will ease the access to StakedCelo for different types of users and, thus, further help to promote the participation in the governance process, including timely and active validation of blocks

  • StakedCelo currently charges no fee - meaning 100% of the epoch rewards accrued by the underlying CELO are shared with stCELO holders

Deployed Contracts

Celo Mainnet

  • :

Votes are distributed evenly among 8 validator groups, which were selected at random from a shortlist of validator groups
  • StakedCelo is upgradeable through a 3 out of 5 multisig with a 7 day timelock, giving users ample time to withdraw funds before a code change should they wish to do so

  • Celo Epoch Rewards
    *
    Account.sol: 0x4aAD04D41FD7fd495503731C5a2579e19054C432
  • StakedCelo.sol: 0xC668583dcbDc9ae6FA3CE46462758188adfdfC24

  • RebasedStakedCelo.sol: 0xDc5762753043327d74e0a538199c1488FC1F44cf

  • Alfajores Testnet

    • Manager.sol: 0xFfe124dde2b29fA848aD8caAEBE85651F0b5c406

    • Account.sol: 0xd11CC172D802c1a94e81c5F432471bD34d1828A1

    • StakedCelo.sol: 0xD22E18556E43cb29D6d6172D4b33Fd2Edb629EF2

    • :

    Manager.sol
    0x0239b96D10a434a56CC9E09383077A0490cF9398
    RebasedStakedCelo.sol
    0xe26Ed019Aa0d780Ac49826604357B1319b12602F

    Disclaimer

    Nothing herein constitutes an offer to sell, or the solicitation of an offer to buy, any securities or tokens. As with all virtual currency, there is a risk of volatility in market conditions and there is a risk that the asset value may go to zero. Please do your own research.

    Links

    View StakedCelo GitHub repository here.

    View audit reports here.

    Join our Discord here.

    StakedCelo

    StakedCelo.sol

    An ERC-20 token (ticker: stCELO) representing a share of the staked pool of CELO. Over time, a unit of stCELO becomes withdrawable for more and more CELO, as epoch rewards accrue in the pool.

    Mainnet Deployment: 0xC668583dcbDc9ae6FA3CE46462758188adfdfC24

  • Alfajores Testnet Deployment: 0xD22E18556E43cb29D6d6172D4b33Fd2Edb629EF2

  • Methods

    Initialize

    Initializes the contract.

    Mint

    Mints new stCELO to an address.

    Burn

    Burns stCELO from an address.

    Source Code
    function mint(address to, uint256 amount) external onlyManager {
            _mint(to, amount);
        }
    function initialize(address _manager, address _owner) external initializer {
            __ERC20_init("Staked CELO", "stCELO");
            __Managed_init(_manager);
            _transferOwnership(_owner);
        }
    
     function burn(address from, uint256 amount) external onlyManager {
            _burn(from, amount);
        }

    Manager

    Manager.sol

    The main control center of the system. Defines exchange rate between CELO and stCELO, has ability to mint and burn stCELO, is the main point of interaction for depositing and withdrawing CELO from the pool, and defines the system's voting strategy.

    • Source Code

    • Mainnet Deployment:

    • Alfajores Testnet Deployment:

    Methods

    initialize

    Initialize the contract with registry and owner. .

    setDependencies

    Set this contract's dependencies in the StakedCelo system. Manager, Account and StakedCelo all reference each other so we need a way of setting these after all contracts are deployed and initialized.

    activateGroup

    Marks a group as votable.

    getGroups

    Returns the array of active groups.

    deprecateGroup

    Marks a group as deprecated. A deprecated group will remain in the `deprecatedGroups` array as long as it is still being voted for by the Account contract. Deprecated groups will be the first to have their votes withdrawn.

    getDeprecatedGroups

    Returns the list of deprecated groups.

    deposit

    Used to deposit CELO into the StakedCelo system. The user will receive an amount of stCELO proportional to their contribution. The CELO will be scheduled to be voted for with the Account contract.

    withdraw

    Used to withdraw CELO from the system, in exchange for burning stCELO.

    toStakedCelo

    Computes the amount of stCELO that should be minted for a given amount of CELO deposited.

    toCelo

    Computes the amount of CELO that should be withdrawn for a given amount of stCELO burn.

    distributeVotes

    Distribute votes, such that groups are receiving the same amount of votes from the system. If a group already has more votes than the average of the total available votes it will not be voted for, and instead we'll try to evenly distribute between the remaining groups.

    Election.sol sets a dynamic limit on the number of votes receivable by a group, based on the group's size, the total amount of Locked CELO, and the total number of electable validators. We don't want to schedule votes for a group when the amount would exceed this threshold. `getVotableGroups` below selects those groups that could receive the entire `votes` amount, and filters out the rest.

    This is a heuristic

    when distributing votes evenly, the group might receive less than`votes`, and the total amount could end up being under the limit. However, doing an exact computation would be both complex and cost a lot of additional gas, hence the heuristic. If indeed all groups are close to their voting limit, causing a larger deposit to revert with NoVotableGroups, despite there still being some room for deposits, this can be worked around by sending a few smaller deposits.

    distributeWithdrawals

    Distributes withdrawals by computing the number of votes that should be withdrawn from each group, then calling out to `Account.scheduleVotes`.

    The withdrawal distribution strategy is to:

    • Withdraw as much as possible from any deprecated groups.

    • If more votes still need to be withdrawn, try and have each validator group end up receiving the same amount of votes from the system. If a group already has less votes than the average of the total remaining votes, it will not be withdrawn from, and instead will try to evenly distribute between the remaining groups.

    getDeprecatedGroupsWithdrawalDistribution

    Calculates how many votes should be withdrawn from each deprecated group.

    getActiveGroupWithdrawalDistribution

    Calculates how votes should be withdrawn from each active group.

    getSortedGroupsWithVotes

    Returns a list of group addresses with their corresponding current total votes, sorted by the number of votes, and the total number of votes in the system.

    getVotableGroups

    Returns the active groups that can receive the entire `votes` amount based on their current receivable votes limit in Election.sol.

    sortGroupsWithVotes

    Sorts an array of GroupWithVotes structs based on increasing `votes` values.

    0x0239b96D10a434a56CC9E09383077A0490cF9398
    0xFfe124dde2b29fA848aD8caAEBE85651F0b5c406
     function initialize(address _registry, address _owner) external initializer {
            _transferOwnership(_owner);
            __UsingRegistry_init(_registry);
        }
       function setDependencies(address _stakedCelo, address _account) external onlyOwner {
            stakedCelo = IStakedCelo(_stakedCelo);
            account = IAccount(_account);
        }
    function activateGroup(address group) external onlyOwner {
            if (activeGroups.contains(group)) {
                revert GroupAlreadyAdded(group);
            }
    
            if (deprecatedGroups.contains(group)) {
                if (!deprecatedGroups.remove(group)) {
                    revert FailedToRemoveDeprecatedGroup(group);
                }
            }
    
            if (
                activeGroups.length() + deprecatedGroups.length() >=
                getElection().maxNumGroupsVotedFor()
            ) {
                revert MaxGroupsVotedForReached();
            }
    
            if (!activeGroups.add(group)) {
                revert FailedToAddActiveGroup(group);
            }
            emit GroupActivated(group);
        }
    function getGroups() external view returns (address[] memory) {
            return activeGroups.values();
        }
        function deprecateGroup(address group) external onlyOwner {
            if (!activeGroups.remove(group)) {
                revert GroupNotActive(group);
            }
    
            emit GroupDeprecated(group);
    
            if (account.getCeloForGroup(group) > 0) {
                if (!deprecatedGroups.add(group)) {
                    revert FailedToAddDeprecatedGroup(group);
                }
            } else {
                emit GroupRemoved(group);
            }
        }
     function getDeprecatedGroups() external view returns (address[] memory) {
            return deprecatedGroups.values();
        }
      function deposit() external payable {
            if (activeGroups.length() == 0) {
                revert NoActiveGroups();
            }
    
            stakedCelo.mint(msg.sender, toStakedCelo(msg.value));
    
            distributeVotes(msg.value);
        }
     function withdraw(uint256 stakedCeloAmount) external {
            if (activeGroups.length() + deprecatedGroups.length() == 0) {
                revert NoGroups();
            }
    
            distributeWithdrawals(toCelo(stakedCeloAmount), msg.sender);
    
            stakedCelo.burn(msg.sender, stakedCeloAmount);
        }
    
      function toStakedCelo(uint256 celoAmount) public view returns (uint256) {
            uint256 stCeloSupply = stakedCelo.totalSupply();
            uint256 celoBalance = account.getTotalCelo();
    
            if (stCeloSupply == 0 || celoBalance == 0) {
                return celoAmount;
            }
    
            return (celoAmount * stCeloSupply) / celoBalance;
        }
       function toCelo(uint256 stCeloAmount) public view returns (uint256) {
            uint256 stCeloSupply = stakedCelo.totalSupply();
            uint256 celoBalance = account.getTotalCelo();
    
            if (stCeloSupply == 0 || celoBalance == 0) {
                return stCeloAmount;
            }
    
            return (stCeloAmount * celoBalance) / stCeloSupply;
        }
    function distributeVotes(uint256 votes) internal {
            /*
             * "Votable" groups are those that will currently fit under the voting
             * limit in Election.sol even if voted for with the entire `votes`
             * amount. Note that some might still not end up getting voted for given
             * the distribution logic below.
             */
            address[] memory votableGroups = getVotableGroups(votes);
            if (votableGroups.length == 0) {
                revert NoVotableGroups();
            }
    
            GroupWithVotes[] memory sortedGroups;
            uint256 availableVotes;
            (sortedGroups, availableVotes) = getSortedGroupsWithVotes(votableGroups);
            availableVotes += votes;
    
            uint256[] memory votesPerGroup = new uint256[](votableGroups.length);
            uint256 groupsVoted = votableGroups.length;
            uint256 targetVotes = availableVotes / groupsVoted;
    
            /*
             * This would normally be (i = votableGroups.length - 1; i >=0; i--),
             * but we can't i-- on the last iteration when i=0, since i is an
             * unsigned integer. So we iterate with the loop variable 1 greater than
             * expected, set index = i-1, and use index inside the loop.
             */
            for (uint256 i = votableGroups.length; i > 0; i--) {
                uint256 index = i - 1;
                if (sortedGroups[index].votes >= targetVotes) {
                    groupsVoted--;
                    availableVotes -= sortedGroups[index].votes;
                    targetVotes = availableVotes / groupsVoted;
                    votesPerGroup[index] = 0;
                } else {
                    votesPerGroup[index] = targetVotes - sortedGroups[index].votes;
    
                    if (availableVotes % groupsVoted > index) {
                        votesPerGroup[index]++;
                    }
                }
            }
    
            address[] memory finalGroups = new address[](groupsVoted);
            uint256[] memory finalVotes = new uint256[](groupsVoted);
    
            for (uint256 i = 0; i < groupsVoted; i++) {
                finalGroups[i] = sortedGroups[i].group;
                finalVotes[i] = votesPerGroup[i];
            }
    
            account.scheduleVotes{value: votes}(finalGroups, finalVotes);
        }
    function distributeWithdrawals(uint256 withdrawal, address beneficiary) internal {
            if (withdrawal == 0) {
                revert ZeroWithdrawal();
            }
    
            address[] memory deprecatedGroupsWithdrawn;
            uint256[] memory deprecatedWithdrawalsPerGroup;
            uint256 numberDeprecatedGroupsWithdrawn;
    
            (
                deprecatedGroupsWithdrawn,
                deprecatedWithdrawalsPerGroup,
                numberDeprecatedGroupsWithdrawn,
                withdrawal
            ) = getDeprecatedGroupsWithdrawalDistribution(withdrawal);
    
            address[] memory groupsWithdrawn;
            uint256[] memory withdrawalsPerGroup;
    
            (groupsWithdrawn, withdrawalsPerGroup) = getActiveGroupWithdrawalDistribution(withdrawal);
    
            address[] memory finalGroups = new address[](
                groupsWithdrawn.length + numberDeprecatedGroupsWithdrawn
            );
            uint256[] memory finalVotes = new uint256[](
                groupsWithdrawn.length + numberDeprecatedGroupsWithdrawn
            );
    
            for (uint256 i = 0; i < numberDeprecatedGroupsWithdrawn; i++) {
                finalGroups[i] = deprecatedGroupsWithdrawn[i];
                finalVotes[i] = deprecatedWithdrawalsPerGroup[i];
            }
    
            for (uint256 i = 0; i < groupsWithdrawn.length; i++) {
                finalGroups[i + numberDeprecatedGroupsWithdrawn] = groupsWithdrawn[i];
                finalVotes[i + numberDeprecatedGroupsWithdrawn] = withdrawalsPerGroup[i];
            }
    
            account.scheduleWithdrawals(finalGroups, finalVotes, beneficiary);
        }
    function getDeprecatedGroupsWithdrawalDistribution(uint256 withdrawal)
            internal
            returns (
                address[] memory deprecatedGroupsWithdrawn,
                uint256[] memory deprecatedWithdrawalsPerGroup,
                uint256 numberDeprecatedGroupsWithdrawn,
                uint256 remainingWithdrawal
            )
        {
            remainingWithdrawal = withdrawal;
            uint256 numberDeprecatedGroups = deprecatedGroups.length();
            deprecatedGroupsWithdrawn = new address[](numberDeprecatedGroups);
            deprecatedWithdrawalsPerGroup = new uint256[](numberDeprecatedGroups);
            numberDeprecatedGroupsWithdrawn = 0;
    
            for (uint256 i = 0; i < numberDeprecatedGroups; i++) {
                numberDeprecatedGroupsWithdrawn++;
                deprecatedGroupsWithdrawn[i] = deprecatedGroups.at(i);
                uint256 currentVotes = account.getCeloForGroup(deprecatedGroupsWithdrawn[i]);
                deprecatedWithdrawalsPerGroup[i] = Math.min(remainingWithdrawal, currentVotes);
                remainingWithdrawal -= deprecatedWithdrawalsPerGroup[i];
    
                if (currentVotes == deprecatedWithdrawalsPerGroup[i]) {
                    if (!deprecatedGroups.remove(deprecatedGroupsWithdrawn[i])) {
                        revert FailedToRemoveDeprecatedGroup(deprecatedGroupsWithdrawn[i]);
                    }
                    emit GroupRemoved(deprecatedGroupsWithdrawn[i]);
                }
    
                if (remainingWithdrawal == 0) {
                    break;
                }
            }
    
            return (
                deprecatedGroupsWithdrawn,
                deprecatedWithdrawalsPerGroup,
                numberDeprecatedGroupsWithdrawn,
                remainingWithdrawal
            );
        }
    
    function getActiveGroupWithdrawalDistribution(uint256 withdrawal)
            internal
            view
            returns (address[] memory, uint256[] memory)
        {
            if (withdrawal == 0) {
                address[] memory noGroups = new address[](0);
                uint256[] memory noWithdrawals = new uint256[](0);
                return (noGroups, noWithdrawals);
            }
    
            uint256 numberGroups = activeGroups.length();
            GroupWithVotes[] memory sortedGroups;
            uint256 availableVotes;
            (sortedGroups, availableVotes) = getSortedGroupsWithVotes(activeGroups.values());
            availableVotes -= withdrawal;
    
            uint256 numberGroupsWithdrawn = numberGroups;
            uint256 targetVotes = availableVotes / numberGroupsWithdrawn;
    
            for (uint256 i = 0; i < numberGroups; i++) {
                if (sortedGroups[i].votes <= targetVotes) {
                    numberGroupsWithdrawn--;
                    availableVotes -= sortedGroups[i].votes;
                    targetVotes = availableVotes / numberGroupsWithdrawn;
                } else {
                    break;
                }
            }
    
            uint256[] memory withdrawalsPerGroup = new uint256[](numberGroupsWithdrawn);
            address[] memory groupsWithdrawn = new address[](numberGroupsWithdrawn);
            uint256 offset = numberGroups - numberGroupsWithdrawn;
    
            for (uint256 i = 0; i < numberGroupsWithdrawn; i++) {
                groupsWithdrawn[i] = sortedGroups[i + offset].group;
                withdrawalsPerGroup[i] = sortedGroups[i + offset].votes - targetVotes;
                if (availableVotes % numberGroupsWithdrawn > i) {
                    withdrawalsPerGroup[i]--;
                }
            }
    
            return (groupsWithdrawn, withdrawalsPerGroup);
        }
    function getSortedGroupsWithVotes(address[] memory groups)
            internal
            view
            returns (GroupWithVotes[] memory, uint256)
        {
            GroupWithVotes[] memory groupsWithVotes = new GroupWithVotes[](groups.length);
            uint256 totalVotes = 0;
            for (uint256 i = 0; i < groups.length; i++) {
                uint256 votes = account.getCeloForGroup(groups[i]);
                totalVotes += votes;
                groupsWithVotes[i] = GroupWithVotes(groups[i], votes);
            }
    
            sortGroupsWithVotes(groupsWithVotes);
            return (groupsWithVotes, totalVotes);
        }
    function getVotableGroups(uint256 votes) internal returns (address[] memory) {
            uint256 numberGroups = activeGroups.length();
            uint256 numberVotableGroups = 0;
            address[] memory votableGroups = new address[](numberGroups);
    
            for (uint256 i = 0; i < numberGroups; i++) {
                address group = activeGroups.at(i);
                uint256 scheduledVotes = account.scheduledVotes(group);
                if (getElection().canReceiveVotes(group, votes + scheduledVotes)) {
                    votableGroups[numberVotableGroups] = group;
                    numberVotableGroups++;
                }
            }
    
            address[] memory votableGroupsFinal = new address[](numberVotableGroups);
            for (uint256 i = 0; i < numberVotableGroups; i++) {
                votableGroupsFinal[i] = votableGroups[i];
            }
    
            return votableGroupsFinal;
        }
    function sortGroupsWithVotes(GroupWithVotes[] memory groupsWithVotes) internal pure {
            for (uint256 i = 1; i < groupsWithVotes.length; i++) {
                uint256 j = i;
                while (j > 0 && groupsWithVotes[j].votes < groupsWithVotes[j - 1].votes) {
                    (groupsWithVotes[j], groupsWithVotes[j - 1]) = (
                        groupsWithVotes[j - 1],
                        groupsWithVotes[j]
                    );
                    j--;
                }
            }
        }
    

    RebasedStakedCelo

    RebasedStakedCelo.sol (wrap & unwrap)

    This is a wrapper token (ticker: rstCELO) around stCELO that, instead of accruing value to each token as staking rewards accrue in the pool, rebases balances, such that an account's balance always represents the amount of CELO that could be withdrawn for the underlying stCELO. Thus, the value of one unit of rstCELO and one unit of CELO should be approximately equivalent.

    • Source Code

    • Mainnet Deployment:

    • Alfajores Testnet Deployment:

    Methods

    Initialize

    Replaces the constructor for proxy implementation.

    Deposit

    Deposit stCELO in return for rstCELO. Although rstCELO is never minted to any account, the rstCELO balance is calculated based on the account's deposited stCELO. See `balanceOf()` function below.

    Withdraw

    Withdraws stCELO. This function transfers back some or all of the sender's. previously deposited stCELO.

    totalSupply

    Used to query the total supply of rstCELO. The calculated total supply of rstCELO.

    balanceOf

    Used to query the rstCELO balance of an address.

    toStakedCelo

    Computes the amount of stCELO that is represented by an amount of rstCELO.

    toRebasedStakedCelo

    Computes the amount of rstCELO that is represented by an amount of stCELO.

    _transfer

    Moves `amount` of rstCELO from `sender` to `recipient`.

    0xDc5762753043327d74e0a538199c1488FC1F44cf
    0xe26Ed019Aa0d780Ac49826604357B1319b12602F
    function initialize(
            address _stakedCelo,
            address _account,
            address _owner
        ) external initializer {
            __ERC20_init("Rebased Staked CELO", "rstCELO");
            _transferOwnership(_owner);
            stakedCelo = IStakedCelo(_stakedCelo);
            account = IAccount(_account);
        }
     function deposit(uint256 stCeloAmount) external {
            if (stCeloAmount == 0) {
                revert ZeroAmount();
            }
    
            totalDeposit += stCeloAmount;
    
            stakedCeloBalance[msg.sender] += stCeloAmount;
    
            emit StakedCeloDeposited(msg.sender, stCeloAmount);
    
            if (!stakedCelo.transferFrom(msg.sender, address(this), stCeloAmount)) {
                revert FailedDeposit(msg.sender, stCeloAmount);
            }
        }
    
    function withdraw(uint256 stCeloAmount) external {
            if (stCeloAmount > stakedCeloBalance[msg.sender]) {
                revert InsufficientBalance(stCeloAmount);
            }
    
            totalDeposit -= stCeloAmount;
    
            unchecked {
                stakedCeloBalance[msg.sender] -= stCeloAmount;
            }
            emit StakedCeloWithdrawn(msg.sender, stCeloAmount);
    
            if (!stakedCelo.transfer(msg.sender, stCeloAmount)) {
                revert FailedWithdrawal(msg.sender, stCeloAmount);
            }
        }
     function totalSupply() public view virtual override returns (uint256) {
            return toRebasedStakedCelo(totalDeposit);
        }
    
     function balanceOf(address _account) public view override returns (uint256) {
            return toRebasedStakedCelo(stakedCeloBalance[_account]);
        }
     function toStakedCelo(uint256 rstCeloAmount) public view returns (uint256) {
            uint256 stCeloSupply = stakedCelo.totalSupply();
            uint256 celoBalance = account.getTotalCelo();
    
            if (stCeloSupply == 0 || celoBalance == 0) {
                return rstCeloAmount;
            }
    
            return (rstCeloAmount * stCeloSupply) / celoBalance;
        }
    function toRebasedStakedCelo(uint256 stCeloAmount) public view returns (uint256) {
            uint256 stCeloSupply = stakedCelo.totalSupply();
            uint256 celoBalance = account.getTotalCelo();
    
            if (stCeloSupply == 0 || celoBalance == 0) {
                return stCeloAmount;
            }
    
            return (stCeloAmount * celoBalance) / stCeloSupply;
        }
    
    function _transfer(
            address from,
            address to,
            uint256 amount
        ) internal override {
            if (from == address(0)) {
                revert NullAddress();
            }
            if (to == address(0)) {
                revert NullAddress();
            }
    
            uint256 fromBalance = stakedCeloBalance[from];
            uint256 equivalentStakedCeloAmount = toStakedCelo(amount);
            if (fromBalance < equivalentStakedCeloAmount) {
                revert InsufficientBalance(amount);
            }
    
            unchecked {
                stakedCeloBalance[from] = fromBalance - equivalentStakedCeloAmount;
            }
            stakedCeloBalance[to] += equivalentStakedCeloAmount;
    
            emit Transfer(from, to, amount);
        }
    }

    FAQ

    What is StakedCelo?

    StakedCelo is a Celo-native open source liquid staking protocol developed by cLabs to encourage the active participation of users in the protocol. It allows anyone to stake CELO to support the network and receive the Epoch Rewards associated with staking, and at the same time to keep these assets liquid so that they can be used to help participate in and engage across other applications in the ecosystem. The StakedCelo protocol can be directly accessed through the smart contracts deployed on chain, or through a WebApp developed by cLabs. Users can also obtain stCELO on decentralized exchanges or engage in the protocol through other third parties that have chosen to integrate StakedCelo.

    What is liquid staking?

    Liquid staking protocols allow users to receive staking rewards without locking assets or maintaining staking infrastructure to increase active participation in the protocol. Users can deposit tokens and receive tradeable liquid tokens in return to further encourage active participation of users in the protocol, dApps, and the wider Celo ecosystem. The assets are controlled by smart contracts in a non-custodial manner.

    How does StakedCelo work?

    Users can deposit CELO into the StakedCelo protocol and in turn receive stCELO tokens representing their assets transferred to the protocol. The protocol stakes deposited CELO and receives the associated with staking, which are shared equally among all holders of stCELO. The protocol is completely non-custodial, meaning that no one can ever withdraw the funds of a given user besides that user themself. At any point in time, a user can choose to withdraw their assets from the protocol by returning the stCELO and receiving the corresponding CELO (including accrued rewards).

    Note that the increase in CELO balance for a user only results from receiving and no lending or borrowing or other activities take place. Unlike other Proof-of-Stake protocols, Celo does not reduce the Locked CELO balance of holders if a validator they are voting for is slashed. Together, this means that no principal is at risk for slashing of validators under normal operating circumstances when using StakedCelo.

    What are Epoch Rewards and how do they impact StakedCelo?

    Celo are similar to block rewards in other blockchains and used to create several types of incentives which help to keep the blockchain working securely. Among others, CELO holders who lock their tokens into receive rewards for active participation, including voting for groups that elected validators. The StakedCelo protocol receives such rewards as it stakes CELO into Locked CELO. These rewards are at the Celo protocol level.

    The rewards accruing in the StakedCelo protocol depend on several factors, including participants and events. For example, the validator groups that StakedCelo votes for and the behavior of their validators (the Slashing Penalty and the Uptime Score influence the distributed tokens), the total amount of CELO staked (the reward rate is adjusted if the number of staked CELO deviates from the target) or Celo governance (Celo governance can vote to change the underlying mechanism fundamentally). More details can be found in the Celo docs on .

    Why is 1 stCELO not equal 1 CELO?

    stCELO is an ERC-20 compatible non-rebasing token. This means that the amount of tokens a user has in their wallet stays constant over time if that user takes no actions. Consequently, one stCELO is increasing in value over time (measured in CELO), as epoch rewards are being accrued in the protocol. Note that this only describes the value of stCELO and CELO when being staked and unstaked via the StakedCelo protocol. Prices on decentralized exchanges are subject to market conditions, market fluctuations, and not per se predictable.

    What is the difference between StakedCelo and stCELO?

    StakedCelo is the name of the protocol and stCELO is the ERC20 token users receive when staking their CELO with the protocol.

    How is StakedCelo secure?

    StakedCelo has been audited and the reports are published

    How long is stCELO locked?

    To withdraw stCELO from the protocol and transfer it into unlocked CELO a waiting period of 3 days has to elapse. This is based on the implementation of the Celo protocol (specifically the ) and the same waiting period as if the CELO were staked directly.

    How does unstaking work?

    After a user unstakes stCELO, the WebApp ensures that all the votes for the validator groups are withdrawn. Then the three day waiting period starts. After this has ended, the WebApp automatically claims the now available CELO, which then appears in the user’s wallet (see “” in the documentation for details). The claiming should take about 5 minutes. If it takes longer, the WebApp backend is not working properly. Feel free to contact the core engineering team and protocol validators on Discord.

    Why do I see “Still claiming…” after unstaking stCELO and waiting for three days?

    See the FAQ “”.

    What validator groups does StakedCelo vote for?

    StakedCelo votes for a group of randomly selected validator groups, adhering to the principles of increasing decentralization and on-chain verifiable neutrality. The details of the selection process are described .

    What are the risks of staking with StakedCelo?

    There exist a number of potential risks when staking with StakedCelo, in particular smart contract security, which we discuss in more detail below. There is an inherent risk that StakedCelo could contain a smart contract vulnerability or bug. The code is open source and has been audited, however, this provides no guarantee that it will always perform as intended. Additionally, independently of how CELO is staked, general staking risks apply. In particular, the underlying tokens have to be locked up for 3 days, meaning that a user is dependent on secondary markets for stCELO for instant liquidity. To access a predictable exchange rate of stCELO -> CELO withdrawing from the StakedProtocol can be used, however, in that case the withdrawal period of 3 days has to be awaited.

    What fee is applied by StakedCelo?

    At this time, there are no fees. This might change at a later point in time and will be enacted through on-chain code changes.

    Epoch Rewards
    Epoch Rewards
    Epoch Rewards
    Locked CELO
    automatically compounded
    Epoch Rewards
    here.
    Locked Gold contract
    Deposit and withdrawal flows
    How does unstaking work?
    here

    Account

    Account.sol

    This contract sets up an account in the core Accounts contract, enabling it to lock CELO and vote in validator elections. The system's pool of CELO is held by this contract. This contract needs to be interacted with to lock/vote/activate votes, as assigned to validator groups according to Manager's strategy, and to finalize withdrawals of CELO, after the unlocking period of LockedGold has elapsed.

    • Source Code

    • Mainnet Deployment:

    • Alfajores Testnet Deployment:

    Methods

    initialize

    • _registry The address of the Celo registry.

    • _manager The address of the Manager contract.

    • _owner The address of the contract owner.

    scheduleVotes

    Deposits CELO sent via msg.value as unlocked CELO intended as votes for groups. Only callable by the Manager contract, which must restrict which groups are valid.

    scheduleWithdrawals

    Schedule a list of withdrawals to be refunded to a beneficiary.

    withdraw

    Starts withdrawal of CELO from `group`. If there is any unlocked CELO for the group, that CELO is used for immediate withdrawal. Otherwise, CELO is taken from pending and active votes, which are subject to the unlock period of LockedGold.sol.

    activateAndVote

    Activates any activatable pending votes for group, and locks & votes any unlocked CELO for group. Callable by anyone. In practice, this is expected to be called near the end of each epoch by an off-chain agent.

    finishPendingWithdrawal

    Finishes a pending withdrawal created as a result of a `Account.withdraw` call, claiming CELO after the `unlockingPeriod` defined in LockedGold.sol. Callable by anyone, but ultimately the withdrawal goes to `beneficiary`. The pending withdrawal info found in both Account.sol and LockedGold must match to ensure that the beneficiary is claiming the appropriate pending withdrawal.

    getTotalCelo

    Gets the total amount of CELO this contract controls. This is the unlocked CELO balance of the contract plus the amount of LockedGold for this contract, which includes unvoting and voting LockedGold.

    Returns the total amount of CELO this contract controls, including LockedGold.

    getPendingWithdrawals

    Returns the pending withdrawals for a beneficiary.

    getNumberPendingWithdrawals

    Returns the number of pending withdrawals for a beneficiary.

    getPendingWithdrawal

    Returns a pending withdrawal for a beneficiary.

    getCeloForGroup

    Returns the total amount of CELO directed towards `group`. This is the Unlocked CELO balance for `group` plus the combined amount in pending and active votes made by this contract.

    scheduledVotesForGroup

    Returns the total amount of CELO that's scheduled to vote for a group.

    scheduledWithdrawalsForGroup

    Returns the total amount of CELO that's scheduled to be withdrawn for a group.

    scheduledWithdrawalsForGroupAndBeneficiary

    Returns the total amount of CELO that's scheduled to be withdrawn for a group scoped by a beneficiary.

    revokeVotes

    Revokes votes from a validator group. It first attempts to revoke pending votes, and then active votes if necessary. Reverts if `revokeAmount` exceeds the total number of pending and active votes for the group from this contract.

    validatePendingWithdrawalRequest

    Validates a local pending withdrawal matches a given beneficiary and LockedGold pending withdrawal. See finishPendingWithdrawal.

    0x4aAD04D41FD7fd495503731C5a2579e19054C432
    0xd11CC172D802c1a94e81c5F432471bD34d1828A1
        function initialize(
            address _registry,
            address _manager,
            address _owner
        ) external initializer {
            __UsingRegistry_init(_registry);
            __Managed_init(_manager);
            _transferOwnership(_owner);
    
            // Create an account so this contract can vote.
            if (!getAccounts().createAccount()) {
                revert AccountCreationFailed();
            }
        }
    
        // solhint-disable-next-line no-empty-blocks
        receive() external payable {}
    
    function scheduleVotes(address[] calldata groups, uint256[] calldata votes)
            external
            payable
            onlyManager
        {
            if (groups.length != votes.length) {
                revert GroupsAndVotesArrayLengthsMismatch();
            }
    
            uint256 totalVotes;
            for (uint256 i = 0; i < groups.length; i++) {
                scheduledVotes[groups[i]].toVote += votes[i];
                totalVotes += votes[i];
                emit VotesScheduled(groups[i], votes[i]);
            }
    
            if (totalVotes != uint256(msg.value)) {
                revert TotalVotesMismatch(msg.value, totalVotes);
            }
        }
    function scheduleWithdrawals(
            address beneficiary,
            address[] calldata groups,
            uint256[] calldata withdrawals
        ) external payable onlyManager {
            if (groups.length != withdrawals.length) {
                revert GroupsAndVotesArrayLengthsMismatch();
            }
    
            uint256 totalWithdrawalsDelta;
    
            for (uint256 i = 0; i < withdrawals.length; i++) {
                uint256 celoAvailableForGroup = this.getCeloForGroup(groups[i]);
                if (celoAvailableForGroup < withdrawals[i]) {
                    revert WithdrawalAmountTooHigh(groups[i], celoAvailableForGroup, withdrawals[i]);
                }
    
                scheduledVotes[groups[i]].toWithdraw += withdrawals[i];
                scheduledVotes[groups[i]].toWithdrawFor[beneficiary] += withdrawals[i];
                totalWithdrawalsDelta += withdrawals[i];
    
                emit CeloWithdrawalScheduled(beneficiary, groups[i], withdrawals[i]);
            }
    function withdraw(
            address beneficiary,
            address group,
            address lesserAfterPendingRevoke,
            address greaterAfterPendingRevoke,
            address lesserAfterActiveRevoke,
            address greaterAfterActiveRevoke,
            uint256 index
        ) external returns (uint256) {
            uint256 withdrawalAmount = scheduledVotes[group].toWithdrawFor[beneficiary];
            if (withdrawalAmount == 0) {
                revert NoScheduledWithdrawal(beneficiary, group);
            }
            // Emit early to return without needing to emit in multiple places.
            emit CeloWithdrawalStarted(beneficiary, group, withdrawalAmount);
            // Subtract withdrawal amount from all bookkeeping
            scheduledVotes[group].toWithdrawFor[beneficiary] = 0;
            scheduledVotes[group].toWithdraw -= withdrawalAmount;
            totalScheduledWithdrawals -= withdrawalAmount;
    
            uint256 immediateWithdrawalAmount = scheduledVotes[group].toVote;
    
            if (immediateWithdrawalAmount > 0) {
                if (immediateWithdrawalAmount > withdrawalAmount) {
                    immediateWithdrawalAmount = withdrawalAmount;
                }
    
                scheduledVotes[group].toVote -= immediateWithdrawalAmount;
    
                // The benefit of using getGoldToken().transfer() rather than transferring
                // using a message value is that the recepient's callback is not called, thus
                // removing concern that a malicious beneficiary would control code at this point.
                bool success = getGoldToken().transfer(beneficiary, immediateWithdrawalAmount);
                if (!success) {
                    revert CeloTransferFailed(beneficiary, immediateWithdrawalAmount);
                }
                // If we've withdrawn the entire amount, return.
                if (immediateWithdrawalAmount == withdrawalAmount) {
                    return immediateWithdrawalAmount;
                }
            }
    
            // We know that withdrawalAmount is >= immediateWithdrawalAmount.
            uint256 revokeAmount = withdrawalAmount - immediateWithdrawalAmount;
    
            ILockedGold lockedGold = getLockedGold();
    
            // Save the pending withdrawal for `beneficiary`.
            pendingWithdrawals[beneficiary].push(
                PendingWithdrawal(revokeAmount, block.timestamp + lockedGold.unlockingPeriod())
            );
    
            revokeVotes(
                group,
                revokeAmount,
                lesserAfterPendingRevoke,
                greaterAfterPendingRevoke,
                lesserAfterActiveRevoke,
                greaterAfterActiveRevoke,
                index
            );
    
            lockedGold.unlock(revokeAmount);
    
            return immediateWithdrawalAmount;
        }
     function activateAndVote(
            address group,
            address voteLesser,
            address voteGreater
        ) external {
            IElection election = getElection();
    
            // The amount of unlocked CELO for group that we want to lock and vote with.
            uint256 unlockedCeloForGroup = scheduledVotes[group].toVote;
    
            // Reset the unlocked CELO amount for group.
            scheduledVotes[group].toVote = 0;
    
            // If there are activatable pending votes from this contract for group, activate them.
            if (election.hasActivatablePendingVotes(address(this), group)) {
                // Revert if the activation fails.
                if (!election.activate(group)) {
                    revert ActivatePendingVotesFailed(group);
                }
            }
    
            // If there is no CELO to lock up and vote with, return.
            if (unlockedCeloForGroup == 0) {
                return;
            }
    
            // Lock up the unlockedCeloForGroup in LockedGold, which increments the
            // non-voting LockedGold balance for this contract.
            getLockedGold().lock{value: unlockedCeloForGroup}();
    
            // Vote for group using the newly locked CELO, reverting if it fails.
            if (!election.vote(group, unlockedCeloForGroup, voteLesser, voteGreater)) {
                revert VoteFailed(group, unlockedCeloForGroup);
            }
        }
    function finishPendingWithdrawal(
            address beneficiary,
            uint256 localPendingWithdrawalIndex,
            uint256 lockedGoldPendingWithdrawalIndex
        ) external returns (uint256 amount) {
            (uint256 value, uint256 timestamp) = validatePendingWithdrawalRequest(
                beneficiary,
                localPendingWithdrawalIndex,
                lockedGoldPendingWithdrawalIndex
            );
    
            // Remove the pending withdrawal.
            PendingWithdrawal[] storage localPendingWithdrawals = pendingWithdrawals[beneficiary];
            localPendingWithdrawals[localPendingWithdrawalIndex] = localPendingWithdrawals[
                localPendingWithdrawals.length - 1
            ];
            localPendingWithdrawals.pop();
    
            // Process withdrawal.
            getLockedGold().withdraw(lockedGoldPendingWithdrawalIndex);
    
            /**
             * The benefit of using getGoldToken().transfer() is that the recepients callback
             * is not called thus removing concern that a malicious
             * caller would control code at this point.
             */
            bool success = getGoldToken().transfer(beneficiary, value);
            if (!success) {
                revert CeloTransferFailed(beneficiary, value);
            }
    
            emit CeloWithdrawalFinished(beneficiary, value, timestamp);
            return value;
        }
    function getTotalCelo() external view returns (uint256) {
            // LockedGold's getAccountTotalLockedGold returns any non-voting locked gold +
            // voting locked gold for each group the account is voting for, which is an
            // O(# of groups voted for) operation.
            return
                address(this).balance +
                getLockedGold().getAccountTotalLockedGold(address(this)) -
                totalScheduledWithdrawals;
        }
    function getPendingWithdrawals(address beneficiary)
            external
            view
            returns (uint256[] memory values, uint256[] memory timestamps)
        {
            uint256 length = pendingWithdrawals[beneficiary].length;
            values = new uint256[](length);
            timestamps = new uint256[](length);
    
            for (uint256 i = 0; i < length; i++) {
                PendingWithdrawal memory p = pendingWithdrawals[beneficiary][i];
                values[i] = p.value;
                timestamps[i] = p.timestamp;
            }
    
            return (values, timestamps);
        }
     function getNumberPendingWithdrawals(address beneficiary) external view returns (uint256) {
            return pendingWithdrawals[beneficiary].length;
        }
    function getPendingWithdrawal(address beneficiary, uint256 index)
            external
            view
            returns (uint256 value, uint256 timestamp)
        {
            PendingWithdrawal memory withdrawal = pendingWithdrawals[beneficiary][index];
    
            return (withdrawal.value, withdrawal.timestamp);
        }
    function getCeloForGroup(address group) external view returns (uint256) {
            return
                getElection().getTotalVotesForGroupByAccount(group, address(this)) +
                scheduledVotes[group].toVote -
                scheduledVotes[group].toWithdraw;
        }
    function scheduledVotesForGroup(address group) external view returns (uint256) {
            return scheduledVotes[group].toVote;
        }
     function scheduledWithdrawalsForGroup(address group) external view returns (uint256) {
            return scheduledVotes[group].toWithdraw;
        }
    function scheduledWithdrawalsForGroupAndBeneficiary(address group, address beneficiary)
            external
            view
            returns (uint256)
        {
            return scheduledVotes[group].toWithdrawFor[beneficiary];
        }
     function revokeVotes(
            address group,
            uint256 revokeAmount,
            address lesserAfterPendingRevoke,
            address greaterAfterPendingRevoke,
            address lesserAfterActiveRevoke,
            address greaterAfterActiveRevoke,
            uint256 index
        ) internal {
            IElection election = getElection();
            uint256 pendingVotesAmount = election.getPendingVotesForGroupByAccount(
                group,
                address(this)
            );
    
            uint256 toRevokeFromPending = Math.min(revokeAmount, pendingVotesAmount);
            if (toRevokeFromPending > 0) {
                if (
                    !election.revokePending(
                        group,
                        toRevokeFromPending,
                        lesserAfterPendingRevoke,
                        greaterAfterPendingRevoke,
                        index
                    )
                ) {
                    revert RevokePendingFailed(group, revokeAmount);
                }
            }
    
            uint256 toRevokeFromActive = revokeAmount - toRevokeFromPending;
            if (toRevokeFromActive == 0) {
                return;
            }
    
            uint256 activeVotesAmount = election.getActiveVotesForGroupByAccount(group, address(this));
    
            if (activeVotesAmount < toRevokeFromActive) {
                revert InsufficientRevokableVotes(group, revokeAmount);
            }
    
            if (
                !election.revokeActive(
                    group,
                    toRevokeFromActive,
                    lesserAfterActiveRevoke,
                    greaterAfterActiveRevoke,
                    index
                )
            ) {
                revert RevokeActiveFailed(group, revokeAmount);
            }
        }
    function validatePendingWithdrawalRequest(
            address beneficiary,
            uint256 localPendingWithdrawalIndex,
            uint256 lockedGoldPendingWithdrawalIndex
        ) internal view returns (uint256 value, uint256 timestamp) {
            if (localPendingWithdrawalIndex >= pendingWithdrawals[beneficiary].length) {
                revert PendingWithdrawalIndexTooHigh(
                    localPendingWithdrawalIndex,
                    pendingWithdrawals[beneficiary].length
                );
            }
    
            (
                uint256 lockedGoldPendingWithdrawalValue,
                uint256 lockedGoldPendingWithdrawalTimestamp
            ) = getLockedGold().getPendingWithdrawal(address(this), lockedGoldPendingWithdrawalIndex);
    
            PendingWithdrawal memory pendingWithdrawal = pendingWithdrawals[beneficiary][
                localPendingWithdrawalIndex
            ];
    
            if (pendingWithdrawal.value != lockedGoldPendingWithdrawalValue) {
                revert InconsistentPendingWithdrawalValues(
                    pendingWithdrawal.value,
                    lockedGoldPendingWithdrawalValue
                );
            }
    
            if (pendingWithdrawal.timestamp != lockedGoldPendingWithdrawalTimestamp) {
                revert InconsistentPendingWithdrawalTimestamps(
                    pendingWithdrawal.timestamp,
                    lockedGoldPendingWithdrawalTimestamp
                );
            }
    
            return (pendingWithdrawal.value, pendingWithdrawal.timestamp);
        }
    }