logo

Profit Dividend

ACS9 - Contract Profit Dividend Standard#

ACS9 defines a standard for distributing profits on aelf's side chain contracts.

Interface#

ACS9 includes several methods given below:

Methods#

Method NameRequest TypeResponse TypeDescription
TakeContractProfitsacs9.TakeContractProfitsInputgoogle.protobuf.EmptyAllows developers to collect contract profits.
GetProfitConfiggoogle.protobuf.Emptyacs9.ProfitConfigRetrieves profit distribution configuration.
GetProfitsAmountgoogle.protobuf.Emptyacs9.ProfitsMapQueries total profits accumulated by the contract.

Types#

acs9.ProfitConfig

FieldTypeDescriptionLabel
donation_parts_per_hundredint32Percentage of profit donated to dividend pool.
profits_token_symbol_liststringList of profit token symbols.repeated
staking_token_symbolstringToken symbol users can lock to claim profits.

acs9.ProfitsMap

FieldTypeDescriptionLabel
valuemap<string, int64>Profits accumulated, symbol -> amount.repeated

acs9.TakeContractProfitsInput

FieldTypeDescriptionLabel
symbolstringToken symbol to withdraw profits.
amountint64Amount of token to withdraw.

Implementation#

The contract initializes by creating a token called APP and establishing a profit distribution scheme using the TokenHolder contract. Users receive 10 APP tokens upon signing up and can deposit ELF to receive APP tokens. The Use method consumes APP tokens.

Upon initialization, ACS9 sets the profit configuration and enables profit distribution to the profit receiver and dividend pool.

