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 Name | Request Type | Response Type | Description |
---|---|---|---|
TakeContractProfits | acs9.TakeContractProfitsInput | google.protobuf.Empty | Allows developers to collect contract profits. |
GetProfitConfig | google.protobuf.Empty | acs9.ProfitConfig | Retrieves profit distribution configuration. |
GetProfitsAmount | google.protobuf.Empty | acs9.ProfitsMap | Queries total profits accumulated by the contract. |
Types#
acs9.ProfitConfig
Field | Type | Description | Label |
---|---|---|---|
donation_parts_per_hundred | int32 | Percentage of profit donated to dividend pool. | |
profits_token_symbol_list | string | List of profit token symbols. | repeated |
staking_token_symbol | string | Token symbol users can lock to claim profits. |
acs9.ProfitsMap
Field | Type | Description | Label |
---|---|---|---|
value | map<string, int64> | Profits accumulated, symbol -> amount. | repeated |
acs9.TakeContractProfitsInput
Field | Type | Description | Label |
---|---|---|---|
symbol | string | Token symbol to withdraw profits. | |
amount | int64 | Amount 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.
1public override Empty Initialize(InitializeInput input)2{3State.TokenHolderContract.Value =4Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName);5State.TokenContract.Value =6Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);7State.DividendPoolContract.Value =8Context.GetContractAddressByName(input.DividendPoolContractName.Value.ToBase64());9State.Symbol.Value = input.Symbol == string.Empty ? "APP" : input.Symbol;10State.ProfitReceiver.Value = input.ProfitReceiver;11CreateToken(input.ProfitReceiver);12// To test TokenHolder Contract.13CreateTokenHolderProfitScheme();14// To test ACS9 workflow.15SetProfitConfig();16State.ProfitReceiver.Value = input.ProfitReceiver;17return new Empty();18}19private void CreateToken(Address profitReceiver, bool isLockWhiteListIncludingSelf = false)20{21var lockWhiteList = new List<Address>22{Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName)};23if (isLockWhiteListIncludingSelf)24lockWhiteList.Add(Context.Self);25State.TokenContract.Create.Send(new CreateInput26{27Symbol = State.Symbol.Value,28TokenName = "DApp Token",29Decimals = ACS9DemoContractConstants.Decimal,30Issuer = Context.Self,31IsBurnable = true,32IsProfitable = true,33TotalSupply = ACS9DemoContractConstants.TotalSupply,34LockWhiteList =35{36lockWhiteList37}38});39State.TokenContract.Issue.Send(new IssueInput40{41To = profitReceiver,42Amount = ACS9DemoContractConstants.TotalSupply / 5,43Symbol = State.Symbol.Value,44Memo = "Issue token for profit receiver"45});46}47private void CreateTokenHolderProfitScheme()48{49State.TokenHolderContract.CreateScheme.Send(new CreateTokenHolderProfitSchemeInput50{51Symbol = State.Symbol.Value52});53}54private void SetProfitConfig()55{56State.ProfitConfig.Value = new ProfitConfig57{58DonationPartsPerHundred = 1,59StakingTokenSymbol = "APP",60ProfitsTokenSymbolList = {"ELF"}61};62}
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>6public override Empty SignUp(Empty input)7{8Assert(State.Profiles[Context.Sender] == null, "Already registered.");9var profile = new Profile10{11UserAddress = Context.Sender12};13State.TokenContract.Issue.Send(new IssueInput14{15Symbol = State.Symbol.Value,16Amount = ACS9DemoContractConstants.ForNewUser,17To = Context.Sender18});19// Update profile.20profile.Records.Add(new Record21{22Type = RecordType.SignUp,23Timestamp = Context.CurrentBlockTime,24Description = string.Format("{0} +{1}",State.Symbol.Value, ACS9DemoContractConstants.ForNewUser)25});26State.Profiles[Context.Sender] = profile;27return new Empty();28}
1public override Empty Deposit(DepositInput input)2{3// User Address -> DApp Contract.4State.TokenContract.TransferFrom.Send(new TransferFromInput5{6From = Context.Sender,7To = Context.Self,8Symbol = "ELF",9Amount = input.Amount10});11State.TokenContract.Issue.Send(new IssueInput12{13Symbol = State.Symbol.Value,14Amount = input.Amount,15To = Context.Sender16});17// Update profile.18var profile = State.Profiles[Context.Sender];19profile.Records.Add(new Record20{21Type = RecordType.Deposit,22Timestamp = Context.CurrentBlockTime,23Description = string.Format("{0} +{1}", State.Symbol.Value, input.Amount)24});25State.Profiles[Context.Sender] = profile;26return new Empty();27}28public override Empty Withdraw(WithdrawInput input)29{30State.TokenContract.TransferFrom.Send(new TransferFromInput31{32From = Context.Sender,33To = Context.Self,34Symbol = State.Symbol.Value,35Amount = input.Amount36});37State.TokenContract.Transfer.Send(new TransferInput38{39To = Context.Sender,40Symbol = input.Symbol,41Amount = input.Amount42});43State.TokenHolderContract.RemoveBeneficiary.Send(new RemoveTokenHolderBeneficiaryInput44{45Beneficiary = Context.Sender,46Amount = input.Amount47});48// Update profile.49var profile = State.Profiles[Context.Sender];50profile.Records.Add(new Record51{52Type = RecordType.Withdraw,53Timestamp = Context.CurrentBlockTime,54Description = string.Format("{0} -{1}", State.Symbol.Value, input.Amount)55});56State.Profiles[Context.Sender] = profile;57return new Empty();58}
1public override Empty Use(Record input)2{3State.TokenContract.TransferFrom.Send(new TransferFromInput4{5From = Context.Sender,6To = Context.Self,7Symbol = State.Symbol.Value,8Amount = ACS9DemoContractConstants.UseFee9});10if (input.Symbol == string.Empty)11input.Symbol = State.TokenContract.GetPrimaryTokenSymbol.Call(new Empty()).Value;12var contributeAmount = ACS9DemoContractConstants.UseFee.Div(3);13State.TokenContract.Approve.Send(new ApproveInput14{15Spender = State.TokenHolderContract.Value,16Symbol = input.Symbol,17Amount = contributeAmount18});19// Contribute 1/3 profits (ELF) to profit scheme.20State.TokenHolderContract.ContributeProfits.Send(new ContributeProfitsInput21{22SchemeManager = Context.Self,23Amount = contributeAmount,24Symbol = input.Symbol25});26// Update profile.27var profile = State.Profiles[Context.Sender];28profile.Records.Add(new Record29{30Type = RecordType.Withdraw,31Timestamp = Context.CurrentBlockTime,32Description = string.Format("{0} -{1}", State.Symbol.Value, ACS9DemoContractConstants.UseFee),33Symbol = input.Symbol34});35State.Profiles[Context.Sender] = profile;36return new Empty();37}
1public override Empty TakeContractProfits(TakeContractProfitsInput input)2{3var config = State.ProfitConfig.Value;4// For Side Chain Dividends Pool.5var amountForSideChainDividendsPool = input.Amount.Mul(config.DonationPartsPerHundred).Div(100);6State.TokenContract.Approve.Send(new ApproveInput7{8Symbol = input.Symbol,9Amount = amountForSideChainDividendsPool,10Spender = State.DividendPoolContract.Value11});12State.DividendPoolContract.Donate.Send(new DonateInput13{14Symbol = input.Symbol,15Amount = amountForSideChainDividendsPool16});17// For receiver.18var amountForReceiver = input.Amount.Sub(amountForSideChainDividendsPool);19State.TokenContract.Transfer.Send(new TransferInput20{21To = State.ProfitReceiver.Value,22Amount = amountForReceiver,23Symbol = input.Symbol24});25// For Token Holder Profit Scheme. (To distribute.)26State.TokenHolderContract.DistributeProfits.Send(new DistributeProfitsInput27{28SchemeManager = Context.Self29});30return new Empty();31}32public override ProfitConfig GetProfitConfig(Empty input)33{34return State.ProfitConfig.Value;35}36public override ProfitsMap GetProfitsAmount(Empty input)37{38var profitsMap = new ProfitsMap();39foreach (var symbol in State.ProfitConfig.Value.ProfitsTokenSymbolList)40{41var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput42{43Owner = Context.Self,44Symbol = symbol45}).Balance;46profitsMap.Value[symbol] = balance;47}48return profitsMap;49}
Test#
Testing involves deploying contracts implementing ACS9 or ACS10, initializing the ACS9 contract using IContractInitializationProvider, and verifying profit distribution among stakeholders.
1public class ACS9DemoContractInitializationProvider : IContractInitializationProvider2{3public List<InitializeMethod> GetInitializeMethodList(byte[] contractCode)4{5return new List<InitializeMethod>6{7new InitializeMethod8{9MethodName = nameof(ACS9DemoContract.Initialize),10Params = new InitializeInput11{12ProfitReceiver = Address.FromPublicKey(SampleECKeyPairs.KeyPairs.Skip(3).First().PublicKey),13DividendPoolContractName = ACS10DemoSmartContractNameProvider.Name14}.ToByteString()15}16};17}18public Hash SystemSmartContractName { get; } = ACS9DemoSmartContractNameProvider.Name;19public string ContractCodeName { get; } = "AElf.Contracts.ACS9DemoContract";20}
1protected List<ECKeyPair> UserKeyPairs => SampleECKeyPairs.KeyPairs.Skip(2).Take(3).ToList();
1var keyPair = UserKeyPairs[0];2var address = Address.FromPublicKey(keyPair.PublicKey);3// Prepare stubs.4var acs9DemoContractStub = GetACS9DemoContractStub(keyPair);5var acs10DemoContractStub = GetACS10DemoContractStub(keyPair);6var userTokenStub =7GetTester<TokenContractImplContainer.TokenContractImplStub>(TokenContractAddress, UserKeyPairs[0]);8var userTokenHolderStub =9GetTester<TokenHolderContractContainer.TokenHolderContractStub>(TokenHolderContractAddress,10UserKeyPairs[0]);
1// Transfer some ELFs to user.2await TokenContractStub.Transfer.SendAsync(new TransferInput3{4To = address,5Symbol = "ELF",6Amount = 1000_000000007});
1await acs9DemoContractStub.SignUp.SendAsync(new Empty());2// User has 10 APP tokens because of signing up.3(await GetFirstUserBalance("APP")).ShouldBe(10_00000000);
1var elfBalanceBefore = await GetFirstUserBalance("ELF");2// User has to Approve an amount of ELF tokens before deposit to the DApp.3await userTokenStub.Approve.SendAsync(new ApproveInput4{5Amount = 1000_00000000,6Spender = ACS9DemoContractAddress,7Symbol = "ELF"8});9await acs9DemoContractStub.Deposit.SendAsync(new DepositInput10{11Amount = 100_0000000012});13// Check the change of balance of ELF.14var elfBalanceAfter = await GetFirstUserBalance("ELF");15elfBalanceAfter.ShouldBe(elfBalanceBefore - 100_00000000);16// Now user has 110 APP tokens.17(await GetFirstUserBalance("APP")).ShouldBe(110_00000000);
1// User lock some APP tokens for getting profits. (APP -57)2await userTokenHolderStub.RegisterForProfits.SendAsync(new RegisterForProfitsInput3{4SchemeManager = ACS9DemoContractAddress,5Amount = 57_000000006});
1await userTokenStub.Approve.SendAsync(new ApproveInput2{3Amount = long.MaxValue,4Spender = ACS9DemoContractAddress,5Symbol = "APP"6});7// User uses 10 times of this DApp. (APP -3)8for (var i = 0; i < 10; i++)9{10await acs9DemoContractStub.Use.SendAsync(new Record());11}12// Now user has 50 APP tokens.13(await GetFirstUserBalance("APP")).ShouldBe(50_00000000);
1const long baseBalance = 0;2{3var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput4{5Owner = UserAddresses[1], Symbol = "ELF"6});7balance.Balance.ShouldBe(baseBalance);8}9// Profits receiver claim 10 ELF profits.10await acs9DemoContractStub.TakeContractProfits.SendAsync(new TakeContractProfitsInput11{12Symbol = "ELF",13Amount = 10_0000_000014});15// Then profits receiver should have 9.9 ELF tokens.16{17var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput18{19Owner = UserAddresses[1], Symbol = "ELF"20});21balance.Balance.ShouldBe(baseBalance + 9_9000_0000);22}
1// And Side Chain Dividends Pool should have 0.1 ELF tokens.2{3var scheme = await TokenHolderContractStub.GetScheme.CallAsync(ACS10DemoContractAddress);4var virtualAddress = await ProfitContractStub.GetSchemeAddress.CallAsync(new SchemePeriod5{6SchemeId = scheme.SchemeId,7Period = 08});9var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput10{11Owner = virtualAddress,12Symbol = "ELF"13});14balance.Balance.ShouldBe(1000_0000);15}
1// Help user to claim profits from token holder profit scheme.2await TokenHolderContractStub.ClaimProfits.SendAsync(new ClaimProfitsInput3{4Beneficiary = UserAddresses[0],5SchemeManager = ACS9DemoContractAddress,6});7// Profits should be 1 ELF.8(await GetFirstUserBalance("ELF")).ShouldBe(elfBalanceAfter + 1_0000_0000);
1// Withdraw2var beforeBalance =3await userTokenStub.GetBalance.CallAsync(new GetBalanceInput4{5Symbol = "APP",6Owner = UserAddresses[0]7});8var withDrawResult = await userTokenHolderStub.Withdraw.SendAsync(ACS9DemoContractAddress);9withDrawResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined);10var resultBalance = await userTokenStub.GetBalance.CallAsync(new GetBalanceInput11{12Symbol = "APP",13Owner = UserAddresses[0]14});15resultBalance.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