Profit合約:統一的分成管理方案
概要
因爲aelf主鏈採用DPoS共識機制,經過持有代幣或者鎖倉來得到權益是aelf治理模型中重要組成部分。這就產生了一個需求:實現一個可以統一管理分成的標準流程,並將其做爲一個基礎的智能合約。這個合約在創世區塊中即部署於鏈上,其應用包括但不限於:生產節點在某一屆任期結束時根據其區塊生產數量(以此做爲權重)得到相應獎勵,選民經過節點競選投票所質押ELF來分享相應的獎勵,DApp合約容許用戶經過抵押Token來分享合約盈利。緩存
分成方案即代幣分配策略:任何地址均可以成爲分成方案(profit scheme)的管理者(manager)。每一個管理者(manager)均可覺得該分成方案添加受益人(beneficiary),併爲每一個受益人設定股份(shares)。以後,當分成項目建立者對其項目受益人發放(distribute)分成時,將按其對應的股份進行代幣分配。每次分成結束後,分成方案的帳期(period)即加一,根據具體分成數額會在該帳期對應的虛擬地址(virtual address)上增長餘額,持有股份的帳戶能夠從中得到相應分成。分成的受益人不只能夠是帳戶地址,也能夠是另外一個分成方案,子分成方案所獲分成可直接打進其總帳(general ledger)。分成方案之間能夠進行級聯。app
進一步闡述幾個概念:less
分成方案(profit scheme):經過分成合約建立出來的代幣分配中心。ide
分成方案管理者(manager):管理分成方案的受益人及其相應股份。區塊鏈
分成受益人(beneficiary):aelf區塊鏈上用來接收分成代幣的帳戶地址。要注意的是,受益人的分成須要經過發送交易來獲取(需填入對應分成方案的id,交易由本身或他人發送皆可)。ui
分成方案虛擬地址(virtual address):每一個分成方案都會經過其惟一標識(scheme id)映射一個虛擬地址,這個地址僅用來釋放分成,沒有對應的公私鑰對(公私鑰對碰撞出來的機率能夠忽略不計)。this
子分成方案(sub profit item):這是一個相對概念,每一個分成方案均可能成爲子分成方案。子分成方案可持有其父分成方案股份,這樣父分成方案在釋放分成時,子分成方案的虛擬地址會得到相應代幣。google
獲取分成(claim profits):做爲一個普通用戶,須要自行發送交易來獲取本身應得的分成,這是爲了不註冊的接收地址過多,釋放分成的交易執行超時。code
股份(shares):股份是每一個分成受益人可以獲取相應分成比例的證實,即某受益人分成數量 = 總分成 * 該受益人持有股份 / 總股份。orm
發放分成(distribute profits):將分成方案虛擬地址上的餘額經過Bancor合約轉化爲ELF,並Transfer給分成接收地址的過程。
帳期(period):帳期時長由分成方案的管理者自行控制,發放分成後帳期自行增一。
國庫(Treasury):多是aelf區塊鏈中額度最大的分成方案,其主要管理者爲Treasury合約,另有兩個子分成方案相應的管理者爲Election合約。其分成額度來源於區塊生產獎勵,當前每生產一個區塊,分成額度即增長0.125個ELF,在本屆任期結束的時候統一打入Treasury scheme總帳,隨後Treasury合約和Election合約負責維護七個分成方案。
方法解讀
建立分成方案
顧名思義,該接口用來建立分成方案。接口以下:
rpc CreateScheme (CreateSchemeInput) returns (aelf.Hash) {
}
...
message CreateSchemeInput {
sint64 profit_receiving_due_period_count = 1;
bool is_release_all_balance_every_time_by_default = 2;
sint32 delay_distribute_period_count = 3;
aelf.Address manager = 4;
bool can_remove_beneficiary_directly = 5;
}
message Scheme {
aelf.Address virtual_address = 1;
sint64 total_shares = 2;
map<string, sint64> undistributed_profits = 3;// token symbol -> amount
sint64 current_period = 4;
repeated SchemeBeneficiaryShare sub_schemes = 5;
bool can_remove_beneficiary_directly = 6;
sint64 profit_receiving_due_period_count = 7;
bool is_release_all_balance_every_time_by_default = 8;
aelf.Hash scheme_id = 9;
sint32 delay_distribute_period_count = 10;
map<sint64, sint64> cached_delay_total_shares = 11;// period -> total shares, max elements count should be delay_distribute_period_count
aelf.Address manager = 12;
}
message SchemeBeneficiaryShare {
aelf.Hash scheme_id = 1;
sint64 shares = 2;
}
一個分成方案,包含如下屬性:
一個惟一的虛擬地址,做爲該分成方案的總帳地址;
總股份;
還沒有發放餘額(可能移除);
當前帳期期數;
子分成方案信息;
是否容許直接移除分成受益人(無視其可領取期數);
分成保留期數(過時即沒法領取);
默認發放總帳上某token對應的所有額度;
分成方案惟一標識;
延遲發放期數;
延遲發放分成所需緩存上的被推遲發放總股份;
管理者。
/// <summary>
/// Create a Scheme of profit distribution.
/// At the first time, the scheme's id is unknown,it may create by transaction id and createdSchemeIds;
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override Hash CreateScheme(CreateSchemeInput input)
{
ValidateContractState(State.TokenContract, SmartContractConstants.TokenContractSystemName);
if (input.ProfitReceivingDuePeriodCount == 0) { // 爲了不分成合約State信息過多,設置一個過時時間。 input.ProfitReceivingDuePeriodCount = ProfitContractConstants.DefaultProfitReceivingDuePeriodCount; } var manager = input.Manager ?? Context.Sender; var schemeId = Context.TransactionId; // Why? Because one transaction may create many profit items via inline transactions. var createdSchemeIds = State.ManagingSchemeIds[manager]?.SchemeIds; if (createdSchemeIds != null && createdSchemeIds.Contains(schemeId)) { // So we choose this way to avoid profit id conflicts in aforementioned situation. schemeId = Hash.FromTwoHashes(schemeId, createdSchemeIds.Last()); } var scheme = GetNewScheme(input, schemeId, manager); State.SchemeInfos[schemeId] = scheme; var schemeIds = State.ManagingSchemeIds[scheme.Manager]; if (schemeIds == null) { schemeIds = new CreatedSchemeIds { SchemeIds = {schemeId} }; } else { schemeIds.SchemeIds.Add(schemeId); } State.ManagingSchemeIds[scheme.Manager] = schemeIds; Context.LogDebug(() => $"Created scheme {State.SchemeInfos[schemeId]}"); Context.Fire(new SchemeCreated { SchemeId = scheme.SchemeId, Manager = scheme.Manager, IsReleaseAllBalanceEveryTimeByDefault = scheme.IsReleaseAllBalanceEveryTimeByDefault, ProfitReceivingDuePeriodCount = scheme.ProfitReceivingDuePeriodCount, VirtualAddress = scheme.VirtualAddress }); return schemeId;
}
子分成方案管理
用以添加和刪除子分成方案。
rpc AddSubScheme (AddSubSchemeInput) returns (google.protobuf.Empty) {
}
rpc RemoveSubScheme (RemoveSubSchemeInput) returns (google.protobuf.Empty) {
}
...
message AddSubSchemeInput {
aelf.Hash scheme_id = 1;
aelf.Hash sub_scheme_id = 2;
sint64 sub_scheme_shares = 3;
}
message RemoveSubSchemeInput {
aelf.Hash scheme_id = 1;
aelf.Hash sub_scheme_id = 2;
}
其中,添加子分成方案須要分別填入兩個發生級聯關係的分成方案的id,而後需輸入子分成項目所佔股份。而移除級聯關係只須要分別輸入兩個分成方案的id便可。
/// <summary>
/// Add a child to a existed scheme.
/// </summary>
/// <param name="input">AddSubSchemeInput</param>
/// <returns></returns>
public override Empty AddSubScheme(AddSubSchemeInput input)
{
Assert(input.SchemeId != input.SubSchemeId, "Two schemes cannot be same.");
Assert(input.SubSchemeShares > 0, "Shares of sub scheme should greater than 0.");
var scheme = State.SchemeInfos[input.SchemeId]; Assert(scheme != null, "Scheme not found."); Assert(Context.Sender == scheme.Manager, "Only manager can add sub-scheme."); var subSchemeId = input.SubSchemeId; var subScheme = State.SchemeInfos[subSchemeId]; Assert(subScheme != null, "Sub scheme not found."); var subItemVirtualAddress = Context.ConvertVirtualAddressToContractAddress(subSchemeId); // Add profit details and total shares of the father scheme. AddBeneficiary(new AddBeneficiaryInput { SchemeId = input.SchemeId, BeneficiaryShare = new BeneficiaryShare { Beneficiary = subItemVirtualAddress, Shares = input.SubSchemeShares }, EndPeriod = long.MaxValue }); // Add a sub profit item. scheme.SubSchemes.Add(new SchemeBeneficiaryShare { SchemeId = input.SubSchemeId, Shares = input.SubSchemeShares }); State.SchemeInfos[input.SchemeId] = scheme; return new Empty();
}
public override Empty RemoveSubScheme(RemoveSubSchemeInput input)
{
Assert(input.SchemeId != input.SubSchemeId, "Two schemes cannot be same.");
var scheme = State.SchemeInfos[input.SchemeId]; Assert(scheme != null, "Scheme not found."); if (scheme == null) return new Empty(); Assert(Context.Sender == scheme.Manager, "Only manager can remove sub-scheme."); var subSchemeId = input.SubSchemeId; var subScheme = State.SchemeInfos[subSchemeId]; Assert(subScheme != null, "Sub scheme not found."); if (subScheme == null) return new Empty(); var subSchemeVirtualAddress = Context.ConvertVirtualAddressToContractAddress(subSchemeId); // Remove profit details State.ProfitDetailsMap[input.SchemeId][subSchemeVirtualAddress] = new ProfitDetails(); var shares = scheme.SubSchemes.Single(d => d.SchemeId == input.SubSchemeId); scheme.SubSchemes.Remove(shares); scheme.TotalShares = scheme.TotalShares.Sub(shares.Shares); State.SchemeInfos[input.SchemeId] = scheme; return new Empty();
}
受益人管理
用以添加和刪除分成受益人,爲方便起見提供了批量管理接口。
rpc AddBeneficiary (AddBeneficiaryInput) returns (google.protobuf.Empty) {
}
rpc RemoveBeneficiary (RemoveBeneficiaryInput) returns (google.protobuf.Empty) {
}
rpc AddBeneficiaries (AddBeneficiariesInput) returns (google.protobuf.Empty) {
}
rpc RemoveBeneficiaries (RemoveBeneficiariesInput) returns (google.protobuf.Empty) {
}
...
message AddBeneficiaryInput {
aelf.Hash scheme_id = 1;
BeneficiaryShare beneficiary_share = 2;
sint64 end_period = 3;
}
message RemoveBeneficiaryInput {
aelf.Address beneficiary = 1;
aelf.Hash scheme_id = 2;
}
message AddBeneficiariesInput {
aelf.Hash scheme_id = 1;
repeated BeneficiaryShare beneficiary_shares = 2;
sint64 end_period = 4;
}
message RemoveBeneficiariesInput {
repeated aelf.Address beneficiaries = 1;
aelf.Hash scheme_id = 2;
}
message BeneficiaryShare {
aelf.Address beneficiary = 1;
sint64 shares = 2;
}
在添加分成受益人時,除了須要指定分成方案id、分成受益人地址和股份,還能夠指定該受益人可以接收分成的最後帳期期數,默認是一經添加,即能永久收到分成,直到分成方案管理者調用RemoveBeneficiary方法將其股份移除(若是該分成方案的can_remove_beneficiary_directly屬性值爲true,能夠直接移除所有股份)。
public override Empty AddBeneficiary(AddBeneficiaryInput input)
{
AssertValidInput(input);
if (input.BeneficiaryShare == null) return new Empty();
if (input.EndPeriod == 0) { // Which means this profit Beneficiary will never expired unless removed. input.EndPeriod = long.MaxValue; } var schemeId = input.SchemeId; var scheme = State.SchemeInfos[schemeId]; Assert(scheme != null, "Scheme not found."); if (scheme == null) return new Empty(); Assert( Context.Sender == scheme.Manager || Context.Sender == Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName), "Only manager can add beneficiary."); Context.LogDebug(() => $"{input.SchemeId}.\n End Period: {input.EndPeriod}, Current Period: {scheme.CurrentPeriod}"); Assert(input.EndPeriod >= scheme.CurrentPeriod, "Invalid end period."); scheme.TotalShares = scheme.TotalShares.Add(input.BeneficiaryShare.Shares); State.SchemeInfos[schemeId] = scheme; var profitDetail = new ProfitDetail { StartPeriod = scheme.CurrentPeriod.Add(scheme.DelayDistributePeriodCount), EndPeriod = input.EndPeriod, Shares = input.BeneficiaryShare.Shares, }; var currentProfitDetails = State.ProfitDetailsMap[schemeId][input.BeneficiaryShare.Beneficiary]; if (currentProfitDetails == null) { currentProfitDetails = new ProfitDetails { Details = {profitDetail} }; } else { currentProfitDetails.Details.Add(profitDetail); } // Remove details too old. foreach (var detail in currentProfitDetails.Details.Where( d => d.EndPeriod != long.MaxValue && d.LastProfitPeriod >= d.EndPeriod && d.EndPeriod.Add(scheme.ProfitReceivingDuePeriodCount) < scheme.CurrentPeriod)) { currentProfitDetails.Details.Remove(detail); } State.ProfitDetailsMap[schemeId][input.BeneficiaryShare.Beneficiary] = currentProfitDetails; Context.LogDebug(() => $"Added {input.BeneficiaryShare.Shares} weights to scheme {input.SchemeId.ToHex()}: {profitDetail}"); return new Empty();
}
public override Empty RemoveBeneficiary(RemoveBeneficiaryInput input)
{
Assert(input.SchemeId != null, "Invalid scheme id.");
Assert(input.Beneficiary != null, "Invalid Beneficiary address.");
var scheme = State.SchemeInfos[input.SchemeId]; Assert(scheme != null, "Scheme not found."); var currentDetail = State.ProfitDetailsMap[input.SchemeId][input.Beneficiary]; if (scheme == null || currentDetail == null) return new Empty(); Assert(Context.Sender == scheme.Manager || Context.Sender == Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName), "Only manager can remove beneficiary."); var expiryDetails = scheme.CanRemoveBeneficiaryDirectly ? currentDetail.Details.ToList() : currentDetail.Details .Where(d => d.EndPeriod < scheme.CurrentPeriod && !d.IsWeightRemoved).ToList(); if (!expiryDetails.Any()) return new Empty(); var shares = expiryDetails.Sum(d => d.Shares); foreach (var expiryDetail in expiryDetails) { expiryDetail.IsWeightRemoved = true; if (expiryDetail.LastProfitPeriod >= scheme.CurrentPeriod) { currentDetail.Details.Remove(expiryDetail); } } // Clear old profit details. if (currentDetail.Details.Count != 0) { State.ProfitDetailsMap[input.SchemeId][input.Beneficiary] = currentDetail; } else { State.ProfitDetailsMap[input.SchemeId].Remove(input.Beneficiary); } scheme.TotalShares = scheme.TotalShares.Sub(shares); State.SchemeInfos[input.SchemeId] = scheme; return new Empty();
}
添加(貢獻)分成
用於給指定分成項目增長必定數量可用來分成的代幣,幣種在添加時自行指定。(也就是說一個分成方案能夠發放多種token,不過每個分成帳期只能發放帳面上的一種token。)
rpc ContributeProfits (ContributeProfitsInput) returns (google.protobuf.Empty) {
}
...
message ContributeProfitsInput {
aelf.Hash scheme_id = 1;
sint64 amount = 2;
sint64 period = 3;
string symbol = 4;
}
當period爲0(爲空)時,這一筆代幣會添加到分成項目的虛擬地址上(能夠稱之爲總帳)。當指定了大於0的period時,這一筆代幣會添加到指定帳期的帳期虛擬地址上。
public override Empty ContributeProfits(ContributeProfitsInput input)
{
Assert(input.Symbol != null && input.Symbol.Any(), "Invalid token symbol.");
Assert(input.Amount > 0, "Amount need to greater than 0.");
if (input.Symbol == null) return new Empty(); // Just to avoid IDE warning.
var scheme = State.SchemeInfos[input.SchemeId]; Assert(scheme != null, "Scheme not found."); if (scheme == null) return new Empty(); // Just to avoid IDE warning. var virtualAddress = Context.ConvertVirtualAddressToContractAddress(input.SchemeId); if (input.Period == 0) { State.TokenContract.TransferFrom.Send(new TransferFromInput { From = Context.Sender, To = virtualAddress, Symbol = input.Symbol, Amount = input.Amount, Memo = $"Add {input.Amount} dividends." }); if (!scheme.UndistributedProfits.ContainsKey(input.Symbol)) { scheme.UndistributedProfits.Add(input.Symbol, input.Amount); } else { scheme.UndistributedProfits[input.Symbol] = scheme.UndistributedProfits[input.Symbol].Add(input.Amount); } State.SchemeInfos[input.SchemeId] = scheme; } else { var distributedPeriodProfitsVirtualAddress = GetDistributedPeriodProfitsVirtualAddress(virtualAddress, input.Period); var distributedProfitsInformation = State.DistributedProfitsMap[distributedPeriodProfitsVirtualAddress]; if (distributedProfitsInformation == null) { distributedProfitsInformation = new DistributedProfitsInfo { ProfitsAmount = {{input.Symbol, input.Amount}} }; } else { Assert(!distributedProfitsInformation.IsReleased, $"Scheme of period {input.Period} already released."); distributedProfitsInformation.ProfitsAmount[input.Symbol] = distributedProfitsInformation.ProfitsAmount[input.Symbol].Add(input.Amount); } State.TokenContract.TransferFrom.Send(new TransferFromInput { From = Context.Sender, To = distributedPeriodProfitsVirtualAddress, Symbol = input.Symbol, Amount = input.Amount, }); State.DistributedProfitsMap[distributedPeriodProfitsVirtualAddress] = distributedProfitsInformation; } return new Empty();
}
發放分成
用來發放分成。
rpc DistributeProfits (DistributeProfitsInput) returns (google.protobuf.Empty) {
}
...
message DistributeProfitsInput {
aelf.Hash scheme_id = 1;
sint64 period = 2;
sint64 amount = 3;
string symbol = 4;
}
period即本次釋放分成的帳期,不能夠跳着釋放,可是有必要傳入該釋放帳期以供合約檢查,防止分成項目建立人可能因誤操做發送兩個相同交易,致使分成釋放兩次。當amount設置爲0且該分成項目建立時指定is_release_all_balance_everytime_by_default爲true時,則會釋放分成項目虛擬地址上的全部餘額。這裏total_weight是爲須要延期釋放分成的分成項目準備的,由於延期釋放分成項目的可用總權重沒法使用釋放時該分成項目的總權重,只能將總權重設置爲過去某個時間點的總權重。好比今天是六號,要給五號註冊當前在分成接收列表中地址釋放分成,就須要把五號時的總權重傳入參數ReleaseProfitInput中。
/// <summary>
/// Will burn/destroy a certain amount of profits if input.Period
is less than 0.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override Empty DistributeProfits(DistributeProfitsInput input)
{
Assert(input.Amount >= 0, "Amount must be greater than or equal to 0");
Assert(input.Symbol != null && input.Symbol.Any(), "Invalid token symbol."); if (input.Symbol == null) return new Empty(); // Just to avoid IDE warning. var scheme = State.SchemeInfos[input.SchemeId]; Assert(scheme != null, "Scheme not found."); if (scheme == null) return new Empty(); // Just to avoid IDE warning. Assert(Context.Sender == scheme.Manager || Context.Sender == Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName), "Only manager can distribute profits."); ValidateContractState(State.TokenContract, SmartContractConstants.TokenContractSystemName); var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput { Owner = scheme.VirtualAddress, Symbol = input.Symbol }).Balance; if (scheme.IsReleaseAllBalanceEveryTimeByDefault && input.Amount == 0) { // Distribute all from general ledger. Context.LogDebug(() => $"Update distributing amount to {balance} because IsReleaseAllBalanceEveryTimeByDefault == true."); input.Amount = balance; } var totalShares = scheme.TotalShares; if (scheme.DelayDistributePeriodCount > 0) { scheme.CachedDelayTotalShares.Add(input.Period.Add(scheme.DelayDistributePeriodCount), totalShares); if (scheme.CachedDelayTotalShares.ContainsKey(input.Period)) { totalShares = scheme.CachedDelayTotalShares[input.Period]; scheme.CachedDelayTotalShares.Remove(input.Period); } else { totalShares = 0; } } var releasingPeriod = scheme.CurrentPeriod; Assert(input.Period == releasingPeriod, $"Invalid period. When release scheme {input.SchemeId.ToHex()} of period {input.Period}. Current period is {releasingPeriod}"); var profitsReceivingVirtualAddress = GetDistributedPeriodProfitsVirtualAddress(scheme.VirtualAddress, releasingPeriod); if (input.Period < 0 || totalShares <= 0) { return BurnProfits(input, scheme, scheme.VirtualAddress, profitsReceivingVirtualAddress); } Context.LogDebug(() => $"Receiving virtual address: {profitsReceivingVirtualAddress}"); var distributedProfitInformation = UpdateDistributedProfits(input, profitsReceivingVirtualAddress, totalShares); Context.LogDebug(() => $"Distributed profit information of {input.SchemeId.ToHex()} in period {input.Period}, " + $"total Shares {distributedProfitInformation.TotalShares}, total amount {distributedProfitInformation.ProfitsAmount} {input.Symbol}s"); PerformDistributeProfits(input, scheme, totalShares, profitsReceivingVirtualAddress); scheme.CurrentPeriod = input.Period.Add(1); scheme.UndistributedProfits[input.Symbol] = balance.Sub(input.Amount); State.SchemeInfos[input.SchemeId] = scheme; return new Empty();
}
private Empty BurnProfits(DistributeProfitsInput input, Scheme scheme, Address profitVirtualAddress,
Address profitsReceivingVirtualAddress)
{
Context.LogDebug(() => "Entered BurnProfits.");
scheme.CurrentPeriod = input.Period > 0 ? input.Period.Add(1) : scheme.CurrentPeriod;
var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput { Owner = profitsReceivingVirtualAddress, Symbol = input.Symbol }).Balance; // Distribute profits to an address that no one can receive this amount of profits. if (input.Amount.Add(balance) == 0) { State.SchemeInfos[input.SchemeId] = scheme; State.DistributedProfitsMap[profitsReceivingVirtualAddress] = new DistributedProfitsInfo { IsReleased = true }; return new Empty(); } // Burn this amount of profits. if (input.Amount > 0) { State.TokenContract.TransferFrom.Send(new TransferFromInput { From = profitVirtualAddress, To = Context.Self, Amount = input.Amount, Symbol = input.Symbol }); } if (balance > 0) { State.TokenContract.TransferFrom.Send(new TransferFromInput { From = profitsReceivingVirtualAddress, To = Context.Self, Amount = balance, Symbol = input.Symbol }); } State.TokenContract.Burn.Send(new BurnInput { Amount = input.Amount.Add(balance), Symbol = input.Symbol }); scheme.UndistributedProfits[input.Symbol] = scheme.UndistributedProfits[input.Symbol].Sub(input.Amount); State.SchemeInfos[input.SchemeId] = scheme; State.DistributedProfitsMap[profitsReceivingVirtualAddress] = new DistributedProfitsInfo { IsReleased = true, ProfitsAmount = {{input.Symbol, input.Amount.Add(balance).Mul(-1)}} }; return new Empty();
}
private DistributedProfitsInfo UpdateDistributedProfits(DistributeProfitsInput input,
Address profitsReceivingVirtualAddress, long totalShares)
{
var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = profitsReceivingVirtualAddress,
Symbol = input.Symbol
}).Balance;
var distributedProfitsInformation = State.DistributedProfitsMap[profitsReceivingVirtualAddress];
if (distributedProfitsInformation == null)
{
distributedProfitsInformation = new DistributedProfitsInfo
{
TotalShares = totalShares,
ProfitsAmount = {{input.Symbol, input.Amount.Add(balance)}},
IsReleased = true
};
}
else
{
// This means someone used DistributeProfits
do donate to the specific account period of current profit item.
distributedProfitsInformation.TotalShares = totalShares;
distributedProfitsInformation.ProfitsAmount[input.Symbol] = balance.Add(input.Amount);
distributedProfitsInformation.IsReleased = true;
}
State.DistributedProfitsMap[profitsReceivingVirtualAddress] = distributedProfitsInformation; return distributedProfitsInformation;
}
private void PerformDistributeProfits(DistributeProfitsInput input, Scheme scheme, long totalShares,
Address profitsReceivingVirtualAddress)
{
var remainAmount = input.Amount;
remainAmount = DistributeProfitsForSubSchemes(input, scheme, totalShares, remainAmount); // Transfer remain amount to individuals' receiving profits address. if (remainAmount != 0) { State.TokenContract.TransferFrom.Send(new TransferFromInput { From = scheme.VirtualAddress, To = profitsReceivingVirtualAddress, Amount = remainAmount, Symbol = input.Symbol }); }
}
private long DistributeProfitsForSubSchemes(DistributeProfitsInput input, Scheme scheme, long totalShares,
long remainAmount)
{
Context.LogDebug(() => $"Sub schemes count: {scheme.SubSchemes.Count}");
foreach (var subScheme in scheme.SubSchemes)
{
Context.LogDebug(() => $"Releasing {subScheme.SchemeId}");
// General ledger of this sub profit item. var subItemVirtualAddress = Context.ConvertVirtualAddressToContractAddress(subScheme.SchemeId); var amount = SafeCalculateProfits(subScheme.Shares, input.Amount, totalShares); if (amount != 0) { State.TokenContract.TransferFrom.Send(new TransferFromInput { From = scheme.VirtualAddress, To = subItemVirtualAddress, Amount = amount, Symbol = input.Symbol }); } remainAmount = remainAmount.Sub(amount); UpdateSubSchemeInformation(input, subScheme, amount); // Update current_period of detail of sub profit item. var subItemDetail = State.ProfitDetailsMap[input.SchemeId][subItemVirtualAddress]; foreach (var detail in subItemDetail.Details) { detail.LastProfitPeriod = scheme.CurrentPeriod; } State.ProfitDetailsMap[input.SchemeId][subItemVirtualAddress] = subItemDetail; } return remainAmount;
}
private void UpdateSubSchemeInformation(DistributeProfitsInput input, SchemeBeneficiaryShare subScheme,
long amount)
{
var subItem = State.SchemeInfos[subScheme.SchemeId];
if (subItem.UndistributedProfits.ContainsKey(input.Symbol))
{
subItem.UndistributedProfits[input.Symbol] =
subItem.UndistributedProfits[input.Symbol].Add(amount);
}
else
{
subItem.UndistributedProfits.Add(input.Symbol, amount);
}
State.SchemeInfos[subScheme.SchemeId] = subItem;
}
獲取分成
分成受益人能夠經過這個接口獲取指定分成方案至今未領取的分成。
rpc ClaimProfits (ClaimProfitsInput) returns (google.protobuf.Empty) {
}
...
message ClaimProfitsInput {
aelf.Hash scheme_id = 1;
string symbol = 2;
aelf.Address beneficiary = 3;
}
提供分成方案的id,獲取當前可以獲取到的分成(上限爲10期)。若是beneficiary爲空,Sender獲取分成;不爲空,則爲代其餘人獲取分成。
/// <summary>
/// Gain the profit form SchemeId from Details.lastPeriod to scheme.currentPeriod - 1;
/// </summary>
/// <param name="input">ClaimProfitsInput</param>
/// <returns></returns>
public override Empty ClaimProfits(ClaimProfitsInput input)
{
Assert(input.Symbol != null && input.Symbol.Any(), "Invalid token symbol.");
if (input.Symbol == null) return new Empty(); // Just to avoid IDE warning.
var scheme = State.SchemeInfos[input.SchemeId];
Assert(scheme != null, "Scheme not found.");
var beneficiary = input.Beneficiary ?? Context.Sender;
var profitDetails = State.ProfitDetailsMap[input.SchemeId][beneficiary];
Assert(profitDetails != null, "Profit details not found.");
if (profitDetails == null || scheme == null) return new Empty(); // Just to avoid IDE warning.
Context.LogDebug( () => $"{Context.Sender} is trying to profit {input.Symbol} from {input.SchemeId.ToHex()} for {beneficiary}."); var profitVirtualAddress = Context.ConvertVirtualAddressToContractAddress(input.SchemeId); var availableDetails = profitDetails.Details.Where(d => d.EndPeriod >= d.LastProfitPeriod).ToList(); var profitableDetails = availableDetails.Where(d => d.LastProfitPeriod < scheme.CurrentPeriod).ToList(); Context.LogDebug(() => $"Profitable details: {profitableDetails.Aggregate("\n", (profit1, profit2) => profit1.ToString() + "\n" + profit2)}"); // Only can get profit from last profit period to actual last period (profit.CurrentPeriod - 1), // because current period not released yet. for (var i = 0; i < Math.Min(ProfitContractConstants.ProfitReceivingLimitForEachTime, profitableDetails.Count); i++) { var profitDetail = profitableDetails[i]; if (profitDetail.LastProfitPeriod == 0) { // This detail never performed profit before. profitDetail.LastProfitPeriod = profitDetail.StartPeriod; } ProfitAllPeriods(scheme, input.Symbol, profitDetail, profitVirtualAddress, beneficiary); } State.ProfitDetailsMap[input.SchemeId][beneficiary] = new ProfitDetails {Details = {availableDetails}}; return new Empty();
}
private long ProfitAllPeriods(Scheme scheme, string symbol, ProfitDetail profitDetail,
Address profitVirtualAddress, Address beneficiary, bool isView = false)
{
var totalAmount = 0L;
var lastProfitPeriod = profitDetail.LastProfitPeriod;
for (var period = profitDetail.LastProfitPeriod;
period <= (profitDetail.EndPeriod == long.MaxValue
? scheme.CurrentPeriod - 1
: Math.Min(scheme.CurrentPeriod - 1, profitDetail.EndPeriod));
period++)
{
var periodToPrint = period;
var detailToPrint = profitDetail;
var distributedPeriodProfitsVirtualAddress =
GetDistributedPeriodProfitsVirtualAddress(profitVirtualAddress, period);
var distributedProfitsInformation = State.DistributedProfitsMap[distributedPeriodProfitsVirtualAddress];
if (distributedProfitsInformation == null || distributedProfitsInformation.TotalShares == 0)
{
continue;
}
Context.LogDebug(() => $"Released profit information: {distributedProfitsInformation}"); var amount = SafeCalculateProfits(profitDetail.Shares, distributedProfitsInformation.ProfitsAmount[symbol], distributedProfitsInformation.TotalShares); if (!isView) { Context.LogDebug(() => $"{beneficiary} is profiting {amount} {symbol} tokens from {scheme.SchemeId.ToHex()} in period {periodToPrint}." + $"Sender's Shares: {detailToPrint.Shares}, total Shares: {distributedProfitsInformation.TotalShares}"); if (distributedProfitsInformation.IsReleased && amount > 0) { State.TokenContract.TransferFrom.Send(new TransferFromInput { From = distributedPeriodProfitsVirtualAddress, To = beneficiary, Symbol = symbol, Amount = amount }); } lastProfitPeriod = period + 1; } totalAmount = totalAmount.Add(amount); } profitDetail.LastProfitPeriod = lastProfitPeriod; return totalAmount;
}
Token Holder合約:鎖定指定代幣分享DApp利潤
用於實現DApp向用戶分成提供的合約,其場景設定爲:DApp經過某種方式發行其代幣,假設代幣名稱爲APP,能夠經過Token Holder合約建立一個分成方案,並在合約盈利時將必定利潤貢獻給分成方案;持有APP代幣的用戶能夠基於Token Holder合約來經過鎖定APP代幣來參與鎖按期內DApp開發者利潤分配。
DApp開發者使用接口:建立和管理分成方案
建立分成方案
rpc CreateScheme (CreateTokenHolderProfitSchemeInput) returns (google.protobuf.Empty) {
}
...
message CreateTokenHolderProfitSchemeInput {
string symbol = 1;
sint64 minimum_lock_minutes = 2;
map<string, sint64> auto_distribute_threshold = 3;
}
實質上該方法的實現直接使用Profit合約中的CreateScheme方法,考慮到該方案是給持幣人分成,具有「持幣人進行投資從而得到相應分成」的上下文,所以對一些參數值進行固定設置,如:
每次發放分成時,是否是默認發放全部分成方案總帳上的分成?是。
允不容許直接移除掉某一個分成受益人?是。(由於持幣人無需聲明鎖倉時長)
除了建立對應的分成方案以外,還容許DApp合約設定持幣人最小鎖倉時間(防止持幣人提早得知分成發放時間進行投機性鎖倉),以及當分成方案總帳額度觸發自動發放的值。
public override Empty CreateScheme(CreateTokenHolderProfitSchemeInput input)
{
if (State.ProfitContract.Value == null)
{
State.ProfitContract.Value =
Context.GetContractAddressByName(SmartContractConstants.ProfitContractSystemName);
}
State.ProfitContract.CreateScheme.Send(new CreateSchemeInput { Manager = Context.Sender, // 若是不指定發放分成額度,默認發放分成項目總帳全部額度 IsReleaseAllBalanceEveryTimeByDefault = true, // 容許直接移除分成方案受益人 CanRemoveBeneficiaryDirectly = true }); State.TokenHolderProfitSchemes[Context.Sender] = new TokenHolderProfitScheme { Symbol = input.Symbol, // 最小鎖倉時間 MinimumLockMinutes = input.MinimumLockMinutes, // 總帳額度大於多少時自動發放 AutoDistributeThreshold = {input.AutoDistributeThreshold} }; return new Empty();
}
管理分成方案
rpc AddBeneficiary (AddTokenHolderBeneficiaryInput) returns (google.protobuf.Empty) {
}
rpc RemoveBeneficiary (RemoveTokenHolderBeneficiaryInput) returns (google.protobuf.Empty) {
}
rpc ContributeProfits (ContributeProfitsInput) returns (google.protobuf.Empty) {
}
rpc DistributeProfits (DistributeProfitsInput) returns (google.protobuf.Empty) {
}
...
message AddTokenHolderBeneficiaryInput {
aelf.Address beneficiary = 1;
sint64 shares = 2;
}
message RemoveTokenHolderBeneficiaryInput {
aelf.Address beneficiary = 1;
sint64 amount = 2;
}
message ContributeProfitsInput {
aelf.Address scheme_manager = 1;
sint64 amount = 2;
string symbol = 3;
}
message DistributeProfitsInput {
aelf.Address scheme_manager = 1;
string symbol = 2;
}
message TokenHolderProfitScheme {
string symbol = 1;
aelf.Hash scheme_id = 2;
sint64 period = 3;
sint64 minimum_lock_minutes = 4;
map<string, sint64> auto_distribute_threshold = 5;
}
以上幾個接口的實現都是對Profit合約同名接口的應用。
在管理分成受益人時,特將每一個受益人的Profit Detail列表僅保留一項,也就是說能夠對受益人的屢次鎖倉進行合併。
public override Empty AddBeneficiary(AddTokenHolderBeneficiaryInput input)
{
var scheme = GetValidScheme(Context.Sender);
var detail = State.ProfitContract.GetProfitDetails.Call(new GetProfitDetailsInput
{
SchemeId = scheme.SchemeId,
Beneficiary = input.Beneficiary
});
var shares = input.Shares;
if (detail.Details.Any())
{
// Only keep one detail.
// 將以前的Shares移除 State.ProfitContract.RemoveBeneficiary.Send(new RemoveBeneficiaryInput { SchemeId = scheme.SchemeId, Beneficiary = input.Beneficiary }); shares.Add(detail.Details.Single().Shares); } // 添加更新後的Shares State.ProfitContract.AddBeneficiary.Send(new AddBeneficiaryInput { SchemeId = scheme.SchemeId, BeneficiaryShare = new BeneficiaryShare { Beneficiary = input.Beneficiary, Shares = shares } }); return new Empty();
}
public override Empty RemoveBeneficiary(RemoveTokenHolderBeneficiaryInput input)
{
var scheme = GetValidScheme(Context.Sender);
var detail = State.ProfitContract.GetProfitDetails.Call(new GetProfitDetailsInput { Beneficiary = input.Beneficiary, SchemeId = scheme.SchemeId }).Details.Single(); var lockedAmount = detail.Shares; State.ProfitContract.RemoveBeneficiary.Send(new RemoveBeneficiaryInput { SchemeId = scheme.SchemeId, Beneficiary = input.Beneficiary }); if (lockedAmount > input.Amount && input.Amount != 0) // If input.Amount == 0, means just remove this beneficiary. { State.ProfitContract.AddBeneficiary.Send(new AddBeneficiaryInput { SchemeId = scheme.SchemeId, BeneficiaryShare = new BeneficiaryShare { Beneficiary = input.Beneficiary, Shares = lockedAmount.Sub(input.Amount) } }); } return new Empty();
}
public override Empty ContributeProfits(ContributeProfitsInput input)
{
var scheme = GetValidScheme(input.SchemeManager);
if (State.TokenContract.Value == null)
{
State.TokenContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
}
State.TokenContract.TransferFrom.Send(new TransferFromInput { From = Context.Sender, To = Context.Self, Symbol = input.Symbol, Amount = input.Amount }); State.TokenContract.Approve.Send(new ApproveInput { Spender = State.ProfitContract.Value, Symbol = input.Symbol, Amount = input.Amount }); State.ProfitContract.ContributeProfits.Send(new Profit.ContributeProfitsInput { SchemeId = scheme.SchemeId, Symbol = input.Symbol, Amount = input.Amount }); return new Empty();
}
public override Empty DistributeProfits(DistributeProfitsInput input)
{
var scheme = GetValidScheme(input.SchemeManager, true);
Assert(Context.Sender == Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName) ||
Context.Sender == input.SchemeManager, "No permission to distribute profits.");
State.ProfitContract.DistributeProfits.Send(new Profit.DistributeProfitsInput
{
SchemeId = scheme.SchemeId,
Symbol = input.Symbol ?? scheme.Symbol,
Period = scheme.Period
});
scheme.Period = scheme.Period.Add(1);
State.TokenHolderProfitSchemes[input.SchemeManager] = scheme;
return new Empty();
}
持幣人使用接口:鎖倉和領取分成
鎖倉接口採用了一個直白淺顯的名字:Register For Profits。而解鎖僅須要提供DApp合約地址便可。
rpc RegisterForProfits (RegisterForProfitsInput) returns (google.protobuf.Empty) {
}
rpc Withdraw (aelf.Address) returns (google.protobuf.Empty) {
}
rpc ClaimProfits (ClaimProfitsInput) returns (google.protobuf.Empty) {
}
...
message RegisterForProfitsInput {
aelf.Address scheme_manager = 1;
sint64 amount = 2;
}
message ClaimProfitsInput {
aelf.Address scheme_manager = 1;
aelf.Address beneficiary = 2;
string symbol = 3;
}
前文提到「當分成方案總帳額度觸發自動發放的值」就是在用戶鎖倉時進行判斷的。這裏觸發的DistributeProfits
屬於inline交易,並不需持幣人額外支付交易費。
而在持幣人解鎖時,會對是否已通過了該分成項目設定的最小鎖倉時間進行判斷。
public override Empty RegisterForProfits(RegisterForProfitsInput input)
{
var scheme = GetValidScheme(input.SchemeManager);
if (State.TokenContract.Value == null)
{
State.TokenContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
}
State.TokenContract.Lock.Send(new LockInput { LockId = Context.TransactionId, Symbol = scheme.Symbol, Address = Context.Sender, Amount = input.Amount, }); State.LockIds[input.SchemeManager][Context.Sender] = Context.TransactionId; State.LockTimestamp[Context.TransactionId] = Context.CurrentBlockTime; State.ProfitContract.AddBeneficiary.Send(new AddBeneficiaryInput { SchemeId = scheme.SchemeId, BeneficiaryShare = new BeneficiaryShare { Beneficiary = Context.Sender, Shares = input.Amount } }); // Check auto-distribute threshold. foreach (var threshold in scheme.AutoDistributeThreshold) { var originScheme = State.ProfitContract.GetScheme.Call(scheme.SchemeId); var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput { Owner = originScheme.VirtualAddress, Symbol = threshold.Key }).Balance; if (balance < threshold.Value) continue; State.ProfitContract.DistributeProfits.Send(new Profit.DistributeProfitsInput { SchemeId = scheme.SchemeId, Symbol = threshold.Key, Period = scheme.Period.Add(1) }); scheme.Period = scheme.Period.Add(1); State.TokenHolderProfitSchemes[input.SchemeManager] = scheme; } return new Empty();
}
public override Empty Withdraw(Address input)
{
var scheme = GetValidScheme(input);
if (State.TokenContract.Value == null)
{
State.TokenContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
}
var amount = State.TokenContract.GetLockedAmount.Call(new GetLockedAmountInput { Address = Context.Sender, LockId = State.LockIds[input][Context.Sender], Symbol = scheme.Symbol }).Amount; var lockId = State.LockIds[input][Context.Sender]; Assert(State.LockTimestamp[lockId].AddMinutes(scheme.MinimumLockMinutes) < Context.CurrentBlockTime, "Cannot withdraw."); State.TokenContract.Unlock.Send(new UnlockInput { Address = Context.Sender, LockId = lockId, Amount = amount, Symbol = scheme.Symbol }); State.LockIds[input].Remove(Context.Sender); State.ProfitContract.RemoveBeneficiary.Send(new RemoveBeneficiaryInput { SchemeId = scheme.SchemeId, Beneficiary = Context.Sender }); return new Empty();
}
public override Empty ClaimProfits(ClaimProfitsInput input)
{
var scheme = GetValidScheme(input.SchemeManager);
var beneficiary = input.Beneficiary ?? Context.Sender;
State.ProfitContract.ClaimProfits.Send(new Profit.ClaimProfitsInput
{
SchemeId = scheme.SchemeId,
Beneficiary = beneficiary,
Symbol = input.Symbol
});
return new Empty();
}
Treasury合約:aelf主鏈分成池
主鏈分成池構建
在aelf主鏈中,系統Treasury合約維護這七個分成項目,一共分爲三級。
第一級:Treasury
Treasury分成方案:
Manager:Treasury合約
分成收入來源:生產節點出塊獎勵
收入到帳時間和分配時間:主鏈每次換屆成功時(也就是任意時刻,該分成方案的總帳都爲空)
分成方案受益人:無
子分成方案:Miner Reward,Citizen Welfare,Backup Subsidy,共計三個
股份:N/A
第二級:Miner Reward,Citizen Welfare,Backup Subsidy
Miner Reward分成方案:
Manager:Treasury合約
分成收入來源:Treasury分成方案
收入到帳時間和分配時間:主鏈每次換屆成功時
分成方案受益人:無
子分成方案:Miner Basic Reward,Miner Votes Weight Reward,Re-Election Miner Reward,共計三個
股份:4(/20,佔Treasury分成方案)
Citizen Welfare分成方案:
Manager:Election合約
分成收入來源:Treasury分成方案
收入到帳時間和分配時間:主鏈每次換屆成功時
分成方案受益人:一屆時間週期以內參與節點競選進行投票的選民
子分成方案:無
股份:15(/20,佔Treasury分成方案)
Backup Subsidy分成方案:
Manager:Election合約;
分成收入來源:Treasury分成方案;
收入到帳時間和分配時間:主鏈每次換屆成功時;
分成方案受益人:得票數排名在五倍於當前生產節點數量以內的候選節點;
子分成方案:無;
股份:1(/20,佔Treasury分成方案)
第三級:Miner Basic Reward,Miner Votes Weight Reward,Re-Election Miner Reward
Miner Basic Reward分成方案:
Manager:Treasury合約
分成收入來源:Miner Reward分成方案
收入到帳時間和分配時間:主鏈每次換屆成功時
分成方案受益人:剛結束這一屆的全部生產節點
子分成方案:無
股份:2(4,佔Miner Reward分成方案)
Miner Votes Weight Reward分成方案:
Manager:Treasury合約
分成收入來源:Miner Reward分成方案
收入到帳時間和分配時間:主鏈每次換屆成功時
分成方案受益人:剛結束這一屆的全部生產節點
子分成方案:無
股份:1(/4,佔Miner Reward分成方案)
Re-Election Miner Reward分成方案:
Manager:Treasury合約
分成收入來源:Miner Reward分成方案
收入到帳時間和分配時間:主鏈每次換屆成功時
分成方案受益人:當前生產節點中有連任記錄的生產節點
子分成方案:無
股份:1(/4,佔Miner Reward分成方案)
注:上述的股份都是能夠後期使用Treasury合約的SetDividendPoolWeightSetting和SetMinerRewardWeightSetting方法進行修改的。
主鏈分成池構建實現
以上三級分成方案在代碼(Treasury合約)中的構建過程以下:
public override Empty InitialTreasuryContract(Empty input)
{
Assert(!State.Initialized.Value, "Already initialized.");
State.ProfitContract.Value = Context.GetContractAddressByName(SmartContractConstants.ProfitContractSystemName); // 建立七個分成方案。 // Create profit items: `Treasury`, `CitizenWelfare`, `BackupSubsidy`, `MinerReward`, // `MinerBasicReward`, `MinerVotesWeightReward`, `ReElectedMinerReward` var profitItemNameList = new List<string> { "Treasury", "MinerReward", "Subsidy", "Welfare", "Basic Reward", "Votes Weight Reward", "Re-Election Reward" }; for (var i = 0; i < 7; i++) { var index = i; Context.LogDebug(() => profitItemNameList[index]); State.ProfitContract.CreateScheme.Send(new CreateSchemeInput { IsReleaseAllBalanceEveryTimeByDefault = true, // Distribution of Citizen Welfare will delay one period. DelayDistributePeriodCount = i == 3 ? 1 : 0, }); } State.Initialized.Value = true; return new Empty();
}
public override Empty InitialMiningRewardProfitItem(Empty input)
{
Assert(State.TreasuryHash.Value == null, "Already initialized.");
var managingSchemeIds = State.ProfitContract.GetManagingSchemeIds.Call(new GetManagingSchemeIdsInput
{
Manager = Context.Self
}).SchemeIds;
Assert(managingSchemeIds.Count == 7, "Incorrect schemes count."); State.TreasuryHash.Value = managingSchemeIds[0]; State.RewardHash.Value = managingSchemeIds[1]; State.SubsidyHash.Value = managingSchemeIds[2]; State.WelfareHash.Value = managingSchemeIds[3]; State.BasicRewardHash.Value = managingSchemeIds[4]; State.VotesWeightRewardHash.Value = managingSchemeIds[5]; State.ReElectionRewardHash.Value = managingSchemeIds[6]; var electionContractAddress = Context.GetContractAddressByName(SmartContractConstants.ElectionContractSystemName); if (electionContractAddress != null) { State.ProfitContract.ResetManager.Send(new ResetManagerInput { SchemeId = managingSchemeIds[2], NewManager = electionContractAddress }); State.ProfitContract.ResetManager.Send(new ResetManagerInput { SchemeId = managingSchemeIds[3], NewManager = electionContractAddress }); } BuildTreasury(); var treasuryVirtualAddress = Address.FromPublicKey(State.ProfitContract.Value.Value.Concat( managingSchemeIds[0].Value.ToByteArray().ComputeHash()).ToArray()); State.TreasuryVirtualAddress.Value = treasuryVirtualAddress; return new Empty();
}
// 構建級聯關係
private void BuildTreasury()
{
// Register MinerReward
to Treasury
State.ProfitContract.AddSubScheme.Send(new AddSubSchemeInput
{
SchemeId = State.TreasuryHash.Value,
SubSchemeId = State.RewardHash.Value,
SubSchemeShares = TreasuryContractConstants.MinerRewardWeight
});
// Register `BackupSubsidy` to `Treasury` State.ProfitContract.AddSubScheme.Send(new AddSubSchemeInput { SchemeId = State.TreasuryHash.Value, SubSchemeId = State.SubsidyHash.Value, SubSchemeShares = TreasuryContractConstants.BackupSubsidyWeight }); // Register `CitizenWelfare` to `Treasury` State.ProfitContract.AddSubScheme.Send(new AddSubSchemeInput { SchemeId = State.TreasuryHash.Value, SubSchemeId = State.WelfareHash.Value, SubSchemeShares = TreasuryContractConstants.CitizenWelfareWeight }); // Register `MinerBasicReward` to `MinerReward` State.ProfitContract.AddSubScheme.Send(new AddSubSchemeInput { SchemeId = State.RewardHash.Value, SubSchemeId = State.BasicRewardHash.Value, SubSchemeShares = TreasuryContractConstants.BasicMinerRewardWeight }); // Register `MinerVotesWeightReward` to `MinerReward` State.ProfitContract.AddSubScheme.Send(new AddSubSchemeInput { SchemeId = State.RewardHash.Value, SubSchemeId = State.VotesWeightRewardHash.Value, SubSchemeShares = TreasuryContractConstants.VotesWeightRewardWeight }); // Register `ReElectionMinerReward` to `MinerReward` State.ProfitContract.AddSubScheme.Send(new AddSubSchemeInput { SchemeId = State.RewardHash.Value, SubSchemeId = State.ReElectionRewardHash.Value, SubSchemeShares = TreasuryContractConstants.ReElectionRewardWeight });
}
主鏈分成池相關分成方案維護
增長分成
爲主鏈分成池增長分成使用Treasury合約的Donate方法:
rpc Donate (DonateInput) returns (google.protobuf.Empty) {
}
...
message DonateInput {
string symbol = 1;
sint64 amount = 2;
}
它的實現分爲兩步:
將交易發送者(Sender)指定數量的代幣經過Token合約TransferFrom方法轉入Treasury合約,此時Treasury合約即有必定的餘額。
若是指定Token爲ELF,直接把Treasury的ELF餘額(第一步中增長的餘額)打入Treasury分成方案總帳,不然就需經過Token Converter合約將代幣轉爲ELF以後再打入Treasufy分成方案總帳;可是若指定代幣不曾在Token Converter配置過鏈接器,就直接把指定Token轉入Treasury分成方案總帳。
public override Empty Donate(DonateInput input)
{
Assert(input.Amount > 0, "Invalid amount of donating. Amount needs to be greater than 0.");
if (State.TokenContract.Value == null)
{
State.TokenContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
}
if (State.TokenConverterContract.Value == null) { State.TokenConverterContract.Value = Context.GetContractAddressByName(SmartContractConstants.TokenConverterContractSystemName); } var isNativeSymbol = input.Symbol == Context.Variables.NativeSymbol; var connector = State.TokenConverterContract.GetConnector.Call(new TokenSymbol {Symbol = input.Symbol}); var canExchangeWithNativeSymbol = connector.RelatedSymbol != string.Empty; State.TokenContract.TransferFrom.Send(new TransferFromInput { From = Context.Sender, To = isNativeSymbol || !canExchangeWithNativeSymbol ? State.TreasuryVirtualAddress.Value : Context.Self, Symbol = input.Symbol, Amount = input.Amount, Memo = "Donate to treasury.", }); Context.Fire(new DonationReceived { From = Context.Sender, To = isNativeSymbol ? State.TreasuryVirtualAddress.Value : Context.Self, Symbol = input.Symbol, Amount = input.Amount, Memo = "Donate to treasury." }); if (input.Symbol != Context.Variables.NativeSymbol && canExchangeWithNativeSymbol) { ConvertToNativeToken(input.Symbol, input.Amount); } return new Empty();
}
其中,爲了實現「經過Token Converter合約將代幣轉爲ELF以後再打入Treasufy分成方案總帳」,增長了DonateAll方法:
rpc DonateAll (DonateAllInput) returns (google.protobuf.Empty) {
}
...
message DonateAllInput {
string symbol = 1;
}
因而ConvertToNativeToken的實現分爲兩步:
賣掉非ELF的token;
使用一個inline交易調用DonateAll,參數填ELF。
DonateAll的實現也是兩步:
查ELF餘額;
直接調用Donate方法並填入ELF和查到的ELF餘額。
private void ConvertToNativeToken(string symbol, long amount)
{
State.TokenConverterContract.Sell.Send(new SellInput
{
Symbol = symbol,
Amount = amount
});
Context.SendInline(Context.Self, nameof(DonateAll), new DonateAllInput { Symbol = Context.Variables.NativeSymbol });
}
public override Empty DonateAll(DonateAllInput input)
{
if (State.TokenContract.Value == null)
{
State.TokenContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
}
var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput { Symbol = input.Symbol, Owner = Context.Sender }).Balance; Donate(new DonateInput { Symbol = input.Symbol, Amount = balance }); return new Empty();
}
配置子分成方案和受益人
對於分成池第二級的三個分成方案:
Citizen Welfare的受益人爲全部選民,選民的股份由其投票時的票數和鎖定時間決定,經過Election合約進行維護,具體時機分別爲Vote和Withdraw時,好比這是Vote方法中爲選民增長對應股份的代碼:
/// <summary>
/// Call the Vote function of VoteContract to do a voting.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override Empty Vote(VoteMinerInput input)
{
// Check candidate information map instead of candidates.
var targetInformation = State.CandidateInformationMap[input.CandidatePubkey];
AssertValidCandidateInformation(targetInformation);
var lockSeconds = (input.EndTimestamp - Context.CurrentBlockTime).Seconds; AssertValidLockSeconds(lockSeconds); State.LockTimeMap[Context.TransactionId] = lockSeconds; // ... CallTokenContractLock(input.Amount); CallTokenContractIssue(input.Amount); CallVoteContractVote(input.Amount, input.CandidatePubkey); var votesWeight = GetVotesWeight(input.Amount, lockSeconds); CallProfitContractAddBeneficiary(votesWeight, lockSeconds); // ... return new Empty();
}
private void CallProfitContractAddBeneficiary(long votesWeight, long lockSeconds)
{
State.ProfitContract.AddBeneficiary.Send(new AddBeneficiaryInput
{
SchemeId = State.WelfareHash.Value,
BeneficiaryShare = new BeneficiaryShare
{
Beneficiary = Context.Sender,
Shares = votesWeight
},
EndPeriod = GetEndPeriod(lockSeconds)
});
}
Backup Subsidy的受益人爲全部聲明參與生產節點競選(須要抵押10萬個ELF)的候選節點,與Citizen Welfare相似的,不過是在Election合約的AnnounceElection和QuitElection方法中維護,且每一個候選人的股份固定爲1:
/// <summary>
/// Actually this method is for adding an option of the Voting Item.
/// Thus the limitation of candidates will be limited by the capacity of voting options.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override Empty AnnounceElection(Empty input)
{
var recoveredPublicKey = Context.RecoverPublicKey();
AnnounceElection(recoveredPublicKey);
var pubkey = recoveredPublicKey.ToHex(); LockCandidateNativeToken(); AddCandidateAsOption(pubkey); if (State.Candidates.Value.Value.Count <= GetValidationDataCenterCount()) { State.DataCentersRankingList.Value.DataCenters.Add(pubkey, 0); RegisterCandidateToSubsidyProfitScheme(); } return new Empty();
}
private void RegisterCandidateToSubsidyProfitScheme()
{
if (State.ProfitContract.Value == null)
{
State.ProfitContract.Value =
Context.GetContractAddressByName(SmartContractConstants.ProfitContractSystemName);
}
// Add 1 Shares for this candidate in subsidy profit item. State.ProfitContract.AddBeneficiary.Send(new AddBeneficiaryInput { SchemeId = State.SubsidyHash.Value, BeneficiaryShare = new BeneficiaryShare {Beneficiary = Context.Sender, Shares = 1} });
}
Miner Reward分成方案不存在受益人,分成到手後會馬上被Treasury合約分配給其下的三個子分成方案,也就是主鏈分成池第三級的三個分成方案,這一部分的維護髮生於每一次分成池釋放的先後:釋放前,根據上一屆的實際出塊數,配置上一屆生產節點Basic Miner Reward分成方案下可以領取分成的股份;釋放後,根據新一屆生產節點的當前得票數和連任狀況,對Miner Votes Weight Reward和Re-Election Miner Reward進行維護。
釋放以前:
private void UpdateTreasurySubItemsSharesBeforeDistribution(Round previousTermInformation)
{
var previousPreviousTermInformation = State.AEDPoSContract.GetPreviousTermInformation.Call(new SInt64Value
{
Value = previousTermInformation.TermNumber.Sub(1)
});
UpdateBasicMinerRewardWeights(new List<Round> {previousPreviousTermInformation, previousTermInformation});
}
/// <summary>
/// Remove current total shares of Basic Reward,
/// Add new shares for miners of next term.
/// 1 share for each miner.
/// </summary>
/// <param name="previousTermInformation"></param>
private void UpdateBasicMinerRewardWeights(IReadOnlyCollection<Round> previousTermInformation)
{
if (previousTermInformation.First().RealTimeMinersInformation != null)
{
State.ProfitContract.RemoveBeneficiaries.Send(new RemoveBeneficiariesInput
{
SchemeId = State.BasicRewardHash.Value,
Beneficiaries = {previousTermInformation.First().RealTimeMinersInformation.Keys.Select(k =>
Address.FromPublicKey(ByteArrayHelper.HexStringToByteArray(k)))}
});
}
// Manage weights of `MinerBasicReward` State.ProfitContract.AddBeneficiaries.Send(new AddBeneficiariesInput { SchemeId = State.BasicRewardHash.Value, EndPeriod = previousTermInformation.Last().TermNumber, BeneficiaryShares = { previousTermInformation.Last().RealTimeMinersInformation.Values.Select(i => new BeneficiaryShare { Beneficiary = Address.FromPublicKey(ByteArrayHelper.HexStringToByteArray(i.Pubkey)), Shares = i.ProducedBlocks }) } });
}
釋放以後:
private void UpdateTreasurySubItemsSharesAfterDistribution(Round previousTermInformation)
{
var victories = State.ElectionContract.GetVictories.Call(new Empty()).Value.Select(bs => bs.ToHex())
.ToList();
UpdateReElectionRewardWeights(previousTermInformation, victories); UpdateVotesWeightRewardWeights(previousTermInformation, victories);
}
/// <summary>
/// Remove current total shares of Re-Election Reward,
/// Add shares to re-elected miners based on their continual appointment count.
/// </summary>
/// <param name="previousTermInformation"></param>
/// <param name="victories"></param>
private void UpdateReElectionRewardWeights(Round previousTermInformation, ICollection<string> victories)
{
var previousMinerAddresses = previousTermInformation.RealTimeMinersInformation.Keys
.Select(k => Address.FromPublicKey(ByteArrayHelper.HexStringToByteArray(k))).ToList();
var reElectionRewardProfitSubBeneficiaries = new RemoveBeneficiariesInput
{
SchemeId = State.ReElectionRewardHash.Value,
Beneficiaries = {previousMinerAddresses}
};
State.ProfitContract.RemoveBeneficiaries.Send(reElectionRewardProfitSubBeneficiaries);
var minerReElectionInformation = State.MinerReElectionInformation.Value ?? InitialMinerReElectionInformation(previousTermInformation.RealTimeMinersInformation.Keys); AddBeneficiariesForReElectionScheme(previousTermInformation.TermNumber.Add(1), victories, minerReElectionInformation); var recordedMiners = minerReElectionInformation.Clone().ContinualAppointmentTimes.Keys; foreach (var miner in recordedMiners) { if (!victories.Contains(miner)) { minerReElectionInformation.ContinualAppointmentTimes.Remove(miner); } } State.MinerReElectionInformation.Value = minerReElectionInformation;
}
private void AddBeneficiariesForReElectionScheme(long endPeriod, IEnumerable<string> victories,
MinerReElectionInformation minerReElectionInformation)
{
var reElectionProfitAddBeneficiaries = new AddBeneficiariesInput
{
SchemeId = State.ReElectionRewardHash.Value,
EndPeriod = endPeriod
};
foreach (var victory in victories) { if (minerReElectionInformation.ContinualAppointmentTimes.ContainsKey(victory)) { var minerAddress = Address.FromPublicKey(ByteArrayHelper.HexStringToByteArray(victory)); var continualAppointmentCount = minerReElectionInformation.ContinualAppointmentTimes[victory].Add(1); minerReElectionInformation.ContinualAppointmentTimes[victory] = continualAppointmentCount; reElectionProfitAddBeneficiaries.BeneficiaryShares.Add(new BeneficiaryShare { Beneficiary = minerAddress, Shares = Math.Min(continualAppointmentCount, TreasuryContractConstants.MaximumReElectionRewardShare) }); } else { minerReElectionInformation.ContinualAppointmentTimes.Add(victory, 0); } } if (reElectionProfitAddBeneficiaries.BeneficiaryShares.Any()) { State.ProfitContract.AddBeneficiaries.Send(reElectionProfitAddBeneficiaries); }
}
private MinerReElectionInformation InitialMinerReElectionInformation(ICollection<string> previousMiners)
{
var information = new MinerReElectionInformation();
foreach (var previousMiner in previousMiners)
{
information.ContinualAppointmentTimes.Add(previousMiner, 0);
}
return information;
}
/// <summary>
/// Remove current total shares of Votes Weight Reward,
/// Add shares to current miners based on votes they obtained.
/// </summary>
/// <param name="previousTermInformation"></param>
/// <param name="victories"></param>
private void UpdateVotesWeightRewardWeights(Round previousTermInformation, IEnumerable<string> victories)
{
var previousMinerAddresses = previousTermInformation.RealTimeMinersInformation.Keys
.Select(k => Address.FromPublicKey(ByteArrayHelper.HexStringToByteArray(k))).ToList();
var votesWeightRewardProfitSubBeneficiaries = new RemoveBeneficiariesInput
{
SchemeId = State.VotesWeightRewardHash.Value,
Beneficiaries = {previousMinerAddresses}
};
State.ProfitContract.RemoveBeneficiaries.Send(votesWeightRewardProfitSubBeneficiaries);
var votesWeightRewardProfitAddBeneficiaries = new AddBeneficiariesInput { SchemeId = State.VotesWeightRewardHash.Value, EndPeriod = previousTermInformation.TermNumber.Add(1) }; var dataCenterRankingList = State.ElectionContract.GetDataCenterRankingList.Call(new Empty()); foreach (var victory in victories) { var obtainedVotes = 0L; if (dataCenterRankingList.DataCenters.ContainsKey(victory)) { obtainedVotes = dataCenterRankingList.DataCenters[victory]; } var minerAddress = Address.FromPublicKey(ByteArrayHelper.HexStringToByteArray(victory)); if (obtainedVotes > 0) { votesWeightRewardProfitAddBeneficiaries.BeneficiaryShares.Add(new BeneficiaryShare { Beneficiary = minerAddress, Shares = obtainedVotes }); } } if (votesWeightRewardProfitAddBeneficiaries.BeneficiaryShares.Any()) { State.ProfitContract.AddBeneficiaries.Send(votesWeightRewardProfitAddBeneficiaries); }
}
主鏈分成池釋放
主鏈分成池的分成發放時間爲換屆時,即基於AEDPoS合約的NextTerm方法。而釋放自己的邏輯位於Treasury合約的Release方法:
rpc Release (ReleaseInput) returns (google.protobuf.Empty) {
}
...
message ReleaseInput {
sint64 term_number = 1;
}
固然,只有AEDPoS合約才能調用這個方法:
public override Empty Release(ReleaseInput input)
{
MaybeLoadAEDPoSContractAddress();
Assert(
Context.Sender == State.AEDPoSContract.Value,
"Only aelf Consensus Contract can release profits from Treasury.");
State.ProfitContract.DistributeProfits.Send(new DistributeProfitsInput
{
SchemeId = State.TreasuryHash.Value,
Period = input.TermNumber,
Symbol = Context.Variables.NativeSymbol
});
MaybeLoadElectionContractAddress();
var previousTermInformation = State.AEDPoSContract.GetPreviousTermInformation.Call(new SInt64Value
{
Value = input.TermNumber
});
UpdateTreasurySubItemsSharesBeforeDistribution(previousTermInformation);
ReleaseTreasurySubProfitItems(input.TermNumber);
UpdateTreasurySubItemsSharesAfterDistribution(previousTermInformation);
return new Empty();
}
private void ReleaseTreasurySubProfitItems(long termNumber)
{
State.ProfitContract.DistributeProfits.Send(new DistributeProfitsInput
{
SchemeId = State.RewardHash.Value,
Period = termNumber,
Symbol = Context.Variables.NativeSymbol
});
State.ProfitContract.DistributeProfits.Send(new DistributeProfitsInput { SchemeId = State.BasicRewardHash.Value, Period = termNumber, Symbol = Context.Variables.NativeSymbol }); State.ProfitContract.DistributeProfits.Send(new DistributeProfitsInput { SchemeId = State.VotesWeightRewardHash.Value, Period = termNumber, Symbol = Context.Variables.NativeSymbol }); State.ProfitContract.DistributeProfits.Send(new DistributeProfitsInput { SchemeId = State.ReElectionRewardHash.Value, Period = termNumber, Symbol = Context.Variables.NativeSymbol });
}