logo

Transaction Fee

ACS1 - Transaction Fee Standard

ACS1 handles transaction fees.

Interface#

Contracts using ACS1 must implement these methods:

Methods#

Method NameRequest TypeResponse TypeDescription
SetMethodFeeacs1.MethodFeesgoogle.protobuf.EmptySets the method fees for a method, overriding all fees.
ChangeMethodFeeControllerAuthorityInfogoogle.protobuf.EmptyChanges the method fee controller. Default is parliament.
GetMethodFeegoogle.protobuf.StringValueacs1.MethodFeesQueries the fee for a method by name.
GetMethodFeeControllergoogle.protobuf.EmptyAuthorityInfoQueries the method fee controller.

Types#

acs1.MethodFee

FieldTypeDescription
symbolstringThe token symbol for the fee.
basic_feeint64The fee amount.

acs1.MethodFees

FieldTypeDescription
method_namestringThe name of the method.
feesMethodFeeList of fees.
is_size_fee_freeboolOptional based on implementation.

AuthorityInfo

FieldTypeDescription
contract_addressaelf.AddressThe controller's contract address.
owner_addressaelf.AddressThe 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.

1
/// <summary>
2
/// Related transactions will be generated by acs1 pre-plugin service,
3
/// and will be executed before the origin transaction.
4
/// </summary>
5
/// <param name="input"></param>
6
/// <returns></returns>
7
public override BoolValue ChargeTransactionFees(ChargeTransactionFeesInput input)
8
{
9
// ...
10
// Record tx fee bill during current charging process.
11
var bill = new TransactionFeeBill();
12
var fromAddress = Context.Sender;
13
var methodFees = Context.Call<MethodFees>(input.ContractAddress, nameof(GetMethodFee),
14
new StringValue {Value = input.MethodName});
15
var successToChargeBaseFee = true;
16
if (methodFees != null && methodFees.Fees.Any())
17
{
18
successToChargeBaseFee = ChargeBaseFee(GetBaseFeeDictionary(methodFees), ref bill);
19
}
20
var successToChargeSizeFee = true;
21
if (!IsMethodFeeSetToZero(methodFees))
22
{
23
// Then also do not charge size fee.
24
successToChargeSizeFee = ChargeSizeFee(input, ref bill);
25
}
26
// Update balances.
27
foreach (var tokenToAmount in bill.FeesMap)
28
{
29
ModifyBalance(fromAddress, tokenToAmount.Key, -tokenToAmount.Value);
30
Context.Fire(new TransactionFeeCharged
31
{
32
Symbol = tokenToAmount.Key,
33
Amount = tokenToAmount.Value
34
});
35
if (tokenToAmount.Value == 0)
36
{
37
//Context.LogDebug(() => $"Maybe incorrect charged tx fee of {tokenToAmount.Key}: it's 0.");
38
}
39
}
40
return new BoolValue {Value = successToChargeBaseFee && successToChargeSizeFee};
41
}

