Contract Proposal
ACS3 - Contract Proposal Standard#
ACS3 is used when a method needs multiple approvals. Implement these methods for voting and approval:
Interface#
Methods#
Method Name | Request Type | Response Type | Description |
---|---|---|---|
CreateProposal | acs3.CreateProposalInput | aelf.Hash | Creates a proposal for voting and returns the proposal ID. |
Approve | aelf.Hash | google.protobuf.Empty | Approves a proposal by its ID. |
Reject | aelf.Hash | google.protobuf.Empty | Rejects a proposal by its ID. |
Abstain | aelf.Hash | google.protobuf.Empty | Abstains from voting on a proposal by its ID. |
Release | aelf.Hash | google.protobuf.Empty | Releases a proposal by its ID, triggering the specified contract call. |
ChangeOrganizationThreshold | acs3.ProposalReleaseThreshold | google.protobuf.Empty | Changes the proposal thresholds, affecting all current proposals. |
ChangeOrganizationProposerWhiteList | acs3.ProposerWhiteList | google.protobuf.Empty | Changes the proposer whitelist for the organization. |
CreateProposalBySystemContract | acs3.CreateProposalBySystemContractInput | aelf.Hash | Creates a proposal by system contracts and returns the proposal ID. |
ClearProposal | aelf.Hash | google.protobuf.Empty | Removes a specified proposal. If the proposal is active, removal fails. |
GetProposal | aelf.Hash | acs3.ProposalOutput | Retrieves a proposal by its ID. |
ValidateOrganizationExist | aelf.Address | google.protobuf.BoolValue | Checks if an organization exists. |
ValidateProposerInWhiteList | acs3.ValidateProposerInWhiteListInput | google.protobuf.BoolValue | Checks if the proposer is in the whitelist. |
Types#
acs3.CreateProposalBySystemContractInput
Field | Type | Description | Label |
---|---|---|---|
proposal_input | CreateProposalInput | Parameters for creating the proposal | |
origin_proposer | aelf.Address | Address of the proposer |
acs3.CreateProposalInput
Field | Type | Description | Label |
---|---|---|---|
contract_method_name | string | Method name to call after release | |
to_address | aelf.Address | Contract address to call after release | |
params | bytes | Parameters for the method call | |
expired_time | google.protobuf.Timestamp | Proposal expiration time | |
organization_address | aelf.Address | Organization address | |
proposal_description_url | string | URL for proposal description | |
token | aelf.Hash | Token for proposal ID generation |
acs3.OrganizationCreated
Field | Type | Description | Label |
---|---|---|---|
organization_address | aelf.Address | Created organization address |
acs3.OrganizationHashAddressPair
Field | Type | Description | Label |
---|---|---|---|
organization_hash | aelf.Hash | Organization ID | |
organization_address | aelf.Address | Organization address |
acs3.OrganizationThresholdChanged
Field | Type | Description | Label |
---|---|---|---|
organization_address | aelf.Address | Organization address | |
proposer_release_threshold | ProposalReleaseThreshold | New release threshold |
acs3.OrganizationWhiteListChanged
Field | Type | Description | Label |
---|---|---|---|
organization_address | aelf.Address | Organization address | |
proposer_white_list | ProposerWhiteList | New proposer whitelist |
acs3.ProposalCreated
Field | Type | Description | Label |
---|---|---|---|
proposal_id | aelf.Hash | Created proposal ID | |
organization_address | aelf.Address | Organization address |
acs3.ProposalOutput
Field | Type | Description | Label |
---|---|---|---|
proposal_id | aelf.Hash | Proposal ID | |
contract_method_name | string | Method name for release | |
to_address | aelf.Address | Target contract address | |
params | bytes | Release transaction parameters | |
expired_time | google.protobuf.Timestamp | Proposal expiration date | |
organization_address | aelf.Address | Organization address | |
proposer | aelf.Address | Proposer address | |
to_be_released | bool | Indicates if releasable | |
approval_count | int64 | Approval count | |
rejection_count | int64 | Rejection count | |
abstention_count | int64 | Abstention count |
acs3.ProposalReleaseThreshold
Field | Type | Description | Label |
---|---|---|---|
minimal_approval_threshold | int64 | Minimum approval threshold | |
maximal_rejection_threshold | int64 | Maximum rejection threshold | |
maximal_abstention_threshold | int64 | Maximum abstention threshold | |
minimal_vote_threshold | int64 | Minimum vote threshold |
acs3.ProposalReleased
Field | Type | Description | Label |
---|---|---|---|
proposal_id | aelf.Hash | Released proposal ID | |
organization_address | aelf.Address | Organization address |
acs3.ProposerWhiteList
Field | Type | Description | Label |
---|---|---|---|
proposers | aelf.Address | Proposer addresses | repeated |
acs3.ReceiptCreated
Field | Type | Description | Label |
---|---|---|---|
proposal_id | aelf.Hash | Proposal ID | |
address | aelf.Address | Sender address | |
receipt_type | string | Receipt type (Approve, Reject, Abstain) | |
time | google.protobuf.Timestamp | Timestamp | |
organization_address | aelf.Address | Organization address |
acs3.ValidateProposerInWhiteListInput
Field | Type | Description | Label |
---|---|---|---|
proposer | aelf.Address | Proposer address | |
organization_address | aelf.Address | Organization 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#
1public MappedState<Hash, ProposalInfo> Proposals { get; set; }2public SingletonState<ProposalReleaseThreshold> ProposalReleaseThreshold { get; set; }
Initialization#
Set the proposal release requirements when the contract initializes:
1public override Empty Initialize(Empty input)2{3State.TokenContract.Value =4Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);5State.ProposalReleaseThreshold.Value = new ProposalReleaseThreshold6{7MinimalApprovalThreshold = 1,8MinimalVoteThreshold = 19};10return new Empty();11}
Requires at least one vote and one approval.
Create Proposal#
Creates a proposal and stores it with its details.
1public override Hash CreateProposal(CreateProposalInput input)2{3var proposalId = Context.GenerateId(Context.Self, input.Token);4Assert(State.Proposals[proposalId] == null, "Proposal with same token already exists.");5State.Proposals[proposalId] = new ProposalInfo6{7ProposalId = proposalId,8Proposer = Context.Sender,9ContractMethodName = input.ContractMethodName,10Params = input.Params,11ExpiredTime = input.ExpiredTime,12ToAddress = input.ToAddress,13ProposalDescriptionUrl = input.ProposalDescriptionUrl14};15return proposalId;16}
Voting Methods#
Abstain
1public override Empty Abstain(Hash input)2{3Charge();4var proposal = State.Proposals[input];5if (proposal == null)6{7throw new AssertionException("Proposal not found.");8}9proposal.Abstentions.Add(Context.Sender);10State.Proposals[input] = proposal;11return new Empty();12}
Approve
1public override Empty Approve(Hash input)2{3Charge();4var proposal = State.Proposals[input];5if (proposal == null)6{7throw new AssertionException("Proposal not found.");8}9proposal.Approvals.Add(Context.Sender);10State.Proposals[input] = proposal;11return new Empty();12}
Reject
1public override Empty Reject(Hash input)2{3Charge();4var proposal = State.Proposals[input];5if (proposal == null)6{7throw new AssertionException("Proposal not found.");8}9proposal.Rejections.Add(Context.Sender);10State.Proposals[input] = proposal;11return new Empty();12}
Charge
1private void Charge()2{3State.TokenContract.TransferFrom.Send(new TransferFromInput4{5From = Context.Sender,6To = Context.Self,7Symbol = Context.Variables.NativeSymbol,8Amount = 1_000000009});10}
Release Proposal#
Releases a proposal if the vote count meets the threshold:
1public override Empty Release(Hash input)2{3var proposal = State.Proposals[input];4if (proposal == null)5{6throw new AssertionException("Proposal not found.");7}8Assert(IsReleaseThresholdReached(proposal), "Didn't reach release threshold.");9Context.SendInline(proposal.ToAddress, proposal.ContractMethodName, proposal.Params);10return new Empty();11}12private bool IsReleaseThresholdReached(ProposalInfo proposal)13{14var isRejected = IsProposalRejected(proposal);15if (isRejected)16return false;17var isAbstained = IsProposalAbstained(proposal);18return !isAbstained && CheckEnoughVoteAndApprovals(proposal);19}20private bool IsProposalRejected(ProposalInfo proposal)21{22var rejectionMemberCount = proposal.Rejections.Count;23return rejectionMemberCount > State.ProposalReleaseThreshold.Value.MaximalRejectionThreshold;24}25private bool IsProposalAbstained(ProposalInfo proposal)26{27var abstentionMemberCount = proposal.Abstentions.Count;28return abstentionMemberCount > State.ProposalReleaseThreshold.Value.MaximalAbstentionThreshold;29}30private bool CheckEnoughVoteAndApprovals(ProposalInfo proposal)31{32var approvedMemberCount = proposal.Approvals.Count;33var isApprovalEnough =34approvedMemberCount >= State.ProposalReleaseThreshold.Value.MinimalApprovalThreshold;35if (!isApprovalEnough)36return false;37var isVoteThresholdReached =38proposal.Abstentions.Concat(proposal.Approvals).Concat(proposal.Rejections).Count() >=39State.ProposalReleaseThreshold.Value.MinimalVoteThreshold;40return isVoteThresholdReached;41}
Test#
Add methods to a Dapp contract and test the proposal with these methods.
State Class#
1public StringState Slogan { get; set; }2public SingletonState<Address> Organization { get; set; }
Set/Get Methods
1public override StringValue GetSlogan(Empty input)2{3return State.Slogan.Value == null ? new StringValue() : new StringValue {Value = State.Slogan.Value};4}56public override Empty SetSlogan(StringValue input)7{8Assert(Context.Sender == State.Organization.Value, "No permission.");9State.Slogan.Value = input.Value;10return new Empty();11}
Prepare a Stub
1var keyPair = SampleECKeyPairs.KeyPairs[0];2var acs3DemoContractStub =3GetTester<ACS3DemoContractContainer.ACS3DemoContractStub>(DAppContractAddress, keyPair);
Approve Token Transaction
1var tokenContractStub =2GetTester<TokenContractContainer.TokenContractStub>(3GetAddress(TokenSmartContractAddressNameProvider.StringName), keyPair);4await tokenContractStub.Approve.SendAsync(new ApproveInput5{6Spender = DAppContractAddress,7Symbol = "ELF",8Amount = long.MaxValue9});
Create and Test Proposal
Create a proposal to change the Slogan to "aelf":
1var proposalId = (await acs3DemoContractStub.CreateProposal.SendAsync(new CreateProposalInput2{3OrganizationAddress = OrganizationAddress4ContractMethodName = nameof(acs3DemoContractStub.SetSlogan),5ToAddress = DAppContractAddress,6ExpiredTime = TimestampHelper.GetUtcNow().AddHours(1),7Params = new StringValue {Value = "aelf"}.ToByteString(),8Token = HashHelper.ComputeFrom("aelf")9})).Output;
Check that Slogan is empty, vote, and release:
1// Check slogan2{3var slogan = await acs3DemoContractStub.GetSlogan.CallAsync(new Empty());4slogan.Value.ShouldBeEmpty();5}6await acs3DemoContractStub.Approve.SendAsync(proposalId);
1await acs3DemoContractStub.Release.SendAsync(proposalId);2// Check slogan3{4var slogan = await acs3DemoContractStub.GetSlogan.CallAsync(new Empty());5slogan.Value.ShouldBe("aelf");6}
Edited on: 15 July 2024 04:52:50 GMT+0