ACS5 - 合约调用阈值标准

如果想提高合约的调用门槛,可以考虑实现ACS5。

接口

要限制对合约中某个方法的调用门槛,仅需要实现一个接口:

  • GetMethodCallingThreshold,参数为string,返回值为acs5.proto文件中定义的MethodCallingThreshold类型。

但是出于合约部署后通过交易来修改门槛,可以实现另一个接口:

  • SetMethodCallingThreshold,参数为SetMethodCallingThresholdInput。

MethodCallingThreshold类型的定义为:

message MethodCallingThreshold {
    map<string, int64> symbol_to_amount = 1;// The order matters.
    ThresholdCheckType threshold_check_type = 2;
}
enum ThresholdCheckType {
    BALANCE = 0;
    ALLOWANCE = 1;
}

枚举类型ThresholdCheckType存在的意义在于,合约方法调用的门槛有两类:

  1. 账户某种代币的余额充足即可调用,这种情况对应ThresholdCheckType.Balance;
  2. 不仅要求该账户某种代币的的余额充足,该账户对目标合约的授权额度也需要充足,这种情况对应ThresholdCheckType.Allowance。
  3. SetMethodCallingThresholdInput的定义为:
message SetMethodCallingThresholdInput {
    string method = 1;
    map<string, int64> symbol_to_amount = 2;// The order matters.
    ThresholdCheckType threshold_check_type = 3;
}

应用

类似于ACS1中使用一个自动生成的名为ChargeTransactionFees的pre-plugin交易来收取交易手续费,ACS5会自动生成一个名为CheckThreshold的pre-plugin交易来检验发送该交易的账户是否能够调用对应的方法。

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();
}

也就是说,如果交易发送者的某种代币余额没达到设定的额度,或对目标合约授权的额度没达到设定的额度,该pre-plugin交易会抛出异常,从而阻止原始交易的执行,以此达到设置调用门槛的效果。

接口实现

可以和ACS1的GetMethodFee一样,仅实现一个GetMethodCallingThreshold方法。

也可以通过在State文件中一个MappedState<string, MethodCallingThreshold>类型的属性来实现:

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

不过与此同时不能忘记配置一下SetMethodCallingThreshold的调用权限,这就需要在State中定义一个Admin(当然也可以使用ACS3来完成):

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

最简单的实现如下:

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();
}

测试

可以针对上面定义的Foo这个方法进行测试。

准备一个Stub:

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

在开始设置门槛之前,查一下现在门槛,是0:

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

虽然直接进行设置,需要Foo的调用者ELF余额大于1个ELF,才可以进行调用:

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

此时再查门槛:

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

让一个余额充足的账户去调用Foo,可以成功:

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

换一个没有ELF的账户去调用Foo,失败:

// 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.");
}