Skip to content

Commit

Permalink
[MoveLib] Add Delegation contract (MystenLabs#1575)
Browse files Browse the repository at this point in the history
  • Loading branch information
lxfind authored May 11, 2022
1 parent a542405 commit 490d2a7
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 45 deletions.
1 change: 1 addition & 0 deletions sui_programmability/framework/sources/Balance.move
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/// that needs to hold coins.
module Sui::Balance {
friend Sui::Coin;
friend Sui::SuiSystem;

/// For when trying to destroy a non-zero balance.
const ENonZero: u64 = 0;
Expand Down
139 changes: 139 additions & 0 deletions sui_programmability/framework/sources/Governance/Delegation.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module Sui::Delegation {
use Std::Option::{Self, Option};
use Sui::Balance::Balance;
use Sui::Coin::{Self, Coin};
use Sui::ID::{Self, VersionedID};
use Sui::SUI::SUI;
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};

friend Sui::SuiSystem;

/// A custodial delegation object. When the delegation is active, the delegation
/// object holds the delegated stake coin. It also contains the delegation
/// target validator address.
/// The delegation object is required to claim delegation reward. The object
/// keeps track of the next reward unclaimed epoch. One can only claim reward
/// for the epoch that matches the `next_reward_unclaimed_epoch`.
/// When the delegation is deactivated, we keep track of the ending epoch
/// so that we know the ending epoch that the delegator can still claim reward.
struct Delegation has key {
id: VersionedID,
/// The delegated stake, if the delegate is still active
active_delegation: Option<Balance<SUI>>,
/// If the delegation is inactive, `ending_epoch` will be
/// set to the ending epoch, i.e. the epoch when the delegation
/// was withdrawn. Delegator will not be eligible to claim reward
/// for ending_epoch and after.
ending_epoch: Option<u64>,
/// The delegated stake amount.
delegate_amount: u64,
/// Delegator is able to claim reward epoch by epoch. `next_reward_unclaimed_epoch`
/// is the next epoch that the delegator can claim epoch. Whenever the delegator
/// claims reward for an epoch, this value increments by one.
next_reward_unclaimed_epoch: u64,
/// The delegation target validator.
validator_address: address,
}

public(friend) fun create(
starting_epoch: u64,
validator_address: address,
stake: Coin<SUI>,
ctx: &mut TxContext,
) {
let delegate_amount = Coin::value(&stake);
let delegation = Delegation {
id: TxContext::new_id(ctx),
active_delegation: Option::some(Coin::into_balance(stake)),
ending_epoch: Option::none(),
delegate_amount,
next_reward_unclaimed_epoch: starting_epoch,
validator_address,
};
Transfer::transfer(delegation, TxContext::sender(ctx))
}

/// Deactivate the delegation. Send back the stake and set the ending epoch.
public(friend) fun undelegate(
self: &mut Delegation,
ending_epoch: u64,
ctx: &mut TxContext,
) {
assert!(is_active(self), 0);
assert!(ending_epoch >= self.next_reward_unclaimed_epoch, 0);

let stake = Option::extract(&mut self.active_delegation);
let sender = TxContext::sender(ctx);
Transfer::transfer(Coin::from_balance(stake, ctx), sender);

self.ending_epoch = Option::some(ending_epoch);
}

/// Claim delegation reward. Increment next_reward_unclaimed_epoch.
public(friend) fun claim_reward(
self: &mut Delegation,
reward: Balance<SUI>,
ctx: &mut TxContext,
) {
let sender = TxContext::sender(ctx);
Coin::transfer(Coin::from_balance(reward, ctx), sender);
self.next_reward_unclaimed_epoch = self.next_reward_unclaimed_epoch + 1;
}


/// Destroy the delegation object. This can be done only when the delegation
/// is inactive and all reward have been claimed.
public(script) fun burn(self: Delegation, _ctx: &mut TxContext) {
assert!(!is_active(&self), 0);

let Delegation {
id,
active_delegation,
ending_epoch,
delegate_amount: _,
next_reward_unclaimed_epoch,
validator_address: _,
} = self;
ID::delete(id);
Option::destroy_none(active_delegation);
let ending_epoch = *Option::borrow(&ending_epoch);
assert!(next_reward_unclaimed_epoch == ending_epoch, 0);
}

public(script) fun transfer(self: Delegation, recipient: address, _ctx: &mut TxContext) {
Transfer::transfer(self, recipient)
}

/// Checks whether the delegation object is eligible to claim the reward
/// given the epoch to claim and the validator address.
public fun can_claim_reward(
self: &Delegation,
epoch_to_claim: u64,
validator: address,
): bool {
if (validator != self.validator_address) {
false
} else if (is_active(self)) {
self.next_reward_unclaimed_epoch <= epoch_to_claim
} else {
let ending_epoch = *Option::borrow(&self.ending_epoch);
ending_epoch > epoch_to_claim
}
}

public fun validator(self: &Delegation): address {
self.validator_address
}

public fun delegate_amount(self: &Delegation): u64 {
self.delegate_amount
}

fun is_active(self: &Delegation): bool {
Option::is_some(&self.active_delegation) && Option::is_none(&self.ending_epoch)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module Sui::EpochRewardRecord {
use Sui::ID::VersionedID;
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};

friend Sui::SuiSystem;
friend Sui::ValidatorSet;

/// EpochRewardRecord is an immutable record created per epoch per active validator.
/// Sufficient information is saved in the record so that delegators can claim
/// delegation rewards from past epochs, and for validators that may no longer be active.
/// TODO: For now we assume that validators don't charge an extra fee.
/// Delegation reward is simply proportional to to overall delegation reward ratio
/// and the delegation amount.
struct EpochRewardRecord has key {
id: VersionedID,
epoch: u64,
computation_charge: u64,
total_stake: u64,
delegator_count: u64,
validator: address,
}

public(friend) fun create(
epoch: u64,
computation_charge: u64,
total_stake: u64,
delegator_count: u64,
validator: address,
ctx: &mut TxContext,
) {
Transfer::share_object(EpochRewardRecord {
id: TxContext::new_id(ctx),
epoch,
computation_charge,
total_stake,
delegator_count,
validator,
})
}

/// Given the delegation amount, calculate the reward, and decrement the `delegator_count`.
public(friend) fun claim_reward(self: &mut EpochRewardRecord, delegation_amount: u64): u64 {
self.delegator_count = self.delegator_count - 1;
// TODO: Once self.delegator_count reaches 0, we should be able to delete this object.
delegation_amount * self.computation_charge / self.total_stake
}

public fun epoch(self: &EpochRewardRecord): u64 {
self.epoch
}

public fun validator(self: &EpochRewardRecord): address {
self.validator
}


}
93 changes: 90 additions & 3 deletions sui_programmability/framework/sources/Governance/SuiSystem.move
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0

module Sui::SuiSystem {
use Sui::Balance::{Self, Balance};
use Sui::Coin::{Self, Coin, TreasuryCap};
use Sui::Balance::Balance;
use Sui::Delegation::{Self, Delegation};
use Sui::EpochRewardRecord::{Self, EpochRewardRecord};
use Sui::ID::VersionedID;
use Sui::SUI::SUI;
use Sui::Transfer;
Expand All @@ -17,6 +19,8 @@ module Sui::SuiSystem {
// TDOO: We will likely add more, a few potential ones:
// - the change in stake across epochs can be at most +/- x%
// - the change in the validator set across epochs can be at most x validators
//
// TODO: The stake threshold should be % threshold instead of amount threshold.
struct SystemParameters has store {
/// Lower-bound on the amount of stake required to become a validator.
min_validator_stake: u64,
Expand All @@ -40,6 +44,9 @@ module Sui::SuiSystem {
storage_fund: Balance<SUI>,
/// A list of system config parameters.
parameters: SystemParameters,
/// The delegation reward pool. All delegation reward goes into this.
/// Delegation reward claims withdraw from this.
delegation_reward: Balance<SUI>,
}

// ==== functions that can only be called by Genesis ====
Expand Down Expand Up @@ -67,6 +74,7 @@ module Sui::SuiSystem {
max_validator_stake,
max_validator_candidate_count,
},
delegation_reward: Balance::zero(),
};
Transfer::share_object(state);
}
Expand All @@ -86,7 +94,7 @@ module Sui::SuiSystem {
ctx: &mut TxContext,
) {
assert!(
ValidatorSet::get_total_validator_candidate_count(&self.validators) < self.parameters.max_validator_candidate_count,
ValidatorSet::total_validator_candidate_count(&self.validators) < self.parameters.max_validator_candidate_count,
0
);
let stake_amount = Coin::value(&stake);
Expand Down Expand Up @@ -152,21 +160,100 @@ module Sui::SuiSystem {
)
}

public(script) fun request_add_delegation(
self: &mut SuiSystemState,
delegate_stake: Coin<SUI>,
validator_address: address,
ctx: &mut TxContext,
) {
let amount = Coin::value(&delegate_stake);
ValidatorSet::request_add_delegation(&mut self.validators, validator_address, amount);

// Delegation starts from the next epoch.
let starting_epoch = self.epoch + 1;
Delegation::create(starting_epoch, validator_address, delegate_stake, ctx);
}

public(script) fun request_remove_delegation(
self: &mut SuiSystemState,
delegation: &mut Delegation,
ctx: &mut TxContext,
) {
ValidatorSet::request_remove_delegation(
&mut self.validators,
Delegation::validator(delegation),
Delegation::delegate_amount(delegation),
);
Delegation::undelegate(delegation, self.epoch, ctx)
}

// TODO: Once we support passing vector of object references as arguments,
// we should support passing a vector of &mut EpochRewardRecord,
// which will allow delegators to claim all their reward in one transaction.
public(script) fun claim_delegation_reward(
self: &mut SuiSystemState,
delegation: &mut Delegation,
epoch_reward_record: &mut EpochRewardRecord,
ctx: &mut TxContext,
) {
let epoch = EpochRewardRecord::epoch(epoch_reward_record);
let validator = EpochRewardRecord::validator(epoch_reward_record);
assert!(Delegation::can_claim_reward(delegation, epoch, validator), 0);
let reward_amount = EpochRewardRecord::claim_reward(
epoch_reward_record,
Delegation::delegate_amount(delegation),
);
let reward = Balance::split(&mut self.delegation_reward, reward_amount);
Delegation::claim_reward(delegation, reward, ctx);
}

/// This function should be called at the end of an epoch, and advances the system to the next epoch.
/// It does the following things:
/// 1. Add storage charge to the storage fund.
/// 2. Distribute computation charge to validator stake and delegation stake.
/// 3. Create reward information records for each validator in this epoch.
/// 4. Update all validators.
public(script) fun advance_epoch(
self: &mut SuiSystemState,
new_epoch: u64,
storage_charge: u64,
computation_charge: u64,
ctx: &mut TxContext,
) {
// Only an active validator can make a call to this function.
assert!(ValidatorSet::is_active_validator(&self.validators, TxContext::sender(ctx)), 0);

let storage_reward = Balance::create_with_value(storage_charge);
let computation_reward = Balance::create_with_value(computation_charge);

let delegation_stake = ValidatorSet::delegation_stake(&self.validators);
let validator_stake = ValidatorSet::validator_stake(&self.validators);
let storage_fund = Balance::value(&self.storage_fund);
let total_stake = delegation_stake + validator_stake + storage_fund;

let delegator_reward_amount = delegation_stake * computation_charge / total_stake;
let delegator_reward = Balance::split(&mut computation_reward, delegator_reward_amount);
Balance::join(&mut self.storage_fund, storage_reward);
Balance::join(&mut self.delegation_reward, delegator_reward);

ValidatorSet::create_epoch_records(
&self.validators,
self.epoch,
computation_charge,
total_stake,
ctx,
);

self.epoch = self.epoch + 1;
// Sanity check to make sure we are advancing to the right epoch.
assert!(new_epoch == self.epoch, 0);
ValidatorSet::advance_epoch(
&mut self.validators,
&mut computation_reward,
ctx,
)
);
// Because of precision issues with integer divisions, we expect that there will be some
// remaining balance in `computation_reward`. All of these go to the storage fund.
Balance::join(&mut self.storage_fund, computation_reward)
}
}
Loading

0 comments on commit 490d2a7

Please sign in to comment.