ACS10 - 分红池标准¶
ACS10用于在当前合约中构造一个分红池。
接口¶
为了构造一个分红池,可以可选地实现以下接口:
- Donate,用于捐赠分红,参数包括要为分红池捐赠的代币symbol和额度;
- Release,用于释放分红,参数为释放分红的届数,要注意设置其调用权限.
- SetSymbolList,用于设置分红池所支持的释放分红的代币symbol,参数为SymbolList类型;
- GetSymbolList,用于获取分红池所支持的释放分红的代币symbol,返回值为SymbolList类型;
- GetUndistributedDividends,用于获取尚未被分红的所有分红池所支持的代币余额,返回值为Dividends类型;
- GetDividends,用于获取某个区块高度中新增的分红的额度,返回值也是Dividends类型。
其中SymbolList类型是string列表:
message SymbolList {
repeated string value = 1;
}
Dividends类型是key为代币symbol、value为数额的map:
message Dividends {
map<string, int64> value = 1;
}
应用¶
ACS10仅仅是统一了分红池的标准接口,这些接口不会跟AElf链上做交互。
接口实现¶
借助Profit合约¶
可以在初始化该合约的时候,使用Profit合约的CreateScheme方法创建一个分红方案(Profit Scheme):
State.ProfitContract.Value =
Context.GetContractAddressByName(SmartContractConstants.ProfitContractSystemName);
var schemeToken = HashHelper.ComputeFrom(Context.Self);
State.ProfitContract.CreateScheme.Send(new CreateSchemeInput
{
Manager = Context.Self,
CanRemoveBeneficiaryDirectly = true,
IsReleaseAllBalanceEveryTimeByDefault = true,
Token = schemeToken
});
State.ProfitSchemeId.Value = Context.GenerateId(State.ProfitContract.Value, schemeToken);
其中,schemeToken会被Profit合约用来计算该合约创建的分红方案的Id,Context.GenerateId方法是AElf智能合约通用的用于产生Id的方法,我们使用Profit合约的地址和提供给Profit合约的schemeToken即可自行计算出该分红方案的Id,设置到State.ProfitSchemeId上(这是一个SingletonState<Hash>类型的属性)。
创建完分红方案后:
- 可使用Profit合约的ContributeProfits方法来实现ACS10接口中的Donate方法;
- 可使用Profit合约的DistributeProfits方法来实现ACS10接口中的Release方法;
- 可使用AddBeneficiary、RemoveBeneficiary等方法来自行管理分红的领取人及权重;
- 可使用AddSubScheme、RemoveSubScheme等方法自行管理分红关联的子分红方案及权重;
- SetSymbolList和GetSymbolList中的SymbolList类型可以自行记录,在Donate和Release时发挥对应的作用即可;
- GetUndistributedDividends方法返回查询到的对应的分红方案总账上SymbolList中包含代币的余额即可。
借助Token Holder合约¶
在初始化该合约时,使用TokenHolder合约的CreateScheme方法创建一个持币人分红方案(Token Holder Profit Scheme):
State.TokenHolderContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName);
State.TokenHolderContract.CreateScheme.Send(new CreateTokenHolderProfitSchemeInput
{
Symbol = Context.Variables.NativeSymbol,
MinimumLockMinutes = input.MinimumLockMinutes
});
return new Empty();
在持币人分红方案中,一个分红方案的实例与其创建者绑定,因此无需计算SchemeId(虽然其底层还是通过在Profit合约中创建分红方案实现的,但是这个关系被记录在TokenHolder合约中)。
考虑GetDividends的作用为记录每个区块高度该分红池的收益增长,因此需要在Donate的时候记录下来。Donate可以实现为:
public override Empty Donate(DonateInput input)
{
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
Symbol = input.Symbol,
Amount = input.Amount,
To = Context.Self
});
State.TokenContract.Approve.Send(new ApproveInput
{
Symbol = input.Symbol,
Amount = input.Amount,
Spender = State.TokenHolderContract.Value
});
State.TokenHolderContract.ContributeProfits.Send(new ContributeProfitsInput
{
SchemeManager = Context.Self,
Symbol = input.Symbol,
Amount = input.Amount
});
Context.Fire(new DonationReceived
{
From = Context.Sender,
Symbol = input.Symbol,
Amount = input.Amount,
PoolContract = Context.Self
});
var currentReceivedDividends = State.ReceivedDividends[Context.CurrentHeight];
if (currentReceivedDividends != null && currentReceivedDividends.Value.ContainsKey(input.Symbol))
{
currentReceivedDividends.Value[input.Symbol] =
currentReceivedDividends.Value[input.Symbol].Add(input.Amount);
}
else
{
currentReceivedDividends = new Dividends
{
Value =
{
{
input.Symbol, input.Amount
}
}
};
}
State.ReceivedDividends[Context.CurrentHeight] = currentReceivedDividends;
Context.LogDebug(() => string.Format("Contributed {0} {1}s to side chain dividends pool.", input.Amount, input.Symbol));
return new Empty();
}
对应的Release直接调用TokenHolder的DistributeProfits方法即可:
public override Empty Release(ReleaseInput input)
{
State.TokenHolderContract.DistributeProfits.Send(new DistributeProfitsInput
{
SchemeManager = Context.Self
});
return new Empty();
}
在持币人分红合约中,默认实现为收到什么代币的捐赠,就释放什么代币,因此SetSymbolList无需实现,GetSymbolList直接读取分红方案中记录的收到捐赠的代币种类即可:
public override Empty SetSymbolList(SymbolList input)
{
Assert(false, "Not support setting symbol list.");
return new Empty();
}
public override SymbolList GetSymbolList(Empty input)
{
return new SymbolList
{
Value =
{
GetDividendPoolScheme().ReceivedTokenSymbols
}
};
}
private Scheme GetDividendPoolScheme()
{
if (State.DividendPoolSchemeId.Value == null)
{
var tokenHolderScheme = State.TokenHolderContract.GetScheme.Call(Context.Self);
State.DividendPoolSchemeId.Value = tokenHolderScheme.SchemeId;
}
return Context.Call<Scheme>(
Context.GetContractAddressByName(SmartContractConstants.ProfitContractSystemName),
nameof(ProfitContractContainer.ProfitContractReferenceState.GetScheme),
State.DividendPoolSchemeId.Value);
}
GetUndistributedDividends的实现同上一节所述,查询余额返回即可:
public override Dividends GetUndistributedDividends(Empty input)
{
var scheme = GetDividendPoolScheme();
return new Dividends
{
Value =
{
scheme.ReceivedTokenSymbols.Select(s => State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = scheme.VirtualAddress,
Symbol = s
})).ToDictionary(b => b.Symbol, b => b.Balance)
}
};
}
除了借助Profit合约和TokenHolder合约,当然也可以自己实现一个分红池,满足接口的语义即可。
测试¶
以借助Token Holder合约实现的分红池为例,分两条线进行测试。
一是针对该分红池进行Donate、Release和一系列查询操作;
二是使用一个账户进行锁仓,然后取出分红。
定义好需要用到的Stub:
const long amount = 10_00000000;
var keyPair = SampleECKeyPairs.KeyPairs[0];
var address = Address.FromPublicKey(keyPair.PublicKey);
var acs10DemoContractStub =
GetTester<ACS10DemoContractContainer.ACS10DemoContractStub>(DAppContractAddress, keyPair);
var tokenContractStub =
GetTester<TokenContractContainer.TokenContractStub>(TokenContractAddress, keyPair);
var tokenHolderContractStub =
GetTester<TokenHolderContractContainer.TokenHolderContractStub>(TokenHolderContractAddress,
keyPair);
在进行操作之前,先对TokenHolder合约和实现了分红池的合约进行一下额度授权。
await tokenContractStub.Approve.SendAsync(new ApproveInput
{
Spender = TokenHolderContractAddress,
Symbol = "ELF",
Amount = long.MaxValue
});
await tokenContractStub.Approve.SendAsync(new ApproveInput
{
Spender = DAppContractAddress,
Symbol = "ELF",
Amount = long.MaxValue
});
锁仓,此时账户余额减少10ELF:
await tokenHolderContractStub.RegisterForProfits.SendAsync(new RegisterForProfitsInput
{
SchemeManager = DAppContractAddress,
Amount = amount
});
捐赠分红,此时账户余额再次减少10ELF:
await acs10DemoContractStub.Donate.SendAsync(new DonateInput
{
Symbol = "ELF",
Amount = amount
});
此时就可以测试GetUndistributedDividends和GetDividends接口了:
// Check undistributed dividends before releasing.
{
var undistributedDividends =
await acs10DemoContractStub.GetUndistributedDividends.CallAsync(new Empty());
undistributedDividends.Value["ELF"].ShouldBe(amount);
}
var blockchainService = Application.ServiceProvider.GetRequiredService<IBlockchainService>();
var currentBlockHeight = (await blockchainService.GetChainAsync()).BestChainHeight;
var dividends =
await acs10DemoContractStub.GetDividends.CallAsync(new Int64Value {Value = currentBlockHeight});
dividends.Value["ELF"].ShouldBe(amount);
释放分红,顺便再次测试GetUndistributedDividends接口是否做出来反应:
await acs10DemoContractStub.Release.SendAsync(new ReleaseInput
{
PeriodNumber = 1
});
// Check undistributed dividends after releasing.
{
var undistributedDividends =
await acs10DemoContractStub.GetUndistributedDividends.CallAsync(new Empty());
undistributedDividends.Value["ELF"].ShouldBe(0);
}
最后让这个账户领取分红,然后观察其账户余额变动:
var balanceBeforeClaimForProfits = await tokenContractStub.GetBalance.CallAsync(new GetBalanceInput
{
Owner = address,
Symbol = "ELF"
});
await tokenHolderContractStub.ClaimProfits.SendAsync(new ClaimProfitsInput
{
SchemeManager = DAppContractAddress,
Beneficiary = address
});
var balanceAfterClaimForProfits = await tokenContractStub.GetBalance.CallAsync(new GetBalanceInput
{
Owner = address,
Symbol = "ELF"
});
balanceAfterClaimForProfits.Balance.ShouldBe(balanceBeforeClaimForProfits.Balance + amount);