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

acs10.DonateInput

google.protobuf.Empty

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

acs10.ReleaseInput

google.protobuf.Empty

Release dividend pool according the period number.

SetSymbolList

acs10.SymbolList

google.protobuf.Empty

Set the token symbols dividend pool supports.

GetSymbolList

google.protobuf.Empty

acs10.SymbolList

Query the token symbols dividend pool supports.

GetUndistributedDividends

google.protobuf.Empty

acs10.Dividends

Query the balance of undistributed tokens whose symbols are included in the symbol list.

GetDividends

google.protobuf.Int64Value

acs10.Dividends

Query the dividend information according to the height.

Types

acs10.Dividends

Field

Type

Description

Label

value

Dividends.ValueEntry

The dividends, symbol -> amount.

repeated

acs10.Dividends.ValueEntry

Field

Type

Description

Label

key

string

value

int64

acs10.DonateInput

Field

Type

Description

Label

symbol

string

The token symbol to donate.

amount

int64

The amount to donate.

acs10.DonationReceived

Field

Type

Description

Label

from

aelf.Address

The address of donors.

pool_contract

aelf.Address

The address of dividend pool.

symbol

string

The token symbol Donated.

amount

int64

The amount Donated.

acs10.ReleaseInput

Field

Type

Description

Label

period_number

int64

The period number to release.

acs10.SymbolList

Field

Type

Description

Label

value

string

The token symbol list.

repeated

aelf.Address

Field

Type

Description

Label

value

bytes

aelf.BinaryMerkleTree

Field

Type

Description

Label

nodes

Hash

The leaf nodes.

repeated

root

Hash

The root node hash.

leaf_count

int32

The count of leaf node.

aelf.Hash

Field

Type

Description

Label

value

bytes

aelf.LogEvent

Field

Type

Description

Label

address

Address

The contract address.

name

string

The name of the log event.

indexed

bytes

The indexed data, used to calculate bloom.

repeated

non_indexed

bytes

The non indexed data.

aelf.MerklePath

Field

Type

Description

Label

merkle_path_nodes

MerklePathNode

The merkle path nodes.

repeated

aelf.MerklePathNode

Field

Type

Description

Label

hash

Hash

The node hash.

is_left_child_node

bool

Whether it is a left child node.

aelf.SInt32Value

Field

Type

Description

Label

value

sint32

aelf.SInt64Value

Field

Type

Description

Label

value

sint64

aelf.ScopedStatePath

Field

Type

Description

Label

address

Address

The scope address, which will be the contract address.

path

StatePath

The path of contract state.

aelf.SmartContractRegistration

Field

Type

Description

Label

category

sint32

The category of contract code(0: C#).

code

bytes

The byte array of the contract code.

code_hash

Hash

The hash of the contract code.

is_system_contract

bool

Whether it is a system contract.

version

int32

The version of the current contract.

aelf.StatePath

Field

Type

Description

Label

parts

string

The partial path of the state path.

repeated

aelf.Transaction

Field

Type

Description

Label

from

Address

The address of the sender of the transaction.

to

Address

The address of the contract when calling a contract.

ref_block_number

int64

The height of the referenced block hash.

ref_block_prefix

bytes

The first four bytes of the referenced block hash.

method_name

string

The name of a method in the smart contract at the To address.

params

bytes

The parameters to pass to the smart contract method.

signature

bytes

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

TransactionExecutingStateSet.WritesEntry

The changed states.

repeated

reads

TransactionExecutingStateSet.ReadsEntry

The read states.

repeated

deletes

TransactionExecutingStateSet.DeletesEntry

The deleted states.

repeated

aelf.TransactionExecutingStateSet.DeletesEntry

Field

Type

Description

Label

key

string

value

bool

aelf.TransactionExecutingStateSet.ReadsEntry

Field

Type

Description

Label

key

string

value

bool

aelf.TransactionExecutingStateSet.WritesEntry

Field

Type

Description

Label

key

string

value

bytes

aelf.TransactionResult

Field

Type

Description

Label

transaction_id

Hash

The transaction id.

status

TransactionResultStatus

The transaction result status.

logs

LogEvent

The log events.

repeated

bloom

bytes

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

bytes

The return value of the transaction execution.

block_number

int64

The height of the block hat packages the transaction.

block_hash

Hash

The hash of the block hat packages the transaction.

error

string

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 the Profit contract;

  • Methods such as AddBeneficiary and RemoveBeneficiary 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 and GetSymbolList can be implemented by yourself. Just make sure the symbol list you set is used correctly in Donate and Release.

  • 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.