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