Skip to main content

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.

/// <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:

  1. System calls GetMethodFee to determine the fee.
  2. Checks if the balance is sufficient:
    • If yes, the fee is billed.
    • If no, the transaction is rejected.
  3. If the method fee is not zero, the system charges a size fee based on the parameter's size.
  4. After charging, an TransactionFeeCharged event is thrown, modifying the sender's balance.
  5. The event is processed to calculate the total transaction fees in the block.
  6. 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();
}
  1. Define a MappedState in the contract's State file for transaction fees.
public MappedState<string, MethodFees> TransactionFees { get; set; }
  1. Modify TransactionFees in SetMethodFee and return the value in GetMethodFee.
public override MethodFees GetMethodFee(StringValue input) {
return State.TransactionFees[input.Value];
}
  1. 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

  1. Define a SingletonState with type AuthorityInfo.
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;
}
  1. Check the sender’s right by comparing its address with the owner’s address.
  2. 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.