-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #29 from AElfProject/feat/references_acs-introduction
References: ACS Introduction
- Loading branch information
Showing
13 changed files
with
2,946 additions
and
0 deletions.
There are no files selected for viewing
298 changes: 298 additions & 0 deletions
298
docs/Reference/ACS Introduction/ACS0 - Contract Deployment Standard.md
Large diffs are not rendered by default.
Oops, something went wrong.
270 changes: 270 additions & 0 deletions
270
docs/Reference/ACS Introduction/ACS1 - Transaction Fee Standard.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
# 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. | ||
```cs | ||
/// <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. | ||
```cs | ||
/// <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. | ||
```cs | ||
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 | ||
1. Define a `MappedState` in the contract's State file for transaction fees. | ||
```cs | ||
public MappedState<string, MethodFees> TransactionFees { get; set; } | ||
``` | ||
2. Modify `TransactionFees` in `SetMethodFee` and return the value in `GetMethodFee`. | ||
```cs | ||
public override MethodFees GetMethodFee(StringValue input) { | ||
return State.TransactionFees[input.Value]; | ||
} | ||
``` | ||
3. Add permission management to `SetMethodFee` to prevent arbitrary fee changes. | ||
```cs | ||
public SingletonState<AuthorityInfo> MethodFeeController { get; set; } | ||
``` | ||
|
||
```cs | ||
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`. | ||
```cs | ||
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; | ||
} | ||
``` | ||
2. Check the sender’s right by comparing its address with the owner’s address. | ||
3. 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. | ||
```cs | ||
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(); | ||
} | ||
``` | ||
```cs | ||
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. |
Oops, something went wrong.