ACS10 - 分红池标准

ACS10用于在当前合约中构造一个分红池。

接口

为了构造一个分红池,可以可选地实现以下接口:

  • Donate,用于捐赠分红,参数包括要为分红池捐赠的代币symbol和额度;
  • Release,用于释放分红,参数为释放分红的届数,要注意设置其调用权限.
  • SetSymbolList,用于设置分红池所支持的释放分红的代币symbol,参数为SymbolList类型;
  • GetSymbolList,用于获取分红池所支持的释放分红的代币symbol,返回值为SymbolList类型;
  • GetUndistributedDividends,用于获取尚未被分红的所有分红池所支持的代币余额,返回值为Dividends类型;
  • GetDividends,用于获取某个区块高度中新增的分红的额度,返回值也是Dividends类型。

其中SymbolList类型是string列表:

message SymbolList {
    repeated string value = 1;
}

Dividends类型是key为代币symbol、value为数额的map:

message Dividends {
    map<string, int64> value = 1;
}

应用

ACS10仅仅是统一了分红池的标准接口,这些接口不会跟AElf链上做交互。

接口实现

借助Profit合约

可以在初始化该合约的时候,使用Profit合约的CreateScheme方法创建一个分红方案(Profit Scheme):

State.ProfitContract.Value =
    Context.GetContractAddressByName(SmartContractConstants.ProfitContractSystemName);
var schemeToken = HashHelper.ComputeFrom(Context.Self);
State.ProfitContract.CreateScheme.Send(new CreateSchemeInput
{
    Manager = Context.Self,
    CanRemoveBeneficiaryDirectly = true,
    IsReleaseAllBalanceEveryTimeByDefault = true,
    Token = schemeToken
});
State.ProfitSchemeId.Value = Context.GenerateId(State.ProfitContract.Value, schemeToken);

其中,schemeToken会被Profit合约用来计算该合约创建的分红方案的Id,Context.GenerateId方法是AElf智能合约通用的用于产生Id的方法,我们使用Profit合约的地址和提供给Profit合约的schemeToken即可自行计算出该分红方案的Id,设置到State.ProfitSchemeId上(这是一个SingletonState<Hash>类型的属性)。

创建完分红方案后:

  • 可使用Profit合约的ContributeProfits方法来实现ACS10接口中的Donate方法;
  • 可使用Profit合约的DistributeProfits方法来实现ACS10接口中的Release方法;
  • 可使用AddBeneficiary、RemoveBeneficiary等方法来自行管理分红的领取人及权重;
  • 可使用AddSubScheme、RemoveSubScheme等方法自行管理分红关联的子分红方案及权重;
  • SetSymbolList和GetSymbolList中的SymbolList类型可以自行记录,在Donate和Release时发挥对应的作用即可;
  • GetUndistributedDividends方法返回查询到的对应的分红方案总账上SymbolList中包含代币的余额即可。

借助Token Holder合约

在初始化该合约时,使用TokenHolder合约的CreateScheme方法创建一个持币人分红方案(Token Holder Profit Scheme):

State.TokenHolderContract.Value =
    Context.GetContractAddressByName(SmartContractConstants.TokenHolderContractSystemName);
State.TokenHolderContract.CreateScheme.Send(new CreateTokenHolderProfitSchemeInput
{
    Symbol = Context.Variables.NativeSymbol,
    MinimumLockMinutes = input.MinimumLockMinutes
});
return new Empty();

在持币人分红方案中,一个分红方案的实例与其创建者绑定,因此无需计算SchemeId(虽然其底层还是通过在Profit合约中创建分红方案实现的,但是这个关系被记录在TokenHolder合约中)。

考虑GetDividends的作用为记录每个区块高度该分红池的收益增长,因此需要在Donate的时候记录下来。Donate可以实现为:

public override Empty Donate(DonateInput input)
{
    State.TokenContract.TransferFrom.Send(new TransferFromInput
    {
        From = Context.Sender,
        Symbol = input.Symbol,
        Amount = input.Amount,
        To = Context.Self
    });
    State.TokenContract.Approve.Send(new ApproveInput
    {
        Symbol = input.Symbol,
        Amount = input.Amount,
        Spender = State.TokenHolderContract.Value
    });
    State.TokenHolderContract.ContributeProfits.Send(new ContributeProfitsInput
    {
        SchemeManager = Context.Self,
        Symbol = input.Symbol,
        Amount = input.Amount
    });
    Context.Fire(new DonationReceived
    {
        From = Context.Sender,
        Symbol = input.Symbol,
        Amount = input.Amount,
        PoolContract = Context.Self
    });
    var currentReceivedDividends = State.ReceivedDividends[Context.CurrentHeight];
    if (currentReceivedDividends != null && currentReceivedDividends.Value.ContainsKey(input.Symbol))
    {
        currentReceivedDividends.Value[input.Symbol] =
            currentReceivedDividends.Value[input.Symbol].Add(input.Amount);
    }
    else
    {
        currentReceivedDividends = new Dividends
        {
            Value =
            {
                {
                    input.Symbol, input.Amount
                }
            }
        };
    }
    State.ReceivedDividends[Context.CurrentHeight] = currentReceivedDividends;
    Context.LogDebug(() => string.Format("Contributed {0} {1}s to side chain dividends pool.", input.Amount, input.Symbol));
    return new Empty();
}

对应的Release直接调用TokenHolder的DistributeProfits方法即可:

public override Empty Release(ReleaseInput input)
{
    State.TokenHolderContract.DistributeProfits.Send(new DistributeProfitsInput
    {
        SchemeManager = Context.Self
    });
    return new Empty();
}

