ACS3 - Contract Proposal Standard

ACS3 is suitable for the case that a method needs to be approved by multiple parties. At this time, you can consider using some of the interfaces provided by ACS3.

Interface

If you want multiple addresses vote to get agreement to do something, you can implement the following methods defined in ACS3:

Methods

Method Name

Request Type

Response Type

Description

CreateProposal

acs3.CreateProposalInput

aelf.Hash

Create a proposal for which organization members can vote. When the proposal is released, a transaction will be sent to the specified contract. Return id of the newly created proposal.

Approve

aelf.Hash

google.protobuf.Empty

Approve a proposal according to the proposal ID.

Reject

aelf.Hash

google.protobuf.Empty

Reject a proposal according to the proposal ID.

Abstain

aelf.Hash

google.protobuf.Empty

Abstain a proposal according to the proposal ID.

Release

aelf.Hash

google.protobuf.Empty

Release a proposal according to the proposal ID and send a transaction to the specified contract.

ChangeOrganizationThreshold

acs3.ProposalReleaseThreshold

google.protobuf.Empty

Change the thresholds associated with proposals. All fields will be overwritten by the input value and this will affect all current proposals of the organization. Note: only the organization can execute this through a proposal.

ChangeOrganizationProposerWhiteList

acs3.ProposerWhiteList

google.protobuf.Empty

Change the white list of organization proposer. This method overrides the list of whitelisted proposers.

CreateProposalBySystemContract

acs3.CreateProposalBySystemContractInput

aelf.Hash

Create a proposal by system contracts, and return id of the newly created proposal.

ClearProposal

aelf.Hash

google.protobuf.Empty

Remove the specified proposal. If the proposal is in effect, the cleanup fails.

GetProposal

aelf.Hash

acs3.ProposalOutput

Get the proposal according to the proposal ID.

ValidateOrganizationExist

aelf.Address

google.protobuf.BoolValue

Check the existence of an organization.

ValidateProposerInWhiteList

acs3.ValidateProposerInWhiteListInput

google.protobuf.BoolValue

Check if the proposer is whitelisted.

Types

acs3.CreateProposalBySystemContractInput

Field

Type

Description

Label

proposal_input

CreateProposalInput

The parameters of creating proposal.

origin_proposer

aelf.Address

The actor that trigger the call.

acs3.CreateProposalInput

Field

Type

Description

Label

contract_method_name

string

The name of the method to call after release.

to_address

aelf.Address

The address of the contract to call after release.

params

bytes

The parameter of the method to be called after the release.

expired_time

google.protobuf.Timestamp

The timestamp at which this proposal will expire.

organization_address

aelf.Address

The address of the organization.

proposal_description_url

string

Url is used for proposal describing.

token

aelf.Hash

The token is for proposal id generation and with this token, proposal id can be calculated before proposing.

acs3.OrganizationCreated

Field

Type

Description

Label

organization_address

aelf.Address

The address of the created organization.

acs3.OrganizationHashAddressPair

Field

Type

Description

Label

organization_hash

aelf.Hash

The id of organization.

organization_address

aelf.Address

The address of organization.

acs3.OrganizationThresholdChanged

Field

Type

Description

Label

organization_address

aelf.Address

The organization address

proposer_release_threshold

ProposalReleaseThreshold

The new release threshold.

acs3.OrganizationWhiteListChanged

Field

Type

Description

Label

organization_address

aelf.Address

The organization address.

proposer_white_list

ProposerWhiteList

The new proposer whitelist.

acs3.ProposalCreated

Field

Type

Description

Label

proposal_id

aelf.Hash

The id of the created proposal.

organization_address

aelf.Address

The organization address of the created proposal.

acs3.ProposalOutput

Field

Type

Description

Label

proposal_id

aelf.Hash

The id of the proposal.

contract_method_name

string

The method that this proposal will call when being released.

to_address

aelf.Address

The address of the target contract.

params

bytes

The parameters of the release transaction.

expired_time

google.protobuf.Timestamp

The date at which this proposal will expire.

organization_address

aelf.Address

The address of this proposals organization.

proposer

aelf.Address

The address of the proposer of this proposal.

to_be_released

bool

Indicates if this proposal is releasable.

