ACS10 - Dividend Pool Standard
ACS10 is used to construct a dividend pool in the contract.
Interface
To construct a dividend pool, you can implement the following interfaces optionally:
Methods
Method Name |
Request Type |
Response Type |
Description |
---|---|---|---|
Donate |
Donates tokens from the caller to the treasury. If the tokens are not native tokens in the current chain, they will be first converted to the native token. |
||
Release |
Release dividend pool according the period number. |
||
SetSymbolList |
Set the token symbols dividend pool supports. |
||
GetSymbolList |
Query the token symbols dividend pool supports. |
||
GetUndistributedDividends |
Query the balance of undistributed tokens whose symbols are included in the symbol list. |
||
GetDividends |
Query the dividend information according to the height. |
Types
acs10.Dividends
Field |
Type |
Description |
Label |
---|---|---|---|
value |
The dividends, symbol -> amount. |
repeated |
acs10.Dividends.ValueEntry
Field |
Type |
Description |
Label |
---|---|---|---|
key |
|||
value |
acs10.DonateInput
Field |
Type |
Description |
Label |
---|---|---|---|
symbol |
The token symbol to donate. |
||
amount |
The amount to donate. |
acs10.DonationReceived
Field |
Type |
Description |
Label |
---|---|---|---|
from |
The address of donors. |
||
pool_contract |
The address of dividend pool. |
||
symbol |
The token symbol Donated. |
||
amount |
The amount Donated. |
acs10.ReleaseInput
Field |
Type |
Description |
Label |
---|---|---|---|
period_number |
The period number to release. |
acs10.SymbolList
Field |
Type |
Description |
Label |
---|---|---|---|
value |
The token symbol list. |
repeated |
aelf.Address
Field |
Type |
Description |
Label |
---|---|---|---|
value |
aelf.BinaryMerkleTree
Field |
Type |
Description |
Label |
---|---|---|---|
nodes |
The leaf nodes. |
repeated |
|
root |
The root node hash. |
||
leaf_count |
The count of leaf node. |
aelf.Hash
Field |
Type |
Description |
Label |
---|---|---|---|
value |
aelf.LogEvent
Field |
Type |
Description |
Label |
---|---|---|---|
address |
The contract address. |
||
name |
The name of the log event. |
||
indexed |
The indexed data, used to calculate bloom. |
repeated |
|
non_indexed |
The non indexed data. |
aelf.MerklePath
Field |
Type |
Description |
Label |
---|---|---|---|
merkle_path_nodes |
The merkle path nodes. |
repeated |
aelf.MerklePathNode
Field |
Type |
Description |
Label |
---|---|---|---|
hash |
The node hash. |
||
is_left_child_node |
Whether it is a left child node. |
aelf.SInt32Value
Field |
Type |
Description |
Label |
---|---|---|---|
value |
aelf.SInt64Value
Field |
Type |
Description |
Label |
---|---|---|---|
value |
aelf.ScopedStatePath
Field |
Type |
Description |
Label |
---|---|---|---|
address |
The scope address, which will be the contract address. |
||
path |
The path of contract state. |
aelf.SmartContractRegistration
Field |
Type |
Description |
Label |
---|---|---|---|
category |
The category of contract code(0: C#). |
||
code |
The byte array of the contract code. |
||
code_hash |
The hash of the contract code. |
||
is_system_contract |
Whether it is a system contract. |
||
version |
The version of the current contract. |
aelf.StatePath
Field |
Type |
Description |
Label |
---|---|---|---|
parts |
The partial path of the state path. |
repeated |
aelf.Transaction
Field |
Type |
Description |
Label |
---|---|---|---|
from |
The address of the sender of the transaction. |
||
to |
The address of the contract when calling a contract. |
||
ref_block_number |
The height of the referenced block hash. |
||
ref_block_prefix |
The first four bytes of the referenced block hash. |
||
method_name |
The name of a method in the smart contract at the To address. |
||
params |
The parameters to pass to the smart contract method. |
||
signature |
When signing a transaction it’s actually a subset of the fields: from/to and the target method as well as the parameter that were given. It also contains the reference block number and prefix. |
aelf.TransactionExecutingStateSet
Field |
Type |
Description |
Label |
---|---|---|---|
writes |
The changed states. |
repeated |
|
reads |
The read states. |
repeated |
|
deletes |
The deleted states. |
repeated |
aelf.TransactionExecutingStateSet.DeletesEntry
Field |
Type |
Description |
Label |
---|---|---|---|
key |
|||
value |
aelf.TransactionExecutingStateSet.ReadsEntry
Field |
Type |
Description |
Label |
---|---|---|---|
key |
|||
value |
aelf.TransactionExecutingStateSet.WritesEntry
Field |
Type |
Description |
Label |
---|---|---|---|
key |
|||
value |
aelf.TransactionResult
Field |
Type |
Description |
Label |
---|---|---|---|
transaction_id |
The transaction id. |
||
status |
The transaction result status. |
||
logs |
The log events. |
repeated |
|
bloom |
Bloom filter for transaction logs. A transaction log event can be defined in the contract and stored in the bloom filter after the transaction is executed. Through this filter, we can quickly search for and determine whether a log exists in the transaction result. |
||
return_value |
The return value of the transaction execution. |
||
block_number |
The height of the block hat packages the transaction. |
||
block_hash |
The hash of the block hat packages the transaction. |
||
error |
Failed execution error message. |
aelf.TransactionResultStatus
Name |
Number |
Description |
---|---|---|
NOT_EXISTED |
0 |
The execution result of the transaction does not exist. |
PENDING |
1 |
The transaction is in the transaction pool waiting to be packaged. |
FAILED |
2 |
Transaction execution failed. |
MINED |
3 |
The transaction was successfully executed and successfully packaged into a block. |
CONFLICT |
4 |
When executed in parallel, there are conflicts with other transactions. |
PENDING_VALIDATION |
5 |
The transaction is waiting for validation. |
NODE_VALIDATION_FAILED |
6 |
Transaction validation failed. |
Usage
ACS10 only unifies the standard interface of the dividend pool, which does not interact with the AElf chain.
Implementation
With the Profit contract
A Profit Scheme can be created using the CreateScheme
method of
Profit contract
:
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);
The Context.GenerateId method is a common method used by the AElf to generate Id. We use the address of the Profit contract and the schemeToken provided to the Profit contract to calculate the Id of the scheme, and we set this id to State.ProfitSchemeId (SingletonState<Hash>).
After the establishment of the dividend scheme:
ContributeProfits
method of Profit can be used to implement the method Donate in ACS10.The Release in the ACS10 can be implemented using the method
DistributeProfits
in theProfit contract
;Methods such as
AddBeneficiary
andRemoveBeneficiary
can be used to manage the recipients and their weight.AddSubScheme
,RemoveSubScheme
and other methods can be used to manage the sub-dividend scheme and its weight;The
SetSymbolList
andGetSymbolList
can be implemented by yourself. Just make sure the symbol list you set is used correctly inDonate
andRelease
.GetUndistributedDividends
returns the balance of the token whose symbol is included in symbol list.
With TokenHolder Contract
When initializing the contract, you should create a token holder dividend scheme using the CreateScheme in the TokenHolder contract(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();
In a token holder dividend scheme, a scheme is bound to its creator, so SchemeId is not necessary to compute (in fact, the scheme is created via the Profit contract).
Considering the GetDividends
returns the dividend information
according to the input height, so each Donate need update dividend
information for each height . A Donate can be implemented as:
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();
}
The method Release directly sends the TokenHolder’s method
DistributeProfits
transaction:
public override Empty Release(ReleaseInput input)
{
State.TokenHolderContract.DistributeProfits.Send(new DistributeProfitsInput
{
SchemeManager = Context.Self
});
return new Empty();
}
In the TokenHolder contract
, the default implementation is to
release what token is received, so SetSymbolList
does not need to be
implemented, and GetSymbolList
returns the symbol list recorded in
dividend scheme:
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);
}
The implementation of GetUndistributedDividends
is the same as
described in the previous section, and it returns the balance:
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)
}
};
}
In addition to the Profit
and TokenHolder
contracts, of course,
you can also implement a dividend pool on your own contract.
Test
The dividend pool, for example, is tested in two ways with the
TokenHolder contract
.
One way is for the dividend pool to send Donate, Release and a series of query operations;
The other way is to use an account to lock up, and then take out dividends.
Define the required Stubs:
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);
Before proceeding, You should Approve the TokenHolder contract
and
the dividend pool contract.
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
});
Lock the position, at which point the account balance is reduced by 10 ELF:
await tokenHolderContractStub.RegisterForProfits.SendAsync(new RegisterForProfitsInput
{
SchemeManager = DAppContractAddress,
Amount = amount
});
Donate, at which point the account balance is reduced by another 10 ELF:
await acs10DemoContractStub.Donate.SendAsync(new DonateInput
{
Symbol = "ELF",
Amount = amount
});
At this point you can test the GetUndistributedDividends
and
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);
Release bonus, and test GetUndistributedDividends
again:
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);
}
Finally, let this account receive the dividend and then observe the change in its balance:
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);
Example
The dividend pool of the main chain and the side chain is built by implementing ACS10.
The dividend pool provided by the Treasury contract
implementing
ACS10 is on the main chain.
The dividend pool provided by the Consensus contract
implementing
ACS10 is on the side chain.