The contract allows users to interact by signing up, depositing and withdrawing tokens, and using APP tokens for transactions. Developers can configure profit distribution settings and monitor accumulated profits.

  • Implementation of Initialize
  • 1
    public override Empty Initialize(InitializeInput input)
    2
    {
    3
    State.TokenHolderContract.Value =
    4
    Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName);
    5
    State.TokenContract.Value =
    6
    Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
    7
    State.DividendPoolContract.Value =
    8
    Context.GetContractAddressByName(input.DividendPoolContractName.Value.ToBase64());
    9
    State.Symbol.Value = input.Symbol == string.Empty ? "APP" : input.Symbol;
    10
    State.ProfitReceiver.Value = input.ProfitReceiver;
    11
    CreateToken(input.ProfitReceiver);
    12
    // To test TokenHolder Contract.
    13
    CreateTokenHolderProfitScheme();
    14
    // To test ACS9 workflow.
    15
    SetProfitConfig();
    16
    State.ProfitReceiver.Value = input.ProfitReceiver;
    17
    return new Empty();
    18
    }
    19
    private void CreateToken(Address profitReceiver, bool isLockWhiteListIncludingSelf = false)
    20
    {
    21
    var lockWhiteList = new List<Address>
    22
    {Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName)};
    23
    if (isLockWhiteListIncludingSelf)
    24
    lockWhiteList.Add(Context.Self);
    25
    State.TokenContract.Create.Send(new CreateInput
    26
    {
    27
    Symbol = State.Symbol.Value,
    28
    TokenName = "DApp Token",
    29
    Decimals = ACS9DemoContractConstants.Decimal,
    30
    Issuer = Context.Self,
    31
    IsBurnable = true,
    32
    IsProfitable = true,
    33
    TotalSupply = ACS9DemoContractConstants.TotalSupply,
    34
    LockWhiteList =
    35
    {
    36
    lockWhiteList
    37
    }
    38
    });
    39
    State.TokenContract.Issue.Send(new IssueInput
    40
    {
    41
    To = profitReceiver,
    42
    Amount = ACS9DemoContractConstants.TotalSupply / 5,
    43
    Symbol = State.Symbol.Value,
    44
    Memo = "Issue token for profit receiver"
    45
    });
    46
    }
    47
    private void CreateTokenHolderProfitScheme()
    48
    {
    49
    State.TokenHolderContract.CreateScheme.Send(new CreateTokenHolderProfitSchemeInput
    50
    {
    51
    Symbol = State.Symbol.Value
    52
    });
    53
    }
    54
    private void SetProfitConfig()
    55
    {
    56
    State.ProfitConfig.Value = new ProfitConfig
    57
    {
    58
    DonationPartsPerHundred = 1,
    59
    StakingTokenSymbol = "APP",
    60
    ProfitsTokenSymbolList = {"ELF"}
    61
    };
    62
    }
  • The user can use the SighUp method to register and get the bonus
  • 1
    /// <summary>
    2
    /// When user sign up, give him 10 APP tokens, then initialize his profile.
    3
    /// </summary>
    4
    /// <param name="input"></param>
    5
    /// <returns></returns>
    6
    public override Empty SignUp(Empty input)
    7
    {
    8
    Assert(State.Profiles[Context.Sender] == null, "Already registered.");
    9
    var profile = new Profile
    10
    {
    11
    UserAddress = Context.Sender
    12
    };
    13
    State.TokenContract.Issue.Send(new IssueInput
    14
    {
    15
    Symbol = State.Symbol.Value,
    16
    Amount = ACS9DemoContractConstants.ForNewUser,
    17
    To = Context.Sender
    18
    });
    19
    // Update profile.
    20
    profile.Records.Add(new Record
    21
    {
    22
    Type = RecordType.SignUp,
    23
    Timestamp = Context.CurrentBlockTime,
    24
    Description = string.Format("{0} +{1}",State.Symbol.Value, ACS9DemoContractConstants.ForNewUser)
    25
    });
    26
    State.Profiles[Context.Sender] = profile;
    27
    return new Empty();
    28
    }
  • Recharge and redemption:
  • 1
    public override Empty Deposit(DepositInput input)
    2
    {
    3
    // User Address -> DApp Contract.
    4
    State.TokenContract.TransferFrom.Send(new TransferFromInput
    5
    {
    6
    From = Context.Sender,
    7
    To = Context.Self,
    8
    Symbol = "ELF",
    9
    Amount = input.Amount
    10
    });
    11
    State.TokenContract.Issue.Send(new IssueInput
    12
    {
    13
    Symbol = State.Symbol.Value,
    14
    Amount = input.Amount,
    15
    To = Context.Sender
    16
    });
    17
    // Update profile.
    18
    var profile = State.Profiles[Context.Sender];
    19
    profile.Records.Add(new Record
    20
    {
    21
    Type = RecordType.Deposit,
    22
    Timestamp = Context.CurrentBlockTime,
    23
    Description = string.Format("{0} +{1}", State.Symbol.Value, input.Amount)
    24
    });
    25
    State.Profiles[Context.Sender] = profile;
    26
    return new Empty();
    27
    }
    28
    public override Empty Withdraw(WithdrawInput input)
    29
    {
    30
    State.TokenContract.TransferFrom.Send(new TransferFromInput
    31
    {
    32
    From = Context.Sender,
    33
    To = Context.Self,
    34
    Symbol = State.Symbol.Value,
    35
    Amount = input.Amount
    36
    });
    37
    State.TokenContract.Transfer.Send(new TransferInput
    38
    {
    39
    To = Context.Sender,
    40
    Symbol = input.Symbol,
    41
    Amount = input.Amount
    42
    });
    43
    State.TokenHolderContract.RemoveBeneficiary.Send(new RemoveTokenHolderBeneficiaryInput
    44
    {
    45
    Beneficiary = Context.Sender,
    46
    Amount = input.Amount
    47
    });
    48
    // Update profile.
    49
    var profile = State.Profiles[Context.Sender];
    50
    profile.Records.Add(new Record
    51
    {
    52
    Type = RecordType.Withdraw,
    53
    Timestamp = Context.CurrentBlockTime,
    54
    Description = string.Format("{0} -{1}", State.Symbol.Value, input.Amount)
    55
    });
    56
    State.Profiles[Context.Sender] = profile;
    57
    return new Empty();
    58
    }
  • Implementation of Use directly transfers 1/3 profits into the token holder dividend scheme:
  • 1
    public override Empty Use(Record input)
    2
    {
    3
    State.TokenContract.TransferFrom.Send(new TransferFromInput
    4
    {
    5
    From = Context.Sender,
    6
    To = Context.Self,
    7
    Symbol = State.Symbol.Value,
    8
    Amount = ACS9DemoContractConstants.UseFee
    9
    });
    10
    if (input.Symbol == string.Empty)
    11
    input.Symbol = State.TokenContract.GetPrimaryTokenSymbol.Call(new Empty()).Value;
    12
    var contributeAmount = ACS9DemoContractConstants.UseFee.Div(3);
    13
    State.TokenContract.Approve.Send(new ApproveInput
    14
    {
    15
    Spender = State.TokenHolderContract.Value,
    16
    Symbol = input.Symbol,
    17
    Amount = contributeAmount
    18
    });
    19
    // Contribute 1/3 profits (ELF) to profit scheme.
    20
    State.TokenHolderContract.ContributeProfits.Send(new ContributeProfitsInput
    21
    {
    22
    SchemeManager = Context.Self,
    23
    Amount = contributeAmount,
    24
    Symbol = input.Symbol
    25
    });
    26
    // Update profile.
    27
    var profile = State.Profiles[Context.Sender];
    28
    profile.Records.Add(new Record
    29
    {
    30
    Type = RecordType.Withdraw,
    31
    Timestamp = Context.CurrentBlockTime,
    32
    Description = string.Format("{0} -{1}", State.Symbol.Value, ACS9DemoContractConstants.UseFee),
    33
    Symbol = input.Symbol
    34
    });
    35
    State.Profiles[Context.Sender] = profile;
    36
    return new Empty();
    37
    }
  • Implement ACS9 for the perfect profit distribution:
  • 1
    public override Empty TakeContractProfits(TakeContractProfitsInput input)
    2
    {
    3
    var config = State.ProfitConfig.Value;
    4
    // For Side Chain Dividends Pool.
    5
    var amountForSideChainDividendsPool = input.Amount.Mul(config.DonationPartsPerHundred).Div(100);
    6
    State.TokenContract.Approve.Send(new ApproveInput
    7
    {
    8
    Symbol = input.Symbol,
    9
    Amount = amountForSideChainDividendsPool,
    10
    Spender = State.DividendPoolContract.Value
    11
    });
    12
    State.DividendPoolContract.Donate.Send(new DonateInput
    13
    {
    14
    Symbol = input.Symbol,
    15
    Amount = amountForSideChainDividendsPool
    16
    });
    17
    // For receiver.
    18
    var amountForReceiver = input.Amount.Sub(amountForSideChainDividendsPool);
    19
    State.TokenContract.Transfer.Send(new TransferInput
    20
    {
    21
    To = State.ProfitReceiver.Value,
    22
    Amount = amountForReceiver,
    23
    Symbol = input.Symbol
    24
    });
    25
    // For Token Holder Profit Scheme. (To distribute.)
    26
    State.TokenHolderContract.DistributeProfits.Send(new DistributeProfitsInput
    27
    {
    28
    SchemeManager = Context.Self
    29
    });
    30
    return new Empty();
    31
    }
    32
    public override ProfitConfig GetProfitConfig(Empty input)
    33
    {
    34
    return State.ProfitConfig.Value;
    35
    }
    36
    public override ProfitsMap GetProfitsAmount(Empty input)
    37
    {
    38
    var profitsMap = new ProfitsMap();
    39
    foreach (var symbol in State.ProfitConfig.Value.ProfitsTokenSymbolList)
    40
    {
    41
    var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput
    42
    {
    43
    Owner = Context.Self,
    44
    Symbol = symbol
    45
    }).Balance;
    46
    profitsMap.Value[symbol] = balance;
    47
    }
    48
    return profitsMap;
    49
    }

    Test#

    Testing involves deploying contracts implementing ACS9 or ACS10, initializing the ACS9 contract using IContractInitializationProvider, and verifying profit distribution among stakeholders.

  • Before the testing begins, the contract implementing ACS9 can be initialized by interface IContractInitializationProvider
  • 1
    public class ACS9DemoContractInitializationProvider : IContractInitializationProvider
    2
    {
    3
    public List<InitializeMethod> GetInitializeMethodList(byte[] contractCode)
    4
    {
    5
    return new List<InitializeMethod>
    6
    {
    7
    new InitializeMethod
    8
    {
    9
    MethodName = nameof(ACS9DemoContract.Initialize),
    10
    Params = new InitializeInput
    11
    {
    12
    ProfitReceiver = Address.FromPublicKey(SampleECKeyPairs.KeyPairs.Skip(3).First().PublicKey),
    13
    DividendPoolContractName = ACS10DemoSmartContractNameProvider.Name
    14
    }.ToByteString()
    15
    }
    16
    };
    17
    }
    18
    public Hash SystemSmartContractName { get; } = ACS9DemoSmartContractNameProvider.Name;
    19
    public string ContractCodeName { get; } = "AElf.Contracts.ACS9DemoContract";
    20
    }
  • Prepare a user account:
  • 1
    protected List<ECKeyPair> UserKeyPairs => SampleECKeyPairs.KeyPairs.Skip(2).Take(3).ToList();
  • Prepare some Stubs:
  • 1
    var keyPair = UserKeyPairs[0];
    2
    var address = Address.FromPublicKey(keyPair.PublicKey);
    3
    // Prepare stubs.
    4
    var acs9DemoContractStub = GetACS9DemoContractStub(keyPair);
    5
    var acs10DemoContractStub = GetACS10DemoContractStub(keyPair);
    6
    var userTokenStub =
    7
    GetTester<TokenContractImplContainer.TokenContractImplStub>(TokenContractAddress, UserKeyPairs[0]);
    8
    var userTokenHolderStub =
    9
    GetTester<TokenHolderContractContainer.TokenHolderContractStub>(TokenHolderContractAddress,
    10
    UserKeyPairs[0]);
  • Then, transfer ELF to the user (TokenContractStub is the Stub of the initial bp who has much ELF) :
  • 1
    // Transfer some ELFs to user.
    2
    await TokenContractStub.Transfer.SendAsync(new TransferInput
    3
    {
    4
    To = address,
    5
    Symbol = "ELF",
    6
    Amount = 1000_00000000
    7
    });
  • User have to call SignUp to check if they got 10 APP tokens:
  • 1
    await acs9DemoContractStub.SignUp.SendAsync(new Empty());
    2
    // User has 10 APP tokens because of signing up.
    3
    (await GetFirstUserBalance("APP")).ShouldBe(10_00000000);
  • Test the recharge method of the contract itself:
  • 1
    var elfBalanceBefore = await GetFirstUserBalance("ELF");
    2
    // User has to Approve an amount of ELF tokens before deposit to the DApp.
    3
    await userTokenStub.Approve.SendAsync(new ApproveInput
    4
    {
    5
    Amount = 1000_00000000,
    6
    Spender = ACS9DemoContractAddress,
    7
    Symbol = "ELF"
    8
    });
    9
    await acs9DemoContractStub.Deposit.SendAsync(new DepositInput
    10
    {
    11
    Amount = 100_00000000
    12
    });
    13
    // Check the change of balance of ELF.
    14
    var elfBalanceAfter = await GetFirstUserBalance("ELF");
    15
    elfBalanceAfter.ShouldBe(elfBalanceBefore - 100_00000000);
    16
    // Now user has 110 APP tokens.
    17
    (await GetFirstUserBalance("APP")).ShouldBe(110_00000000);
  • The user locks up 57 APP via the TokenHolder contract in order to obtain profits from the contract:
  • 1
    // User lock some APP tokens for getting profits. (APP -57)
    2
    await userTokenHolderStub.RegisterForProfits.SendAsync(new RegisterForProfitsInput
    3
    {
    4
    SchemeManager = ACS9DemoContractAddress,
    5
    Amount = 57_00000000
    6
    });
  • The Use method is invoked 10 times and 0.3 APP is consumed each time, and finally the user have 50 APP left:
  • 1
    await userTokenStub.Approve.SendAsync(new ApproveInput
    2
    {
    3
    Amount = long.MaxValue,
    4
    Spender = ACS9DemoContractAddress,
    5
    Symbol = "APP"
    6
    });
    7
    // User uses 10 times of this DApp. (APP -3)
    8
    for (var i = 0; i < 10; i++)
    9
    {
    10
    await acs9DemoContractStub.Use.SendAsync(new Record());
    11
    }
    12
    // Now user has 50 APP tokens.
    13
    (await GetFirstUserBalance("APP")).ShouldBe(50_00000000);
  • Using the TakeContractProfits method, the developer attempts to withdraw 10 ELF as profits. The 10 ELF will be transferred to the developer in this method:
  • 1
    const long baseBalance = 0;
    2
    {
    3
    var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput
    4
    {
    5
    Owner = UserAddresses[1], Symbol = "ELF"
    6
    });
    7
    balance.Balance.ShouldBe(baseBalance);
    8
    }
    9
    // Profits receiver claim 10 ELF profits.
    10
    await acs9DemoContractStub.TakeContractProfits.SendAsync(new TakeContractProfitsInput
    11
    {
    12
    Symbol = "ELF",
    13
    Amount = 10_0000_0000
    14
    });
    15
    // Then profits receiver should have 9.9 ELF tokens.
    16
    {
    17
    var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput
    18
    {
    19
    Owner = UserAddresses[1], Symbol = "ELF"
    20
    });
    21
    balance.Balance.ShouldBe(baseBalance + 9_9000_0000);
    22
    }
  • Next check the profit distribution results. The dividend pool should be allocated 0.1 ELF:
  • 1
    // And Side Chain Dividends Pool should have 0.1 ELF tokens.
    2
    {
    3
    var scheme = await TokenHolderContractStub.GetScheme.CallAsync(ACS10DemoContractAddress);
    4
    var virtualAddress = await ProfitContractStub.GetSchemeAddress.CallAsync(new SchemePeriod
    5
    {
    6
    SchemeId = scheme.SchemeId,
    7
    Period = 0
    8
    });
    9
    var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput
    10
    {
    11
    Owner = virtualAddress,
    12
    Symbol = "ELF"
    13
    });
    14
    balance.Balance.ShouldBe(1000_0000);
    15
    }
  • The user receives 1 ELF from the token holder dividend scheme:
  • 1
    // Help user to claim profits from token holder profit scheme.
    2
    await TokenHolderContractStub.ClaimProfits.SendAsync(new ClaimProfitsInput
    3
    {
    4
    Beneficiary = UserAddresses[0],
    5
    SchemeManager = ACS9DemoContractAddress,
    6
    });
    7
    // Profits should be 1 ELF.
    8
    (await GetFirstUserBalance("ELF")).ShouldBe(elfBalanceAfter + 1_0000_0000);
  • Finally, let’s test the Withdraw method.
  • 1
    // Withdraw
    2
    var beforeBalance =
    3
    await userTokenStub.GetBalance.CallAsync(new GetBalanceInput
    4
    {
    5
    Symbol = "APP",
    6
    Owner = UserAddresses[0]
    7
    });
    8
    var withDrawResult = await userTokenHolderStub.Withdraw.SendAsync(ACS9DemoContractAddress);
    9
    withDrawResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined);
    10
    var resultBalance = await userTokenStub.GetBalance.CallAsync(new GetBalanceInput
    11
    {
    12
    Symbol = "APP",
    13
    Owner = UserAddresses[0]
    14
    });
    15
    resultBalance.Balance.ShouldBe(beforeBalance.Balance + 57_00000000);

    This documentation provides a framework for implementing profit distribution and verifying its functionality through various test scenarios.

    Edited on: 15 July 2024 05:28:10 GMT+0