approval_count

int64

Approval count for this proposal.

rejection_count

int64

Rejection count for this proposal.

abstention_count

int64

Abstention count for this proposal.

acs3.ProposalReleaseThreshold

Field

Type

Description

Label

minimal_approval_threshold

int64

The value for the minimum approval threshold.

maximal_rejection_threshold

int64

The value for the maximal rejection threshold.

maximal_abstention_threshold

int64

The value for the maximal abstention threshold.

minimal_vote_threshold

int64

The value for the minimal vote threshold.

acs3.ProposalReleased

Field

Type

Description

Label

proposal_id

aelf.Hash

The id of the released proposal.

organization_address

aelf.Address

The organization address of the released proposal.

acs3.ProposerWhiteList

Field

Type

Description

Label

proposers

aelf.Address

The address of the proposers

repeated

acs3.ReceiptCreated

Field

Type

Description

Label

proposal_id

aelf.Hash

The id of the proposal.

address

aelf.Address

The sender address.

receipt_type

string

The type of receipt(Approve, Reject or Abstain).

time

google.protobuf.Timestamp

The timestamp of this method call.

organization_address

aelf.Address

The address of the organization.

acs3.ValidateProposerInWhiteListInput

Field

Type

Description

Label

proposer

aelf.Address

The address to search/check.

organization_address

aelf.Address

The address of the organization.

aelf.Address

Field

Type

Description

Label

value

bytes

aelf.BinaryMerkleTree

Field

Type

Description

Label

nodes

Hash

The leaf nodes.

repeated

root

Hash

The root node hash.

leaf_count

int32

The count of leaf node.

aelf.Hash

Field

Type

Description

Label

value

bytes

aelf.LogEvent

Field

Type

Description

Label

address

Address

The contract address.

name

string

The name of the log event.

indexed

bytes

The indexed data, used to calculate bloom.

repeated

non_indexed

bytes

The non indexed data.

aelf.MerklePath

Field

Type

Description

Label

merkle_path_nodes

MerklePathNode

The merkle path nodes.

repeated

aelf.MerklePathNode

Field

Type

Description

Label

hash

Hash

The node hash.

is_left_child_node

bool

Whether it is a left child node.

aelf.SInt32Value

Field

Type

Description

Label

value

sint32

aelf.SInt64Value

Field

Type

Description

Label

value

sint64

aelf.ScopedStatePath

Field

Type

Description

Label

address

Address

The scope address, which will be the contract address.

path

StatePath

The path of contract state.

aelf.SmartContractRegistration

Field

Type

Description

Label

category

sint32

