logo

Contract Threshold

ACS5 - Contract Threshold Standard#

To raise the threshold for using a contract, consider implementing ACS5.

Interface#

To limit calling a method in a contract, implement these interfaces:

Methods#

Method NameRequest TypeResponse TypeDescription
SetMethodCallingThresholdacs5.SetMethodCallingThresholdInputgoogle.protobuf.EmptySet the threshold for method calling.
GetMethodCallingThresholdgoogle.protobuf.StringValueacs5.MethodCallingThresholdGet the threshold for method calling.

Types#

acs5.MethodCallingThreshold

FieldTypeDescriptionLabel
symbol_to_amountMethodCallingThreshold.SymbolToAmountEntryThe threshold for method calling, token symbol -> amount.repeated
threshold_check_typeThresholdCheckTypeThe type of threshold check.

acs5.MethodCallingThreshold.SymbolToAmountEntry

FieldTypeDescriptionLabel
keystring
valueint64

acs5.SetMethodCallingThresholdInput

FieldTypeDescriptionLabel
methodstringThe method name to check.
symbol_to_amountSetMethodCallingThresholdInput.SymbolToAmountEntryThe threshold for method calling, token symbol -> amount.repeated
threshold_check_typeThresholdCheckTypeThe type of threshold check.

acs5.SetMethodCallingThresholdInput.SymbolToAmountEntry

FieldTypeDescriptionLabel
keystring
valueint64

acs5.ThresholdCheckType

NameNumberDescription
BALANCECheck balance only.
ALLOWANCE1Check balance and allowance at the same time.

Usage#

ACS5 works similarly to ACS1, which uses a pre-plugin transaction called ChargeTransactionFees to charge a transaction fee. ACS5 uses a pre-plugin transaction called CheckThreshold to ensure the account sending the transaction can invoke the method.

Implementation of CheckThreshold:#

1
public override Empty CheckThreshold(CheckThresholdInput input)
2
{
3
var meetThreshold = false;
4
var meetBalanceSymbolList = new List<string>();
5
foreach (var symbolToThreshold in input.SymbolToThreshold)
6
{
7
if (GetBalance(input.Sender, symbolToThreshold.Key) < symbolToThreshold.Value)
8
continue;
9
meetBalanceSymbolList.Add(symbolToThreshold.Key);
10
}
11
if (meetBalanceSymbolList.Count > 0)
12
{
13
if (input.IsCheckAllowance)
14
{
15
foreach (var symbol in meetBalanceSymbolList)
16
{
17
if (State.Allowances[input.Sender][Context.Sender][symbol] <
18
input.SymbolToThreshold[symbol]) continue;
19
meetThreshold = true;
20
break;
21
}
22
}
23
else
24
{
25
meetThreshold = true;
26
}
27
}
28
if (input.SymbolToThreshold.Count == 0)
29
{
30
meetThreshold = true;
31
}
32
Assert(meetThreshold, "Cannot meet the calling threshold.");
33
return new Empty();
34
}

If the sender's token balance or the authorized amount for the target contract doesn't meet the set limit, the pre-plugin transaction throws an exception and prevents the original transaction from executing.

Implementation#

Implement a single GetMethodCallingThreshold method like GetMethodFee in ACS1. Use MappedState<string, MethodCallingThreshold> in the State class:

1
public MappedState<string, MethodCallingThreshold> MethodCallingThresholds { get; set; }

Configure the call permission of SetMethodCallingThreshold, requiring an Admin in the State:

1
public SingletonState<Address> Admin { get; set; }
1
public override Empty SetMethodCallingThreshold(SetMethodCallingThresholdInput input)
2
{
3
Assert(State.Admin.Value == Context.Sender, "No permission.");
4
State.MethodCallingThresholds[input.Method] = new MethodCallingThreshold
5
{
6
SymbolToAmount = {input.SymbolToAmount}
7
};
8
return new Empty();
9
}
10
11
public override MethodCallingThreshold GetMethodCallingThreshold(StringValue input)
12
{
13
return State.MethodCallingThresholds[input.Value];
14
}
15
16
public override Empty Foo(Empty input)
17
{
18
return new Empty();
19
}
20
21
message SetMethodCallingThresholdInput {
22
string method = 1;
23
map<string, int64> symbol_to_amount = 2;// The order matters.
24
ThresholdCheckType threshold_check_type = 3;
25
}

Test#

Test the Foo method:

  • Make a Stub
  • 1
    var keyPair = SampleECKeyPairs.KeyPairs[0];
    2
    var acs5DemoContractStub =
    3
    GetTester<ACS5DemoContractContainer.ACS5DemoContractStub>(DAppContractAddress, keyPair);
  • Check the current threshold (should be 0):
  • 1
    var methodResult = await acs5DemoContractStub.GetMethodCallingThreshold.CallAsync(
    2
    new StringValue
    3
    {
    4
    Value = nameof(acs5DemoContractStub.Foo)
    5
    });
    6
    methodResult.SymbolToAmount.Count.ShouldBe(0);
  • Ensure the caller's ELF balance is greater than 1 ELF:
  • 1
    await acs5DemoContractStub.SetMethodCallingThreshold.SendAsync(
    2
    new SetMethodCallingThresholdInput
    3
    {
    4
    Method = nameof(acs5DemoContractStub.Foo),
    5
    SymbolToAmount =
    6
    {
    7
    {"ELF", 1_0000_0000}
    8
    },
    9
    ThresholdCheckType = ThresholdCheckType.Balance
    10
    });
  • Check the threshold again:
  • 1
    methodResult = await acs5DemoContractStub.GetMethodCallingThreshold.CallAsync(
    2
    new StringValue
    3
    {
    4
    Value = nameof(acs5DemoContractStub.Foo)
    5
    });
    6
    methodResult.SymbolToAmount.Count.ShouldBe(1);
    7
    methodResult.ThresholdCheckType.ShouldBe(ThresholdCheckType.Balance);
  • Send the Foo transaction with an account that has enough balance:
  • 1
    // Call with enough balance.
    2
    {
    3
    var executionResult = await acs5DemoContractStub.Foo.SendAsync(new Empty());
    4
    executionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined);
    5
    }
  • Send the Foo transaction with an account without ELF:
  • 1
    // Call without enough balance.
    2
    {
    3
    var poorStub =
    4
    GetTester<ACS5DemoContractContainer.ACS5DemoContractStub>(DAppContractAddress,
    5
    SampleECKeyPairs.KeyPairs[1]);
    6
    var executionResult = await poorStub.Foo.SendWithExceptionAsync(new Empty());
    7
    executionResult.TransactionResult.Error.ShouldContain("Cannot meet the calling threshold.");
    8
    }

    Edited on: 15 July 2024 05:16:22 GMT+0