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