logo

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 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#

1
public MappedState<Hash, ProposalInfo> Proposals { get; set; }
2
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:

    1
    public override Empty Initialize(Empty input)
    2
    {
    3
    State.TokenContract.Value =
    4
    Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
    5
    State.ProposalReleaseThreshold.Value = new ProposalReleaseThreshold
    6
    {
    7
    MinimalApprovalThreshold = 1,
    8
    MinimalVoteThreshold = 1
    9
    };
    10
    return new Empty();
    11
    }

    Requires at least one vote and one approval.

    Create Proposal#

    Creates a proposal and stores it with its details.

    1
    public override Hash CreateProposal(CreateProposalInput input)
    2
    {
    3
    var proposalId = Context.GenerateId(Context.Self, input.Token);
    4
    Assert(State.Proposals[proposalId] == null, "Proposal with same token already exists.");
    5
    State.Proposals[proposalId] = new ProposalInfo
    6
    {
    7
    ProposalId = proposalId,
    8
    Proposer = Context.Sender,
    9
    ContractMethodName = input.ContractMethodName,
    10
    Params = input.Params,
    11
    ExpiredTime = input.ExpiredTime,
    12
    ToAddress = input.ToAddress,
    13
    ProposalDescriptionUrl = input.ProposalDescriptionUrl
    14
    };
    15
    return proposalId;
    16
    }

    Voting Methods#

    Abstain

    1
    public override Empty Abstain(Hash input)
    2
    {
    3
    Charge();
    4
    var proposal = State.Proposals[input];
    5
    if (proposal == null)
    6
    {
    7
    throw new AssertionException("Proposal not found.");
    8
    }
    9
    proposal.Abstentions.Add(Context.Sender);
    10
    State.Proposals[input] = proposal;
    11
    return new Empty();
    12
    }

    Approve

    1
    public override Empty Approve(Hash input)
    2
    {
    3
    Charge();
    4
    var proposal = State.Proposals[input];
    5
    if (proposal == null)
    6
    {
    7
    throw new AssertionException("Proposal not found.");
    8
    }
    9
    proposal.Approvals.Add(Context.Sender);
    10
    State.Proposals[input] = proposal;
    11
    return new Empty();
    12
    }

    Reject

    1
    public override Empty Reject(Hash input)
    2
    {
    3
    Charge();
    4
    var proposal = State.Proposals[input];
    5
    if (proposal == null)
    6
    {
    7
    throw new AssertionException("Proposal not found.");
    8
    }
    9
    proposal.Rejections.Add(Context.Sender);
    10
    State.Proposals[input] = proposal;
    11
    return new Empty();
    12
    }

    Charge

    1
    private void Charge()
    2
    {
    3
    State.TokenContract.TransferFrom.Send(new TransferFromInput
    4
    {
    5
    From = Context.Sender,
    6
    To = Context.Self,
    7
    Symbol = Context.Variables.NativeSymbol,
    8
    Amount = 1_00000000
    9
    });
    10
    }

    Release Proposal#

    Releases a proposal if the vote count meets the threshold:

    1
    public override Empty Release(Hash input)
    2
    {
    3
    var proposal = State.Proposals[input];
    4
    if (proposal == null)
    5
    {
    6
    throw new AssertionException("Proposal not found.");
    7
    }
    8
    Assert(IsReleaseThresholdReached(proposal), "Didn't reach release threshold.");
    9
    Context.SendInline(proposal.ToAddress, proposal.ContractMethodName, proposal.Params);
    10
    return new Empty();
    11
    }
    12
    private bool IsReleaseThresholdReached(ProposalInfo proposal)
    13
    {
    14
    var isRejected = IsProposalRejected(proposal);
    15
    if (isRejected)
    16
    return false;
    17
    var isAbstained = IsProposalAbstained(proposal);
    18
    return !isAbstained && CheckEnoughVoteAndApprovals(proposal);
    19
    }
    20
    private bool IsProposalRejected(ProposalInfo proposal)
    21
    {
    22
    var rejectionMemberCount = proposal.Rejections.Count;
    23
    return rejectionMemberCount > State.ProposalReleaseThreshold.Value.MaximalRejectionThreshold;
    24
    }
    25
    private bool IsProposalAbstained(ProposalInfo proposal)
    26
    {
    27
    var abstentionMemberCount = proposal.Abstentions.Count;
    28
    return abstentionMemberCount > State.ProposalReleaseThreshold.Value.MaximalAbstentionThreshold;
    29
    }
    30
    private bool CheckEnoughVoteAndApprovals(ProposalInfo proposal)
    31
    {
    32
    var approvedMemberCount = proposal.Approvals.Count;
    33
    var isApprovalEnough =
    34
    approvedMemberCount >= State.ProposalReleaseThreshold.Value.MinimalApprovalThreshold;
    35
    if (!isApprovalEnough)
    36
    return false;
    37
    var isVoteThresholdReached =
    38
    proposal.Abstentions.Concat(proposal.Approvals).Concat(proposal.Rejections).Count() >=
    39
    State.ProposalReleaseThreshold.Value.MinimalVoteThreshold;
    40
    return isVoteThresholdReached;
    41
    }

    Test#

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

    State Class#

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

    Set/Get Methods

    1
    public override StringValue GetSlogan(Empty input)
    2
    {
    3
    return State.Slogan.Value == null ? new StringValue() : new StringValue {Value = State.Slogan.Value};
    4
    }
    5
    6
    public override Empty SetSlogan(StringValue input)
    7
    {
    8
    Assert(Context.Sender == State.Organization.Value, "No permission.");
    9
    State.Slogan.Value = input.Value;
    10
    return new Empty();
    11
    }

    Prepare a Stub

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

    Approve Token Transaction

    1
    var tokenContractStub =
    2
    GetTester<TokenContractContainer.TokenContractStub>(
    3
    GetAddress(TokenSmartContractAddressNameProvider.StringName), keyPair);
    4
    await tokenContractStub.Approve.SendAsync(new ApproveInput
    5
    {
    6
    Spender = DAppContractAddress,
    7
    Symbol = "ELF",
    8
    Amount = long.MaxValue
    9
    });

    Create and Test Proposal

    Create a proposal to change the Slogan to "aelf":

    1
    var proposalId = (await acs3DemoContractStub.CreateProposal.SendAsync(new CreateProposalInput
    2
    {
    3
    OrganizationAddress = OrganizationAddress
    4
    ContractMethodName = nameof(acs3DemoContractStub.SetSlogan),
    5
    ToAddress = DAppContractAddress,
    6
    ExpiredTime = TimestampHelper.GetUtcNow().AddHours(1),
    7
    Params = new StringValue {Value = "aelf"}.ToByteString(),
    8
    Token = HashHelper.ComputeFrom("aelf")
    9
    })).Output;

    Check that Slogan is empty, vote, and release:

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

    Edited on: 15 July 2024 04:52:50 GMT+0