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.

Methods

initialize

Initialize the contract with registry and owner. .

 function initialize(address _registry, address _owner) external initializer {
        _transferOwnership(_owner);
        __UsingRegistry_init(_registry);
    }

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.

   function setDependencies(address _stakedCelo, address _account) external onlyOwner {
        stakedCelo = IStakedCelo(_stakedCelo);
        account = IAccount(_account);
    }

activateGroup

Marks a group as votable.

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

getGroups

Returns the array of active groups.

function getGroups() external view returns (address[] memory) {
        return activeGroups.values();
    }

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.

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

getDeprecatedGroups

Returns the list of deprecated groups.

 function getDeprecatedGroups() external view returns (address[] memory) {
        return deprecatedGroups.values();
    }

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.

  function deposit() external payable {
        if (activeGroups.length() == 0) {
            revert NoActiveGroups();
        }

        stakedCelo.mint(msg.sender, toStakedCelo(msg.value));

        distributeVotes(msg.value);
    }

withdraw

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

 function withdraw(uint256 stakedCeloAmount) external {
        if (activeGroups.length() + deprecatedGroups.length() == 0) {
            revert NoGroups();
        }

        distributeWithdrawals(toCelo(stakedCeloAmount), msg.sender);

        stakedCelo.burn(msg.sender, stakedCeloAmount);
    }

toStakedCelo

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

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

toCelo

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

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

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.

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

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.

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

getDeprecatedGroupsWithdrawalDistribution

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

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

getActiveGroupWithdrawalDistribution

Calculates how votes should be withdrawn from each active group.

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

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.

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

getVotableGroups

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

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

sortGroupsWithVotes

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

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

Last updated