The category of contract code(0: C#).

code

bytes

The byte array of the contract code.

code_hash

Hash

The hash of the contract code.

is_system_contract

bool

Whether it is a system contract.

version

int32

The version of the current contract.

aelf.StatePath

Field

Type

Description

Label

parts

string

The partial path of the state path.

repeated

aelf.Transaction

Field

Type

Description

Label

from

Address

The address of the sender of the transaction.

to

Address

The address of the contract when calling a contract.

ref_block_number

int64

The height of the referenced block hash.

ref_block_prefix

bytes

The first four bytes of the referenced block hash.

method_name

string

The name of a method in the smart contract at the To address.

params

bytes

The parameters to pass to the smart contract method.

signature

bytes

When signing a transaction it’s actually a subset of the fields: from/to and the target method as well as the parameter that were given. It also contains the reference block number and prefix.

aelf.TransactionExecutingStateSet

Field

Type

Description

Label

writes

TransactionExecutingStateSet.WritesEntry

The changed states.

repeated

reads

TransactionExecutingStateSet.ReadsEntry

The read states.

repeated

deletes

TransactionExecutingStateSet.DeletesEntry

The deleted states.

repeated

aelf.TransactionExecutingStateSet.DeletesEntry

Field

Type

Description

Label

key

string

value

bool

aelf.TransactionExecutingStateSet.ReadsEntry

Field

Type

Description

Label

key

string

value

bool

aelf.TransactionExecutingStateSet.WritesEntry

Field

Type

Description

Label

key

string

value

bytes

aelf.TransactionResult

Field

Type

Description

Label

transaction_id

Hash

The transaction id.

status

TransactionResultStatus

The transaction result status.

logs

LogEvent

The log events.

repeated

bloom

bytes

Bloom filter for transaction logs. A transaction log event can be defined in the contract and stored in the bloom filter after the transaction is executed. Through this filter, we can quickly search for and determine whether a log exists in the transaction result.

return_value

bytes

The return value of the transaction execution.

block_number

int64

The height of the block hat packages the transaction.

block_hash

Hash

The hash of the block hat packages the transaction.

error

string

Failed execution error message.

aelf.TransactionResultStatus

Name

Number

Description

NOT_EXISTED

0

The execution result of the transaction does not exist.

PENDING

1

The transaction is in the transaction pool waiting to be packaged.

FAILED

2

Transaction execution failed.

MINED

3

The transaction was successfully executed and successfully packaged into a block.

CONFLICT

4

When executed in parallel, there are conflicts with other transactions.

PENDING_VALIDATION

5

The transaction is waiting for validation.

NODE_VALIDATION_FAILED

6

Transaction validation failed.

Implementation

It is assumed here that there is only one organization in a contract, that is, there is no need to specifically define the Organization type. Since the organization is not explicitly declared and created, the organization’s proposal whitelist does not exist. The process here is that the voter must use a certain token to vote.

For simplicity, only the core methods CreateProposal, Approve, Reject, Abstain, and Release are implemented here.

There are only two necessary State attributes:

public MappedState<Hash, ProposalInfo> Proposals { get; set; }
public SingletonState<ProposalReleaseThreshold> ProposalReleaseThreshold { get; set; }

The Proposals stores all proposal’s information, and the ProposalReleaseThreshold is used to save the requirements that the contract needs to meet to release the proposal.

When the contract is initialized, the proposal release requirements should be set:

public override Empty Initialize(Empty input)
{
    State.TokenContract.Value =
        Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
    State.ProposalReleaseThreshold.Value = new ProposalReleaseThreshold
    {
        MinimalApprovalThreshold = 1,
        MinimalVoteThreshold = 1
    };
    return new Empty();
}

The requirement is at least one member who vote and at least one approval. Create proposal:

public override Hash CreateProposal(CreateProposalInput input)
{
    var proposalId = Context.GenerateId(Context.Self, input.Token);
    Assert(State.Proposals[proposalId] == null, "Proposal with same token already exists.");
    State.Proposals[proposalId] = new ProposalInfo
    {
        ProposalId = proposalId,
        Proposer = Context.Sender,
        ContractMethodName = input.ContractMethodName,
        Params = input.Params,
        ExpiredTime = input.ExpiredTime,
        ToAddress = input.ToAddress,
        ProposalDescriptionUrl = input.ProposalDescriptionUrl
    };
    return proposalId;
}

Vote:

public override Empty Abstain(Hash input)
{
    Charge();
    var proposal = State.Proposals[input];
    if (proposal == null)
    {
        throw new AssertionException("Proposal not found.");
    }
    proposal.Abstentions.Add(Context.Sender);
    State.Proposals[input] = proposal;
    return new Empty();
}
public override Empty Approve(Hash input)
{
    Charge();
    var proposal = State.Proposals[input];
    if (proposal == null)
    {
        throw new AssertionException("Proposal not found.");
    }
    proposal.Approvals.Add(Context.Sender);
    State.Proposals[input] = proposal;
    return new Empty();
}
public override Empty Reject(Hash input)
{
    Charge();
    var proposal = State.Proposals[input];
    if (proposal == null)
    {
        throw new AssertionException("Proposal not found.");
    }
    proposal.Rejections.Add(Context.Sender);
    State.Proposals[input] = proposal;
    return new Empty();
}
private void Charge()
{
    State.TokenContract.TransferFrom.Send(new TransferFromInput
    {
        From = Context.Sender,
        To = Context.Self,
        Symbol = Context.Variables.NativeSymbol,
        Amount = 1_00000000
    });
}

Release is just count the vote, here is a recommended implementation:

public override Empty Release(Hash input)
{
    var proposal = State.Proposals[input];
    if (proposal == null)
    {
        throw new AssertionException("Proposal not found.");
    }
    Assert(IsReleaseThresholdReached(proposal), "Didn't reach release threshold.");
    Context.SendInline(proposal.ToAddress, proposal.ContractMethodName, proposal.Params);
    return new Empty();
}
private bool IsReleaseThresholdReached(ProposalInfo proposal)
{
    var isRejected = IsProposalRejected(proposal);
    if (isRejected)
        return false;
    var isAbstained = IsProposalAbstained(proposal);
    return !isAbstained && CheckEnoughVoteAndApprovals(proposal);
}
private bool IsProposalRejected(ProposalInfo proposal)
{
    var rejectionMemberCount = proposal.Rejections.Count;
    return rejectionMemberCount > State.ProposalReleaseThreshold.Value.MaximalRejectionThreshold;
}
private bool IsProposalAbstained(ProposalInfo proposal)
{
    var abstentionMemberCount = proposal.Abstentions.Count;
    return abstentionMemberCount > State.ProposalReleaseThreshold.Value.MaximalAbstentionThreshold;
}
private bool CheckEnoughVoteAndApprovals(ProposalInfo proposal)
{
    var approvedMemberCount = proposal.Approvals.Count;
    var isApprovalEnough =
        approvedMemberCount >= State.ProposalReleaseThreshold.Value.MinimalApprovalThreshold;
    if (!isApprovalEnough)
        return false;
    var isVoteThresholdReached =
        proposal.Abstentions.Concat(proposal.Approvals).Concat(proposal.Rejections).Count() >=
        State.ProposalReleaseThreshold.Value.MinimalVoteThreshold;
    return isVoteThresholdReached;
}

Test

Before testing, two methods were added to a Dapp contract. We will test the proposal with these methods.

Define a singleton string and an organization address state in the State class:

public StringState Slogan { get; set; }
public SingletonState<Address> Organization { get; set; }

A pair of Set/Get methods:

public override StringValue GetSlogan(Empty input)
{
    return State.Slogan.Value == null ? new StringValue() : new StringValue {Value = State.Slogan.Value};
}

public override Empty SetSlogan(StringValue input)
{
    Assert(Context.Sender == State.Organization.Value, "No permission.");
    State.Slogan.Value = input.Value;
    return new Empty();
}

In this way, during the test, create a proposal for the SetSlogan. After passing and releasing, use the GetSlogan method to check whether the Slogan has been modified.

Prepare a Stub that implements the ACS3 contract:

var keyPair = SampleECKeyPairs.KeyPairs[0];
var acs3DemoContractStub =
    GetTester<ACS3DemoContractContainer.ACS3DemoContractStub>(DAppContractAddress, keyPair);

Since approval requires the contract to charge users, the user should send Approve transaction of the Token contract.

var tokenContractStub =
    GetTester<TokenContractContainer.TokenContractStub>(
        GetAddress(TokenSmartContractAddressNameProvider.StringName), keyPair);
await tokenContractStub.Approve.SendAsync(new ApproveInput
{
    Spender = DAppContractAddress,
    Symbol = "ELF",
    Amount = long.MaxValue
});

Create a proposal, the target method is SetSlogan, here we want to change the Slogan to “AElf” :

var proposalId = (await acs3DemoContractStub.CreateProposal.SendAsync(new CreateProposalInput
{
    OrganizationAddress = OrganizationAddress
    ContractMethodName = nameof(acs3DemoContractStub.SetSlogan),
    ToAddress = DAppContractAddress,
    ExpiredTime = TimestampHelper.GetUtcNow().AddHours(1),
    Params = new StringValue {Value = "AElf"}.ToByteString(),
    Token = HashHelper.ComputeFrom("AElf")
})).Output;

Make sure that the Slogan is still an empty string at this time and then vote:

// Check slogan
{
    var slogan = await acs3DemoContractStub.GetSlogan.CallAsync(new Empty());
    slogan.Value.ShouldBeEmpty();
}
await acs3DemoContractStub.Approve.SendAsync(proposalId);

Release proposal, and the Slogan becomes “AElf”.

await acs3DemoContractStub.Release.SendAsync(proposalId);
// Check slogan
{
    var slogan = await acs3DemoContractStub.GetSlogan.CallAsync(new Empty());
    slogan.Value.ShouldBe("AElf");
}