Steps:#

  • System calls GetMethodFee to determine the fee.
  • Checks if the balance is sufficient:
  • 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:
  • 1
    /// <summary>
    2
    /// Burn 10% of tx fees.
    3
    /// If Side Chain didn't set FeeReceiver, burn all.
    4
    /// </summary>
    5
    /// <param name="symbol"></param>
    6
    /// <param name="totalAmount"></param>
    7
    private void TransferTransactionFeesToFeeReceiver(string symbol, long totalAmount)
    8
    {
    9
    Context.LogDebug(() => "Transfer transaction fee to receiver.");
    10
    if (totalAmount <= 0) return;
    11
    var burnAmount = totalAmount.Div(10);
    12
    if (burnAmount > 0)
    13
    Context.SendInline(Context.Self, nameof(Burn), new BurnInput
    14
    {
    15
    Symbol = symbol,
    16
    Amount = burnAmount
    17
    });
    18
    var transferAmount = totalAmount.Sub(burnAmount);
    19
    if (transferAmount == 0)
    20
    return;
    21
    var treasuryContractAddress =
    22
    Context.GetContractAddressByName(SmartContractConstants.TreasuryContractSystemName);
    23
    if ( treasuryContractAddress!= null)
    24
    {
    25
    // Main chain would donate tx fees to dividend pool.
    26
    if (State.DividendPoolContract.Value == null)
    27
    State.DividendPoolContract.Value = treasuryContractAddress;
    28
    State.DividendPoolContract.Donate.Send(new DonateInput
    29
    {
    30
    Symbol = symbol,
    31
    Amount = transferAmount
    32
    });
    33
    }
    34
    else
    35
    {
    36
    if (State.FeeReceiver.Value != null)
    37
    {
    38
    Context.SendInline(Context.Self, nameof(Transfer), new TransferInput
    39
    {
    40
    To = State.FeeReceiver.Value,
    41
    Symbol = symbol,
    42
    Amount = transferAmount,
    43
    });
    44
    }
    45
    else
    46
    {
    47
    // Burn all!
    48
    Context.SendInline(Context.Self, nameof(Burn), new BurnInput
    49
    {
    50
    Symbol = symbol,
    51
    Amount = transferAmount
    52
    });
    53
    }
    54
    }
    55
    }

    Implementation#

    Simple Implementation#

    Implement only GetMethodFee to set fixed fees for methods.

    1
    public override MethodFees GetMethodFee(StringValue input)
    2
    {
    3
    if (input.Value == nameof(Foo1) || input.Value == nameof(Foo2))
    4
    {
    5
    return new MethodFees
    6
    {
    7
    MethodName = input.Value,
    8
    Fees =
    9
    {
    10
    new MethodFee
    11
    {
    12
    BasicFee = 1_00000000,
    13
    Symbol = Context.Variables.NativeSymbol
    14
    }
    15
    }
    16
    };
    17
    }
    18
    if (input.Value == nameof(Bar1) || input.Value == nameof(Bar2))
    19
    {
    20
    return new MethodFees
    21
    {
    22
    MethodName = input.Value,
    23
    Fees =
    24
    {
    25
    new MethodFee
    26
    {
    27
    BasicFee = 2_00000000,
    28
    Symbol = Context.Variables.NativeSymbol
    29
    }
    30
    }
    31
    };
    32
    }
    33
    return new MethodFees();
    34
    }
  • Define a MappedState in the contract's State file for transaction fees.
  • 1
    public MappedState<string, MethodFees> TransactionFees { get; set; }
  • Modify TransactionFees in SetMethodFee and return the value in GetMethodFee.
  • 1
    public override MethodFees GetMethodFee(StringValue input) {
    2
    return State.TransactionFees[input.Value];
    3
    }
  • Add permission management to SetMethodFee to prevent arbitrary fee changes.
  • 1
    public SingletonState<AuthorityInfo> MethodFeeController { get; set; }
    1
    public override Empty SetMethodFee(MethodFees input)
    2
    {
    3
    foreach (var symbolToAmount in input.Fees)
    4
    {
    5
    AssertValidToken(symbolToAmount.Symbol, symbolToAmount.BasicFee);
    6
    }
    7
    RequiredMethodFeeControllerSet();
    8
    Assert(Context.Sender == State.MethodFeeController.Value.OwnerAddress, "Unauthorized to set method fee.");
    9
    State.TransactionFees[input.MethodName] = input;
    10
    return new Empty();
    11
    }

    Permission Management#

  • Define a SingletonState with type AuthorityInfo.
  • 1
    private void RequiredMethodFeeControllerSet()
    2
    {
    3
    if (State.MethodFeeController.Value != null) return;
    4
    if (State.ParliamentContract.Value == null)
    5
    {
    6
    State.ParliamentContract.Value = Context.GetContractAddressByName(SmartContractConstants.ParliamentContractSystemName);
    7
    }
    8
    var defaultAuthority = new AuthorityInfo();
    9
    // Parliament Auth Contract maybe not deployed.
    10
    if (State.ParliamentContract.Value != null)
    11
    {
    12
    defaultAuthority.OwnerAddress = State.ParliamentContract.GetDefaultOrganizationAddress.Call(new Empty());
    13
    defaultAuthority.ContractAddress = State.ParliamentContract.Value;
    14
    }
    15
    State.MethodFeeController.Value = defaultAuthority;
    16
    }
  • 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.

    1
    public override Empty ChangeMethodFeeController(AuthorityInfo input)
    2
    {
    3
    RequiredMethodFeeControllerSet();
    4
    AssertSenderAddressWith(State.MethodFeeController.Value.OwnerAddress);
    5
    var organizationExist = CheckOrganizationExist(input);
    6
    Assert(organizationExist, "Invalid authority input.");
    7
    State.MethodFeeController.Value = input;
    8
    return new Empty();
    9
    }
    1
    public override AuthorityInfo GetMethodFeeController(Empty input)
    2
    {
    3
    RequiredMethodFeeControllerSet();
    4
    return State.MethodFeeController.Value;
    5
    }

    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.

    Edited on: 15 July 2024 04:24:19 GMT+0