ACS5 - Contract Threshold Standard

If you want to raise the threshold for using contract, consider implementing ACS5.

Interface

To limit to call a method in a contract, you only need to implement the following five interfaces:

Methods

Method Name

Request Type

Response Type

Description

SetMethodCallingThreshold

acs5.SetMethodCallingThresholdInput

google.protobuf.Empty

Set the threshold for method calling.

GetMethodCallingThreshold

google.protobuf.StringValue

acs5.MethodCallingThreshold

Get the threshold for method calling.

Types

acs5.MethodCallingThreshold

Field

Type

Description

Label

symbol_to_amount

MethodCallingThreshold.SymbolToAmountEntry

The threshold for method calling, token symbol -> amount.

repeated

threshold_check_type

ThresholdCheckType

The type of threshold check.

acs5.MethodCallingThreshold.SymbolToAmountEntry

Field

Type

Description

Label

key

string

value

int64

acs5.SetMethodCallingThresholdInput

Field

Type

Description

Label

method

string

The method name to check.

symbol_to_amount

SetMethodCallingThresholdInput.SymbolToAmountEntry

The threshold for method calling, token symbol -> amount.

repeated

threshold_check_type

ThresholdCheckType

The type of threshold check.

acs5.SetMethodCallingThresholdInput.SymbolToAmountEntry

Field

Type

Description

Label

key

string

value

int64

acs5.ThresholdCheckType

Name

Number

Description

BALANCE

0

Check balance only.

ALLOWANCE

1

Check balance and allowance at the same time.

Usage

Similar to ACS1, which uses an automatically generated pre-plugin transaction called ChargeTransactionFees to charge a transaction fee, ACS5 automatically generates a pre-plugin transaction called CheckThreshold to test whether the account that sent the transaction can invoke the corresponding method.

The implementation of CheckThreshold:

public override Empty CheckThreshold(CheckThresholdInput input)
{
    var meetThreshold = false;
    var meetBalanceSymbolList = new List<string>();
    foreach (var symbolToThreshold in input.SymbolToThreshold)
    {
        if (GetBalance(input.Sender, symbolToThreshold.Key) < symbolToThreshold.Value)
            continue;
        meetBalanceSymbolList.Add(symbolToThreshold.Key);
    }
    if (meetBalanceSymbolList.Count > 0)
    {
        if (input.IsCheckAllowance)
        {
            foreach (var symbol in meetBalanceSymbolList)
            {
                if (State.Allowances[input.Sender][Context.Sender][symbol] <
                    input.SymbolToThreshold[symbol]) continue;
                meetThreshold = true;
                break;
            }
        }
        else
        {
            meetThreshold = true;
        }
    }
    if (input.SymbolToThreshold.Count == 0)
    {
        meetThreshold = true;
    }
    Assert(meetThreshold, "Cannot meet the calling threshold.");
    return new Empty();
}

In other words, if the token balance of the sender of the transaction or the amount authorized for the target contract does not reach the set limit, the pre-plugin transaction will throw an exception, thereby it prevents the original transaction from executing.

Implementation

Just lik the GetMethodFee of ACS1, you can implement only one GetMethodCallingThreshold method.

It can also be achieved by using MappedState<string, MethodCallingThreshold> in the State class:

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

But at the same time, do not forget to configure the call permission of SetMethodCallingThreshold, which requires the definition of an Admin in the State (of course, you can also use ACS3):

public SingletonState<Address> Admin { get; set; }

The easiest implementation:

public override Empty SetMethodCallingThreshold(SetMethodCallingThresholdInput input)
{
    Assert(State.Admin.Value == Context.Sender, "No permission.");
    State.MethodCallingThresholds[input.Method] = new MethodCallingThreshold
    {
        SymbolToAmount = {input.SymbolToAmount}
    };
    return new Empty();
}

public override MethodCallingThreshold GetMethodCallingThreshold(StringValue input)
{
    return State.MethodCallingThresholds[input.Value];
}

public override Empty Foo(Empty input)
{
    return new Empty();
}

message SetMethodCallingThresholdInput {
    string method = 1;
    map<string, int64> symbol_to_amount = 2;// The order matters.
    ThresholdCheckType threshold_check_type = 3;
}

Test

You can test the Foo method defined above.

Make a Stub:

var keyPair = SampleECKeyPairs.KeyPairs[0];
var acs5DemoContractStub =
    GetTester<ACS5DemoContractContainer.ACS5DemoContractStub>(DAppContractAddress, keyPair);

Before setting the threshold, check the current threshold, which should be 0:

var methodResult = await acs5DemoContractStub.GetMethodCallingThreshold.CallAsync(
    new StringValue
    {
        Value = nameof(acs5DemoContractStub.Foo)
    });
methodResult.SymbolToAmount.Count.ShouldBe(0);

The ELF balance of the caller of Foo should be greater than 1 ELF:

await acs5DemoContractStub.SetMethodCallingThreshold.SendAsync(
    new SetMethodCallingThresholdInput
    {
        Method = nameof(acs5DemoContractStub.Foo),
        SymbolToAmount =
        {
            {"ELF", 1_0000_0000}
        },
        ThresholdCheckType = ThresholdCheckType.Balance
    });

Check the threshold again:

methodResult = await acs5DemoContractStub.GetMethodCallingThreshold.CallAsync(
    new StringValue
    {
        Value = nameof(acs5DemoContractStub.Foo)
    });
methodResult.SymbolToAmount.Count.ShouldBe(1);
methodResult.ThresholdCheckType.ShouldBe(ThresholdCheckType.Balance);

Send the Foo transaction via an account who has sufficient balance can succeed:

// Call with enough balance.
{
    var executionResult = await acs5DemoContractStub.Foo.SendAsync(new Empty());
    executionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined);
}

Send the Foo transaction via another account without ELF fails:

// Call without enough balance.
{
    var poorStub =
        GetTester<ACS5DemoContractContainer.ACS5DemoContractStub>(DAppContractAddress,
            SampleECKeyPairs.KeyPairs[1]);
    var executionResult = await poorStub.Foo.SendWithExceptionAsync(new Empty());
    executionResult.TransactionResult.Error.ShouldContain("Cannot meet the calling threshold.");
}