Skip to main content

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.

  1. Implementation of Initialize
public override Empty Initialize(InitializeInput input)
{
State.TokenHolderContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName);
State.TokenContract.Value =
Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
State.DividendPoolContract.Value =
Context.GetContractAddressByName(input.DividendPoolContractName.Value.ToBase64());
State.Symbol.Value = input.Symbol == string.Empty ? "APP" : input.Symbol;
State.ProfitReceiver.Value = input.ProfitReceiver;
CreateToken(input.ProfitReceiver);
// To test TokenHolder Contract.
CreateTokenHolderProfitScheme();
// To test ACS9 workflow.
SetProfitConfig();
State.ProfitReceiver.Value = input.ProfitReceiver;
return new Empty();
}
private void CreateToken(Address profitReceiver, bool isLockWhiteListIncludingSelf = false)
{
var lockWhiteList = new List<Address>
{Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName)};
if (isLockWhiteListIncludingSelf)
lockWhiteList.Add(Context.Self);
State.TokenContract.Create.Send(new CreateInput
{
Symbol = State.Symbol.Value,
TokenName = "DApp Token",
Decimals = ACS9DemoContractConstants.Decimal,
Issuer = Context.Self,
IsBurnable = true,
IsProfitable = true,
TotalSupply = ACS9DemoContractConstants.TotalSupply,
LockWhiteList =
{
lockWhiteList
}
});
State.TokenContract.Issue.Send(new IssueInput
{
To = profitReceiver,
Amount = ACS9DemoContractConstants.TotalSupply / 5,
Symbol = State.Symbol.Value,
Memo = "Issue token for profit receiver"
});
}
private void CreateTokenHolderProfitScheme()
{
State.TokenHolderContract.CreateScheme.Send(new CreateTokenHolderProfitSchemeInput
{
Symbol = State.Symbol.Value
});
}
private void SetProfitConfig()
{
State.ProfitConfig.Value = new ProfitConfig
{
DonationPartsPerHundred = 1,
StakingTokenSymbol = "APP",
ProfitsTokenSymbolList = {"ELF"}
};
}
  1. The user can use the SighUp method to register and get the bonus
/// <summary>
/// When user sign up, give him 10 APP tokens, then initialize his profile.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override Empty SignUp(Empty input)
{
Assert(State.Profiles[Context.Sender] == null, "Already registered.");
var profile = new Profile
{
UserAddress = Context.Sender
};
State.TokenContract.Issue.Send(new IssueInput
{
Symbol = State.Symbol.Value,
Amount = ACS9DemoContractConstants.ForNewUser,
To = Context.Sender
});
// Update profile.
profile.Records.Add(new Record
{
Type = RecordType.SignUp,
Timestamp = Context.CurrentBlockTime,
Description = string.Format("{0} +{1}",State.Symbol.Value, ACS9DemoContractConstants.ForNewUser)
});
State.Profiles[Context.Sender] = profile;
return new Empty();
}
  1. Recharge and redemption:
public override Empty Deposit(DepositInput input)
{
// User Address -> DApp Contract.
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = "ELF",
Amount = input.Amount
});
State.TokenContract.Issue.Send(new IssueInput
{
Symbol = State.Symbol.Value,
Amount = input.Amount,
To = Context.Sender
});
// Update profile.
var profile = State.Profiles[Context.Sender];
profile.Records.Add(new Record
{
Type = RecordType.Deposit,
Timestamp = Context.CurrentBlockTime,
Description = string.Format("{0} +{1}", State.Symbol.Value, input.Amount)
});
State.Profiles[Context.Sender] = profile;
return new Empty();
}
public override Empty Withdraw(WithdrawInput input)
{
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = State.Symbol.Value,
Amount = input.Amount
});
State.TokenContract.Transfer.Send(new TransferInput
{
To = Context.Sender,
Symbol = input.Symbol,
Amount = input.Amount
});
State.TokenHolderContract.RemoveBeneficiary.Send(new RemoveTokenHolderBeneficiaryInput
{
Beneficiary = Context.Sender,
Amount = input.Amount
});
// Update profile.
var profile = State.Profiles[Context.Sender];
profile.Records.Add(new Record
{
Type = RecordType.Withdraw,
Timestamp = Context.CurrentBlockTime,
Description = string.Format("{0} -{1}", State.Symbol.Value, input.Amount)
});
State.Profiles[Context.Sender] = profile;
return new Empty();
}
  1. Implementation of Use directly transfers 1/3 profits into the token holder dividend scheme:
public override Empty Use(Record input)
{
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = State.Symbol.Value,
Amount = ACS9DemoContractConstants.UseFee
});
if (input.Symbol == string.Empty)
input.Symbol = State.TokenContract.GetPrimaryTokenSymbol.Call(new Empty()).Value;
var contributeAmount = ACS9DemoContractConstants.UseFee.Div(3);
State.TokenContract.Approve.Send(new ApproveInput
{
Spender = State.TokenHolderContract.Value,
Symbol = input.Symbol,
Amount = contributeAmount
});
// Contribute 1/3 profits (ELF) to profit scheme.
State.TokenHolderContract.ContributeProfits.Send(new ContributeProfitsInput
{
SchemeManager = Context.Self,
Amount = contributeAmount,
Symbol = input.Symbol
});
// Update profile.
var profile = State.Profiles[Context.Sender];
profile.Records.Add(new Record
{
Type = RecordType.Withdraw,
Timestamp = Context.CurrentBlockTime,
Description = string.Format("{0} -{1}", State.Symbol.Value, ACS9DemoContractConstants.UseFee),
Symbol = input.Symbol
});
State.Profiles[Context.Sender] = profile;
return new Empty();
}
  1. Implement ACS9 for the perfect profit distribution:
public override Empty TakeContractProfits(TakeContractProfitsInput input)
{
var config = State.ProfitConfig.Value;
// For Side Chain Dividends Pool.
var amountForSideChainDividendsPool = input.Amount.Mul(config.DonationPartsPerHundred).Div(100);
State.TokenContract.Approve.Send(new ApproveInput
{
Symbol = input.Symbol,
Amount = amountForSideChainDividendsPool,
Spender = State.DividendPoolContract.Value
});
State.DividendPoolContract.Donate.Send(new DonateInput
{
Symbol = input.Symbol,
Amount = amountForSideChainDividendsPool
});
// For receiver.
var amountForReceiver = input.Amount.Sub(amountForSideChainDividendsPool);
State.TokenContract.Transfer.Send(new TransferInput
{
To = State.ProfitReceiver.Value,
Amount = amountForReceiver,
Symbol = input.Symbol
});
// For Token Holder Profit Scheme. (To distribute.)
State.TokenHolderContract.DistributeProfits.Send(new DistributeProfitsInput
{
SchemeManager = Context.Self
});
return new Empty();
}
public override ProfitConfig GetProfitConfig(Empty input)
{
return State.ProfitConfig.Value;
}
public override ProfitsMap GetProfitsAmount(Empty input)
{
var profitsMap = new ProfitsMap();
foreach (var symbol in State.ProfitConfig.Value.ProfitsTokenSymbolList)
{
var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = Context.Self,
Symbol = symbol
}).Balance;
profitsMap.Value[symbol] = balance;
}
return profitsMap;
}

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

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