ACS1 - Transaction Fee Standard
ACS1 handles transaction fees.
Interface
Contracts using ACS1 must implement these methods:
Methods
Method Name | Request Type | Response Type | Description |
---|---|---|---|
SetMethodFee | acs1.MethodFees | google.protobuf.Empty | Sets the method fees for a method, overriding all fees. |
ChangeMethodFeeController | AuthorityInfo | google.protobuf.Empty | Changes the method fee controller. Default is parliament. |
GetMethodFee | google.protobuf.StringValue | acs1.MethodFees | Queries the fee for a method by name. |
GetMethodFeeController | google.protobuf.Empty | AuthorityInfo | Queries the method fee controller. |
Types
acs1.MethodFee
Field | Type | Description |
---|---|---|
symbol | string | The token symbol for the fee. |
basic_fee | int64 | The fee amount. |
acs1.MethodFees
Field | Type | Description |
---|---|---|
method_name | string | The name of the method. |
fees | MethodFee | List of fees. |
is_size_fee_free | bool | Optional based on implementation. |
AuthorityInfo
Field | Type | Description |
---|---|---|
contract_address | aelf.Address | The controller's contract address. |
owner_address | aelf.Address | The owner's address. |
Note: Only system contracts on the main chain can implement ACS1.
Usage
A pre-transaction, generated by FeeChargePreExecutionPlugin, charges the transaction fee before main processing.
/// <summary>
/// Related transactions will be generated by acs1 pre-plugin service,
/// and will be executed before the origin transaction.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override BoolValue ChargeTransactionFees(ChargeTransactionFeesInput input)
{
// ...
// Record tx fee bill during current charging process.
var bill = new TransactionFeeBill();
var fromAddress = Context.Sender;
var methodFees = Context.Call<MethodFees>(input.ContractAddress, nameof(GetMethodFee),
new StringValue {Value = input.MethodName});
var successToChargeBaseFee = true;
if (methodFees != null && methodFees.Fees.Any())
{
successToChargeBaseFee = ChargeBaseFee(GetBaseFeeDictionary(methodFees), ref bill);
}
var successToChargeSizeFee = true;
if (!IsMethodFeeSetToZero(methodFees))
{
// Then also do not charge size fee.
successToChargeSizeFee = ChargeSizeFee(input, ref bill);
}
// Update balances.
foreach (var tokenToAmount in bill.FeesMap)
{
ModifyBalance(fromAddress, tokenToAmount.Key, -tokenToAmount.Value);
Context.Fire(new TransactionFeeCharged
{
Symbol = tokenToAmount.Key,
Amount = tokenToAmount.Value
});
if (tokenToAmount.Value == 0)
{
//Context.LogDebug(() => $"Maybe incorrect charged tx fee of {tokenToAmount.Key}: it's 0.");
}
}
return new BoolValue {Value = successToChargeBaseFee && successToChargeSizeFee};
}
Steps:
- System calls
GetMethodFee
to determine the fee. - Checks if the balance is sufficient:
- If yes, the fee is billed.
- If no, the transaction is rejected.
- If the method fee is not zero, the system charges a size fee based on the parameter's size.
- After charging, an
TransactionFeeCharged
event is thrown, modifying the sender's balance. - The event is processed to calculate the total transaction fees in the block.
- In the next block:
- 10% of the fees are destroyed.
- 90% goes to the dividend pool on the main chain and to the FeeReceiver on the side chain.
/// <summary>
/// Burn 10% of tx fees.
/// If Side Chain didn't set FeeReceiver, burn all.
/// </summary>
/// <param name="symbol"></param>
/// <param name="totalAmount"></param>
private void TransferTransactionFeesToFeeReceiver(string symbol, long totalAmount)
{
Context.LogDebug(() => "Transfer transaction fee to receiver.");
if (totalAmount <= 0) return;
var burnAmount = totalAmount.Div(10);
if (burnAmount > 0)
Context.SendInline(Context.Self, nameof(Burn), new BurnInput
{
Symbol = symbol,
Amount = burnAmount
});
var transferAmount = totalAmount.Sub(burnAmount);
if (transferAmount == 0)
return;
var treasuryContractAddress =
Context.GetContractAddressByName(SmartContractConstants.TreasuryContractSystemName);
if ( treasuryContractAddress!= null)
{
// Main chain would donate tx fees to dividend pool.
if (State.DividendPoolContract.Value == null)
State.DividendPoolContract.Value = treasuryContractAddress;
State.DividendPoolContract.Donate.Send(new DonateInput
{
Symbol = symbol,
Amount = transferAmount
});
}
else
{
if (State.FeeReceiver.Value != null)
{
Context.SendInline(Context.Self, nameof(Transfer), new TransferInput
{
To = State.FeeReceiver.Value,
Symbol = symbol,
Amount = transferAmount,
});
}
else
{
// Burn all!
Context.SendInline(Context.Self, nameof(Burn), new BurnInput
{
Symbol = symbol,
Amount = transferAmount
});
}
}
}
Implementation
Simple Implementation
Implement only GetMethodFee
to set fixed fees for methods.
public override MethodFees GetMethodFee(StringValue input)
{
if (input.Value == nameof(Foo1) || input.Value == nameof(Foo2))
{
return new MethodFees
{
MethodName = input.Value,
Fees =
{
new MethodFee
{
BasicFee = 1_00000000,
Symbol = Context.Variables.NativeSymbol
}
}
};
}
if (input.Value == nameof(Bar1) || input.Value == nameof(Bar2))
{
return new MethodFees
{
MethodName = input.Value,
Fees =
{
new MethodFee
{
BasicFee = 2_00000000,
Symbol = Context.Variables.NativeSymbol
}
}
};
}
return new MethodFees();
}
Recommended Implementation
- Define a
MappedState
in the contract's State file for transaction fees.
public MappedState<string, MethodFees> TransactionFees { get; set; }
- Modify
TransactionFees
inSetMethodFee
and return the value inGetMethodFee
.
public override MethodFees GetMethodFee(StringValue input) {
return State.TransactionFees[input.Value];
}
- Add permission management to
SetMethodFee
to prevent arbitrary fee changes.
public SingletonState<AuthorityInfo> MethodFeeController { get; set; }
public override Empty SetMethodFee(MethodFees input)
{
foreach (var symbolToAmount in input.Fees)
{
AssertValidToken(symbolToAmount.Symbol, symbolToAmount.BasicFee);
}
RequiredMethodFeeControllerSet();
Assert(Context.Sender == State.MethodFeeController.Value.OwnerAddress, "Unauthorized to set method fee.");
State.TransactionFees[input.MethodName] = input;
return new Empty();
}
Permission Management
- Define a
SingletonState
with typeAuthorityInfo
.
private void RequiredMethodFeeControllerSet()
{
if (State.MethodFeeController.Value != null) return;
if (State.ParliamentContract.Value == null)
{
State.ParliamentContract.Value = Context.GetContractAddressByName(SmartContractConstants.ParliamentContractSystemName);
}
var defaultAuthority = new AuthorityInfo();
// Parliament Auth Contract maybe not deployed.
if (State.ParliamentContract.Value != null)
{
defaultAuthority.OwnerAddress = State.ParliamentContract.GetDefaultOrganizationAddress.Call(new Empty());
defaultAuthority.ContractAddress = State.ParliamentContract.Value;
}
State.MethodFeeController.Value = defaultAuthority;
}
- Check the sender’s right by comparing its address with the owner’s address.
- Implement permission checks to ensure only authorized changes.
Changing Authority
The authority for SetMethodFee
can be changed through a transaction from the default parliament address.
public override Empty ChangeMethodFeeController(AuthorityInfo input)
{
RequiredMethodFeeControllerSet();
AssertSenderAddressWith(State.MethodFeeController.Value.OwnerAddress);
var organizationExist = CheckOrganizationExist(input);
Assert(organizationExist, "Invalid authority input.");
State.MethodFeeController.Value = input;
return new Empty();
}
public override AuthorityInfo GetMethodFeeController(Empty input)
{
RequiredMethodFeeControllerSet();
return State.MethodFeeController.Value;
}
Testing
Create ACS1’s Stub and call GetMethodFee
and GetMethodFeeController
to check the return values.
Example
All aelf system contracts implement ACS1 and can be used as references.