Profit合约:统一的分红管理方案
概要
由于aelf主链采用DPoS共识机制,通过持有代币或者锁仓来获得权益是aelf治理模型中重要组成部分。这就产生了一个需求:实现一个能够统一管理分红的标准流程,并将其作为一个基础的智能合约。这个合约在创世区块中即部署于链上,其应用包括但不限于:生产节点在某一届任期结束时根据其区块生产数量(以此作为权重)获得相应奖励,选民通过节点竞选投票所质押ELF来分享相应的奖励,DApp合约允许用户通过抵押Token来分享合约盈利。
分红方案即代币分配策略:任何地址都可以成为分红方案(profit scheme)的管理者(manager)。每个管理者(manager)都可以为该分红方案添加受益人(beneficiary),并为每个受益人设定股份(shares)。之后,当分红项目创建者对其项目受益人发放(distribute)分红时,将按其对应的股份进行代币分配。每次分红结束后,分红方案的账期(period)即加一,根据具体分红数额会在该账期对应的虚拟地址(virtual address)上增加余额,持有股份的账户可以从中获得相应分红。分红的受益人不仅可以是账户地址,也可以是另一个分红方案,子分红方案所获分红可直接打进其总账(general ledger)。分红方案之间可以进行级联。
进一步阐述几个概念:
分红方案(profit scheme):通过分红合约创建出来的代币分配中心。
分红方案管理者(manager):管理分红方案的受益人及其相应股份。
分红受益人(beneficiary):aelf区块链上用来接收分红代币的账户地址。要注意的是,受益人的分红需要通过发送交易来获取(需填入对应分红方案的id,交易由自己或他人发送皆可)。
分红方案虚拟地址(virtual address):每个分红方案都会通过其唯一标识(scheme id)映射一个虚拟地址,这个地址仅用来释放分红,没有对应的公私钥对(公私钥对碰撞出来的概率可以忽略不计)。
子分红方案(sub profit item):这是一个相对概念,每个分红方案都可能成为子分红方案。子分红方案可持有其父分红方案股份,这样父分红方案在释放分红时,子分红方案的虚拟地址会获得相应代币。
获取分红(claim profits):作为一个普通用户,需要自行发送交易来获取自己应得的分红,这是为了避免注册的接收地址过多,释放分红的交易执行超时。
股份(shares):股份是每个分红受益人能够获取相应分红比例的证明,即某受益人分红数量 = 总分红 * 该受益人持有股份 / 总股份。
发放分红(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
});
}