We’re going to clone compound’s governance model and adapt it for our needs. In general, the process is the same: Token holders can make (executable) proposals if they possess enough voting power, vote on proposals during a predefined voting period and in the end evaluate the outcome. If successful, the proposal will be scheduled on the timelock contract. Only after sufficient time passed, it can be executed. A minimum voting power is required for making a proposal as well as a minimum quorum. Here is a more detailed explanation of compounds model: https://drive.google.com/file/d/1ekw4YWEt6AJd7rKr0esynJy1IeZZb-GK/view?usp=sharing
It’s also worth checking out Ponjinges analysis: https://hackmd.io/eLCgFHT3QEKre9bf_nyCOA?view#Governance-Brainstorming
The most relevant change we’re introducing is a staking contract. In contrast to compound, we only grant voting rights to staked tokens. Therefore, the staking contract needs to contain the relevant logic from the Comp token contract (vote checkpointing, delegating), but consider that the voting power depends on the length of the staking period.
When staking, the user needs to decide for how long. The tokens will be locked on the smart contract for the selected period. The more time lies between the time of voting and the date of unlocking, the higher the voting power.
The weighting function should be of quadratic nature. The function proposed in this document is merely a suggestion and can be replaced by any other function.
Let m be the maximum staking period (1095 days) , x be the number of days passed since the beginning of the period and V be the maximum voting weight (minus one). The voting weight at a given time x can be described as:
f(x) = V *(m2 -x2) / m2 +1
+1 is added in the end to shift the weights to lie in [1, V+1] instead of [0, V].
Users will usually not stake the full period, so x has to be computed based on the remaining days until unstaking.
x = m-remaining days
The user’s voting power VP at a given time x is the product of his stake s and his voting weight:
VP(x) = f(x) * s
The total voting power equals the sum of the voting power of all users. However, since we can’t iterate over an array with unlimited size, we need to compute it differently. Instead of summing up the user voting powers, we introduce a mapping, which stores the total amount of tokens to be unstaked on a given day, and call it stakedUntil.
Whenever a user stakes tokens, not only is his staking balance updated, but also stakedUntil[unstakingDay]. Whenever a user increases his staking balance, the mapping also needs to be increased. Whenever a user increases his staking time, his stake is subtracted from stakedUntil[previousUnstakingDay] and added to stakedUntil[newUnstakingDay].
Now, we can compute the voting power of all tokens staked until a given point in time in a single operation. The daily voting power DVP is given by:
DVP(x) = f(x) * usu(x), where usu(x)is the content of stakedUntil[x].
The total voting power can then be computed by summing up the daily voting powers of the next m days.
TVP = x=0mDVP(x)
This process requires m iterations. With a maximum staking period of 3 years, this would cost approximately 1M Gas. In order to save gas cost, we decided to stake in bi-weekly periods instead. As a consequence, voting weights are only adjusted every 2 weeks and we need a maximum of 78 iterations (instead of 1095).
The stakedUntil mapping needs to be checkpointed to allow the computation of the total voting power for a point of time in the past. The same is true for the user stakes.
Users are able to delegate their voting power to another address. Therefore, an address can possess a possibly large number of delegators, each of them staking for different periods. Therefore, the voting power of a delegate needs to be computed the same way the total voting power is computed: as a sum of the voting powers per unstaking day. This makes additional checkpoints necessary.
Staking is not only granting voting rights, but also access to fee sharing according to the own voting power in relation to the total. Whenever somebody decides to collect the fees from the protocol, they get transferred to a proxy contract which invests the funds in the lending pool and keeps the pool tokens.
The fee sharing proxy will be set as feesController of the protocol contract. This allows the fee sharing proxy to withdraw the fees. The process is depicted below.
The fee sharing proxy holds the pool tokens and keeps track of which user owns how many tokens. In order to know how many tokens a user owns, the fee sharing proxy needs to know the user’s weighted stake in relation to the total weighted stake (aka total voting power). Because both values are subject to change, they may be different on each fee withdrawal. To be able to calculate a user’s share of tokens when he wants to withdraw, we need checkpoints.
Whenever fees are withdrawn, the staking contract needs to checkpoint the block number, the number of pool tokens and the total voting power at that time (read from the staking contract). While the total voting power would not necessarily need to be checkpointed, it makes sense to save gas cost on withdrawal.
/// checkpoints by index per pool token address
mapping(address => mapping(uint => Checkpoint)) checkpoints;
struct Checkpoint{
uint32 blockNumber,
uint96 totalWeightedStake,
uint128 numTokens
}
When the user wants to withdraw his share of tokens, we need to iterate over all of the checkpoints since the users last withdrawal (note: remember last withdrawal block), query the user’s balance at the checkpoint blocks from the staking contract, compute his share of the checkpointed tokens and add them up. The maximum number of checkpoints to process at once should be limited.
Withdrawal should only be possible for blocks which were already mined. means: if the fees are withdrawn in the same block as the user withdrawal, they are not considered by the withdrawing logic (to avoid inconsistencies).
Team tokens and investor tokens are vested. Therefore, a smart contract needs to be developed to enforce the vesting schedule.
For team tokens, the vesting contract additionally needs to enable the owner (the governance) to transfer the remaining vesting tokens to another address. Team vested tokens are automatically staked in governance and get unlocked according to the vesting schedule. Governance (the owner of a staking contract) is able to withdraw locked tokens for vesting contracts. Diagram below describes this process.
Investor tokens are not staked in governance, the vesting contract holds tokens. Investor tokens can be vested according to the schedule or deposited directly. Vested tokens will be unlocked by the given schedule. Deposited tokens can be withdrawn any time. Vesting schedules can be changed by the owner (the governance). Tokens will be unlocked immediately for a "zero schedule" (cliff = 0, duration = 0, frequency = 0). Investor tokens can be vested many times. Each tranche will have the same vesting schedule, but own start date.
Staking contract allows an address to have N (up to 78) unlocking dates (1 per 2 weeks).
Amount of stake can be increased. Each stake can be extended (moved to another date).