Skip to content

Commit

Permalink
Merge pull request #29 from AElfProject/feat/references_acs-introduction
Browse files Browse the repository at this point in the history
References: ACS Introduction
  • Loading branch information
AelfHongliang authored Jun 24, 2024
2 parents 421b656 + 8e1805a commit 88781e5
Show file tree
Hide file tree
Showing 13 changed files with 2,946 additions and 0 deletions.
298 changes: 298 additions & 0 deletions docs/Reference/ACS Introduction/ACS0 - Contract Deployment Standard.md

Large diffs are not rendered by default.

270 changes: 270 additions & 0 deletions docs/Reference/ACS Introduction/ACS1 - Transaction Fee Standard.md
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.
Loading

0 comments on commit 88781e5

Please sign in to comment.