在持币人分红合约中,默认实现为收到什么代币的捐赠,就释放什么代币,因此SetSymbolList无需实现,GetSymbolList直接读取分红方案中记录的收到捐赠的代币种类即可:

public override Empty SetSymbolList(SymbolList input)
{
    Assert(false, "Not support setting symbol list.");
    return new Empty();
}
public override SymbolList GetSymbolList(Empty input)
{
    return new SymbolList
    {
        Value =
        {
            GetDividendPoolScheme().ReceivedTokenSymbols
        }
    };
}
private Scheme GetDividendPoolScheme()
{
    if (State.DividendPoolSchemeId.Value == null)
    {
        var tokenHolderScheme = State.TokenHolderContract.GetScheme.Call(Context.Self);
        State.DividendPoolSchemeId.Value = tokenHolderScheme.SchemeId;
    }
    return Context.Call<Scheme>(
        Context.GetContractAddressByName(SmartContractConstants.ProfitContractSystemName),
        nameof(ProfitContractContainer.ProfitContractReferenceState.GetScheme),
        State.DividendPoolSchemeId.Value);
}

GetUndistributedDividends的实现同上一节所述,查询余额返回即可:

public override Dividends GetUndistributedDividends(Empty input)
{
    var scheme = GetDividendPoolScheme();
    return new Dividends
    {
        Value =
        {
            scheme.ReceivedTokenSymbols.Select(s => State.TokenContract.GetBalance.Call(new GetBalanceInput
            {
                Owner = scheme.VirtualAddress,
                Symbol = s
            })).ToDictionary(b => b.Symbol, b => b.Balance)
        }
    };
}

除了借助Profit合约和TokenHolder合约,当然也可以自己实现一个分红池,满足接口的语义即可。

测试

以借助Token Holder合约实现的分红池为例,分两条线进行测试。

一是针对该分红池进行Donate、Release和一系列查询操作;

二是使用一个账户进行锁仓,然后取出分红。

定义好需要用到的Stub:

const long amount = 10_00000000;
var keyPair = SampleECKeyPairs.KeyPairs[0];
var address = Address.FromPublicKey(keyPair.PublicKey);
var acs10DemoContractStub =
    GetTester<ACS10DemoContractContainer.ACS10DemoContractStub>(DAppContractAddress, keyPair);
var tokenContractStub =
    GetTester<TokenContractContainer.TokenContractStub>(TokenContractAddress, keyPair);
var tokenHolderContractStub =
    GetTester<TokenHolderContractContainer.TokenHolderContractStub>(TokenHolderContractAddress,
        keyPair);

在进行操作之前,先对TokenHolder合约和实现了分红池的合约进行一下额度授权。

await tokenContractStub.Approve.SendAsync(new ApproveInput
{
    Spender = TokenHolderContractAddress,
    Symbol = "ELF",
    Amount = long.MaxValue
});
await tokenContractStub.Approve.SendAsync(new ApproveInput
{
    Spender = DAppContractAddress,
    Symbol = "ELF",
    Amount = long.MaxValue
});

锁仓,此时账户余额减少10ELF:

await tokenHolderContractStub.RegisterForProfits.SendAsync(new RegisterForProfitsInput
{
    SchemeManager = DAppContractAddress,
    Amount = amount
});

捐赠分红,此时账户余额再次减少10ELF:

await acs10DemoContractStub.Donate.SendAsync(new DonateInput
{
    Symbol = "ELF",
    Amount = amount
});

此时就可以测试GetUndistributedDividends和GetDividends接口了:

// Check undistributed dividends before releasing.
{
    var undistributedDividends =
        await acs10DemoContractStub.GetUndistributedDividends.CallAsync(new Empty());
    undistributedDividends.Value["ELF"].ShouldBe(amount);
}
var blockchainService = Application.ServiceProvider.GetRequiredService<IBlockchainService>();
var currentBlockHeight = (await blockchainService.GetChainAsync()).BestChainHeight;
var dividends =
    await acs10DemoContractStub.GetDividends.CallAsync(new Int64Value {Value = currentBlockHeight});
dividends.Value["ELF"].ShouldBe(amount);

释放分红,顺便再次测试GetUndistributedDividends接口是否做出来反应:

await acs10DemoContractStub.Release.SendAsync(new ReleaseInput
{
    PeriodNumber = 1
});
// Check undistributed dividends after releasing.
{
    var undistributedDividends =
        await acs10DemoContractStub.GetUndistributedDividends.CallAsync(new Empty());
    undistributedDividends.Value["ELF"].ShouldBe(0);
}

最后让这个账户领取分红,然后观察其账户余额变动:

var balanceBeforeClaimForProfits = await tokenContractStub.GetBalance.CallAsync(new GetBalanceInput
{
    Owner = address,
    Symbol = "ELF"
});
await tokenHolderContractStub.ClaimProfits.SendAsync(new ClaimProfitsInput
{
    SchemeManager = DAppContractAddress,
    Beneficiary = address
});
var balanceAfterClaimForProfits = await tokenContractStub.GetBalance.CallAsync(new GetBalanceInput
{
    Owner = address,
    Symbol = "ELF"
});
balanceAfterClaimForProfits.Balance.ShouldBe(balanceBeforeClaimForProfits.Balance + amount);

示例

当前主链分红池和侧链分红池都是通过实现ACS10来构建的。

Treasury合约对ACS10的实现为主链分红池。

AEDPoS合约对ACS10的实现为侧链分红池。