Skip to main content

ACS3 - Contract Proposal Standard

ACS3 is used when a method needs multiple approvals. Implement these methods for voting and approval:

Interface

Methods

Method NameRequest TypeResponse TypeDescription
CreateProposalacs3.CreateProposalInputaelf.HashCreates a proposal for voting and returns the proposal ID.
Approveaelf.Hashgoogle.protobuf.EmptyApproves a proposal by its ID.
Rejectaelf.Hashgoogle.protobuf.EmptyRejects a proposal by its ID.
Abstainaelf.Hashgoogle.protobuf.EmptyAbstains from voting on a proposal by its ID.
Releaseaelf.Hashgoogle.protobuf.EmptyReleases a proposal by its ID, triggering the specified contract call.
ChangeOrganizationThresholdacs3.ProposalReleaseThresholdgoogle.protobuf.EmptyChanges the proposal thresholds, affecting all current proposals.
ChangeOrganizationProposerWhiteListacs3.ProposerWhiteListgoogle.protobuf.EmptyChanges the proposer whitelist for the organization.
CreateProposalBySystemContractacs3.CreateProposalBySystemContractInputaelf.HashCreates a proposal by system contracts and returns the proposal ID.
ClearProposalaelf.Hashgoogle.protobuf.EmptyRemoves a specified proposal. If the proposal is active, removal fails.
GetProposalaelf.Hashacs3.ProposalOutputRetrieves a proposal by its ID.
ValidateOrganizationExistaelf.Addressgoogle.protobuf.BoolValueChecks if an organization exists.
ValidateProposerInWhiteListacs3.ValidateProposerInWhiteListInputgoogle.protobuf.BoolValueChecks if the proposer is in the whitelist.

Types

acs3.CreateProposalBySystemContractInput

FieldTypeDescriptionLabel
proposal_inputCreateProposalInputParameters for creating the proposal
origin_proposeraelf.AddressAddress of the proposer

acs3.CreateProposalInput

FieldTypeDescriptionLabel
contract_method_namestringMethod name to call after release
to_addressaelf.AddressContract address to call after release
paramsbytesParameters for the method call
expired_timegoogle.protobuf.TimestampProposal expiration time
organization_addressaelf.AddressOrganization address
proposal_description_urlstringURL for proposal description
tokenaelf.HashToken for proposal ID generation

acs3.OrganizationCreated

FieldTypeDescriptionLabel
organization_addressaelf.AddressCreated organization address

acs3.OrganizationHashAddressPair

FieldTypeDescriptionLabel
organization_hashaelf.HashOrganization ID
organization_addressaelf.AddressOrganization address

acs3.OrganizationThresholdChanged

FieldTypeDescriptionLabel
organization_addressaelf.AddressOrganization address
proposer_release_thresholdProposalReleaseThresholdNew release threshold

acs3.OrganizationWhiteListChanged

FieldTypeDescriptionLabel
organization_addressaelf.AddressOrganization address
proposer_white_listProposerWhiteListNew proposer whitelist

acs3.ProposalCreated

FieldTypeDescriptionLabel
proposal_idaelf.HashCreated proposal ID
organization_addressaelf.AddressOrganization address

acs3.ProposalOutput

FieldTypeDescriptionLabel
proposal_idaelf.HashProposal ID
contract_method_namestringMethod name for release
to_addressaelf.AddressTarget contract address
paramsbytesRelease transaction parameters
expired_timegoogle.protobuf.TimestampProposal expiration date
organization_addressaelf.AddressOrganization address
proposeraelf.AddressProposer address
to_be_releasedboolIndicates if releasable
approval_countint64Approval count
rejection_countint64Rejection count
abstention_countint64Abstention count

acs3.ProposalReleaseThreshold

FieldTypeDescriptionLabel
minimal_approval_thresholdint64Minimum approval threshold
maximal_rejection_thresholdint64Maximum rejection threshold
maximal_abstention_thresholdint64Maximum abstention threshold
minimal_vote_thresholdint64Minimum vote threshold

acs3.ProposalReleased

FieldTypeDescriptionLabel
proposal_idaelf.HashReleased proposal ID
organization_addressaelf.AddressOrganization address

acs3.ProposerWhiteList

FieldTypeDescriptionLabel
proposersaelf.AddressProposer addressesrepeated

acs3.ReceiptCreated

FieldTypeDescriptionLabel
proposal_idaelf.HashProposal ID
addressaelf.AddressSender address
receipt_typestringReceipt type (Approve, Reject, Abstain)
timegoogle.protobuf.TimestampTimestamp
organization_addressaelf.AddressOrganization address

acs3.ValidateProposerInWhiteListInput

FieldTypeDescriptionLabel
proposeraelf.AddressProposer address
organization_addressaelf.AddressOrganization address

Implementation

Assume there's only one organization in a contract, so no need to define the Organization type. Voters must use a token to vote. We'll focus on the core methods: CreateProposal, Approve, Reject, Abstain, and Release.

State Attributes

public MappedState<Hash, ProposalInfo> Proposals { get; set; }
public SingletonState<ProposalReleaseThreshold> ProposalReleaseThreshold { get; set; }
  • Proposals stores all proposal info.
  • ProposalReleaseThreshold saves the requirements to release a proposal.

Initialization

Set the proposal release requirements when the contract initializes:

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

Requires at least one vote and one approval.

Create Proposal

Creates a proposal and stores it with its details.

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

Voting Methods

Abstain

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

Approve

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

Reject

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

Charge

private void Charge()
{
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = Context.Variables.NativeSymbol,
Amount = 1_00000000
});
}

Release Proposal

Releases a proposal if the vote count meets the threshold:

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

Add methods to a Dapp contract and test the proposal with these methods.

State Class

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

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

Prepare a Stub

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

Approve Token Transaction

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

Create and Test Proposal

Create a proposal 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;

Check that Slogan is empty, vote, and release:

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