Skip to content

Commit

Permalink
feat!: add a claimsRegistrar based on attestations (#3622)
Browse files Browse the repository at this point in the history
* feat!: use contractGovernor to govern Treasury using ParamManager

extract params to a separate file
integrate contract governance into treasury
swingset test for treasury governance

closes #3189
closes #3473

* chore: minor cleanups: drop extra logs, standardize asserts, tsc fix

* chore: improve typescript declarations

* feat: add noAction electorate for assurance of no governance changes

* chore: validate() in test checks the installations

* fix: import types.js into params so bundle is usable in tests

* feat!: add a claimsRegistrar based on attestations

Agents who can deposit an attestation payment will get the ability to vote that amount.
refactor common registrat code to a library
tests that BinaryBallotCounter can count these votes.

* chore: review sugestions: types, cleanups, comments

* chore: better type decls, capitalize handles

more handles have capitalized names than not.

Suggestions from #3932

* fix: remove spurious distinction in naming of Liquidity keyword

* fix: handle<'attestation'> ==>  Handle<'Attestation'>

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
Chris-Hibbert and mergify[bot] authored Oct 8, 2021
1 parent c19f141 commit 3acf78d
Show file tree
Hide file tree
Showing 19 changed files with 636 additions and 102 deletions.
7 changes: 3 additions & 4 deletions packages/governance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,9 @@ contract makes no attempt to make the voters legible to others. This might be
useful for a private group making a decision, or a case where a dictator has the
ability to appoint a committee that will make decisions.

The AttestedElectorate (coming soon!) is an Electorate that gives the ability to
vote to anyone who has an Attestation payment from the Attestation contract.
Observers can't tell who the voters are, but they can validate the
qualifications to vote.
ShareHolders is an Electorate that gives the ability to vote to anyone who has
an Attestation payment from the Attestation contract. Observers can't tell who
the voters are, but they can validate the qualifications to vote.

Another plausible electorate would use the result of a public vote to give
voting facets to the election winners. There would have to be some kind of
Expand Down
4 changes: 4 additions & 0 deletions packages/governance/src/binaryVoteCounter.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ const makeBinaryVoteCounter = (questionSpec, threshold, instance) => {

let isOpen = true;
const positions = questionSpec.positions;
/** @type { PromiseRecord<Position> } */
const outcomePromise = makePromiseKit();
/** @type { PromiseRecord<VoteStatistics> } */
const tallyPromise = makePromiseKit();
// The Electorate is responsible for creating a unique seat for each voter.
// This voteCounter allows voters to re-vote, and replaces their previous
Expand Down Expand Up @@ -110,6 +112,7 @@ const makeBinaryVoteCounter = (questionSpec, threshold, instance) => {
}
});

/** @type { VoteStatistics } */
const stats = {
spoiled,
votes: allBallots.entries().length,
Expand Down Expand Up @@ -169,6 +172,7 @@ const makeBinaryVoteCounter = (questionSpec, threshold, instance) => {
// It schedules the closing of the vote, finally inserting the contract
// instance in the publicFacet before returning public and creator facets.

/** @param { ContractFacet } zcf */
const start = zcf => {
// There are a variety of ways of counting quorums. The parameters must be
// visible in the terms. We're doing a simple threshold here. If we wanted to
Expand Down
67 changes: 18 additions & 49 deletions packages/governance/src/committee.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
import { E } from '@agoric/eventual-send';
import { Far } from '@agoric/marshal';
import { makeSubscriptionKit } from '@agoric/notifier';
import { allComparable } from '@agoric/same-structure';
import { makeStore } from '@agoric/store';
import { natSafeMath } from '@agoric/zoe/src/contractSupport/index.js';

import { makeHandle } from '@agoric/zoe/src/makeHandle';
import { QuorumRule } from './question.js';
import {
startCounter,
getOpenQuestions,
getQuestion,
getPoserInvitation,
} from './electorateTools.js';

const { ceilDivide } = natSafeMath;

Expand All @@ -24,29 +29,10 @@ const { ceilDivide } = natSafeMath;
* @type {ContractStartFn}
*/
const start = zcf => {
/**
* @typedef {Object} QuestionRecord
* @property {ERef<VoteCounterCreatorFacet>} voteCap
* @property {VoteCounterPublicFacet} publicFacet
*/

/** @type {Store<Handle<'Question'>, QuestionRecord>} */
const allQuestions = makeStore('Question');
const { subscription, publication } = makeSubscriptionKit();

const getOpenQuestions = async () => {
const isOpenPQuestions = allQuestions.keys().map(key => {
const { publicFacet } = allQuestions.get(key);
return [E(publicFacet).isOpen(), key];
});

/** @type {[boolean, Handle<'Question'>][]} */
const isOpenQuestions = await allComparable(harden(isOpenPQuestions));
return isOpenQuestions
.filter(([open, _key]) => open)
.map(([_open, key]) => key);
};

const makeCommitteeVoterInvitation = index => {
/** @type {OfferHandler} */
const offerHandler = Far('voter offerHandler', () => {
Expand Down Expand Up @@ -92,45 +78,28 @@ const start = zcf => {
}
};

/** @type {QuestionTerms} */
const voteCounterTerms = {
return startCounter(
zcf,
questionSpec,
electorate: zcf.getInstance(),
quorumThreshold: quorumThreshold(questionSpec.quorumRule),
};

// facets of the vote counter. creatorInvitation and adminFacet not used
const { creatorFacet, publicFacet, instance } = await E(
zcf.getZoeService(),
).startInstance(voteCounter, {}, voteCounterTerms);
const details = await E(publicFacet).getDetails();
const voteCounterFacets = { voteCap: creatorFacet, publicFacet };
allQuestions.init(details.questionHandle, voteCounterFacets);

publication.updateState(details);
return { creatorFacet, publicFacet, instance };
quorumThreshold(questionSpec.quorumRule),
voteCounter,
allQuestions,
publication,
);
};

/** @type {ElectoratePublic} */
/** @type {CommitteeElectoratePublic} */
const publicFacet = Far('publicFacet', {
getQuestionSubscription: () => subscription,
getOpenQuestions,
getOpenQuestions: () => getOpenQuestions(allQuestions),
getName: () => committeeName,
getInstance: zcf.getInstance,
getQuestion: questionHandleP =>
E.when(questionHandleP, questionHandle =>
E(allQuestions.get(questionHandle).publicFacet).getQuestion(),
),
getQuestion: handleP => getQuestion(handleP, allQuestions),
});

const getPoserInvitation = () => {
const questionPoserHandler = () => Far(`questionPoser`, { addQuestion });
return zcf.makeInvitation(questionPoserHandler, `questionPoser`);
};

/** @type {ElectorateCreatorFacet} */
/** @type {CommitteeElectorateCreatorFacet} */
const creatorFacet = Far('adminFacet', {
getPoserInvitation,
getPoserInvitation: () => getPoserInvitation(zcf, addQuestion),
addQuestion,
getVoterInvitations: () => invitations,
getQuestionSubscription: () => subscription,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { E } from '@agoric/eventual-send';
import { allComparable } from '@agoric/same-structure';
import { Far } from '@agoric/marshal';

/**
* Start up a new Counter for a question
*
* @type {StartCounter}
*/
const startCounter = async (
zcf,
questionSpec,
Expand All @@ -12,16 +17,17 @@ const startCounter = async (
questionStore,
publication,
) => {
const ballotCounterTerms = {
const voteCounterTerms = {
questionSpec,
electorate: zcf.getInstance(),
quorumThreshold,
};

// facets of the voteCounter. creatorInvitation and adminFacet not used
/** @type {{ creatorFacet: VoteCounterCreatorFacet, publicFacet: VoteCounterPublicFacet, instance: Instance }} */
const { creatorFacet, publicFacet, instance } = await E(
zcf.getZoeService(),
).startInstance(voteCounter, {}, ballotCounterTerms);
).startInstance(voteCounter, {}, voteCounterTerms);
const details = await E(publicFacet).getDetails();
const { deadline } = questionSpec.closingRule;
publication.updateState(details);
Expand All @@ -34,23 +40,33 @@ const startCounter = async (
return { creatorFacet, publicFacet, instance, deadline, questionHandle };
};

/** @param {Store<Handle<'Question'>, QuestionRecord>} questionStore */
const getOpenQuestions = async questionStore => {
const isOpenPQuestions = questionStore.keys().map(key => {
const { publicFacet } = questionStore.get(key);
return [E(publicFacet).isOpen(), key];
});
const isOpenPQuestions = questionStore
.entries()
.map(([key, { publicFacet }]) => {
return [E(publicFacet).isOpen(), key];
});

const isOpenQuestions = await allComparable(harden(isOpenPQuestions));
return isOpenQuestions
.filter(([open, _key]) => open)
.map(([_open, key]) => key);
};

/**
* @param {ERef<Handle<'Question'>>} questionHandleP
* @param {Store<Handle<'Question'>, QuestionRecord>} questionStore
*/
const getQuestion = (questionHandleP, questionStore) =>
E.when(questionHandleP, questionHandle =>
E(questionStore.get(questionHandle).publicFacet).getQuestion(),
);

/**
* @param {ContractFacet} zcf
* @param {AddQuestion} addQuestion
*/
const getPoserInvitation = (zcf, addQuestion) => {
const questionPoserHandler = () => Far(`questionPoser`, { addQuestion });
return zcf.makeInvitation(questionPoserHandler, `questionPoser`);
Expand Down
11 changes: 11 additions & 0 deletions packages/governance/src/internalTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@
* @property {VoteCounterPublicFacet} publicFacet
* @property {Timestamp} deadline
*/

/**
* @callback StartCounter
* @param {ContractFacet} zcf
* @param {QuestionSpec} questionSpec
* @param {unknown} quorumThreshold
* @param {ERef<Installation>} voteCounter
* @param {Store<Handle<'Question'>, QuestionRecord>} questionStore
* @param {IterationObserver<unknown>} publication
* @returns {AddQuestionReturn}
*/
11 changes: 6 additions & 5 deletions packages/governance/src/question.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,19 +187,20 @@ const buildUnrankedQuestion = (questionSpec, counterInstance) => {
});
};

harden(buildUnrankedQuestion);
harden(ChoiceMethod);
harden(QuorumRule);
harden(ElectionType);
harden(looksLikeIssueForType);
harden(looksLikeQuestionSpec);
harden(positionIncluded);
harden(buildUnrankedQuestion);
harden(QuorumRule);

export {
buildUnrankedQuestion,
ChoiceMethod,
ElectionType,
QuorumRule,
looksLikeIssueForType,
looksLikeQuestionSpec,
positionIncluded,
looksLikeIssueForType,
buildUnrankedQuestion,
QuorumRule,
};
113 changes: 113 additions & 0 deletions packages/governance/src/shareHolders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// @ts-check

import { E } from '@agoric/eventual-send';
import { Far } from '@agoric/marshal';
import { makeSubscriptionKit } from '@agoric/notifier';
import { makeStore } from '@agoric/store';
import { AmountMath, AssetKind } from '@agoric/ertp';
import {
startCounter,
getOpenQuestions,
getQuestion,
getPoserInvitation,
} from './electorateTools';

const { details: X } = assert;

// shareHolders is an Electorate that relies on an attestation contract to
// validate ownership of voting shares. The electorate provides voting facets
// corresponding to the attestations to ensure that only valid holders of shares
// have the ability to vote.

// The attestation contract is responsible for ensuring that each votable share
// has a persistent handle that survives through extending the duration of the
// lien and augmenting the number of shares it represents. This contract makes
// that persistent handle visible to ballotCounters.

/** @type {ContractStartFn} */
const start = zcf => {
const {
brands: { Attestation: attestationBrand },
} = zcf.getTerms();
const empty = AmountMath.makeEmpty(attestationBrand, AssetKind.SET);

/** @type {Store<Handle<'Question'>, QuestionRecord>} */
const allQuestions = makeStore('Question');
const { subscription, publication } = makeSubscriptionKit();

/** @param { SetValue } attestations */
const makeVoterInvitation = attestations => {
// TODO: The UI will probably want something better, but I don't know what.
const voterDescription = attestations.reduce((desc, amount) => {
const { address, amountLiened } = amount;
return `${desc} ${address}/${amountLiened}`;
}, 'Address/Amount:');

// The electorate doesn't know the clock, but it believes the times are
// comparable between the clock for voting deadlines and lien expirations.

return Far(`a voter ${voterDescription}`, {
/**
* @param {Handle<'Question'>} questionHandle
* @param {Position[]} positions
*/
castBallotFor: (questionHandle, positions) => {
const { voteCap, deadline } = allQuestions.get(questionHandle);
return attestations
.filter(({ expiration }) => expiration > deadline)
.forEach(({ amountLiened, handle }) => {
return E(voteCap).submitVote(handle, positions, amountLiened);
});
},
});
};

/** @type {OfferHandler} */
const vote = seat => {
/** @type {Amount} */
const attestation = seat.getAmountAllocated('Attestation');
assert(
AmountMath.isGTE(attestation, empty, attestationBrand),
X`There was no attestation escrowed`,
);
// Give the user their attestation payment back
seat.exit();

assert.typeof(attestation.value, 'object'); // entailed by isGTE on empty SET
return makeVoterInvitation(attestation.value);
};

/** @type {AddQuestion} */
const addQuestion = async (voteCounter, questionSpec) => {
return startCounter(
zcf,
questionSpec,
0n,
voteCounter,
allQuestions,
publication,
);
};

/** @type {ClaimsElectoratePublic} */
const publicFacet = Far('publicFacet', {
getQuestionSubscription: () => subscription,
getOpenQuestions: () => getOpenQuestions(allQuestions),
getInstance: zcf.getInstance,
getQuestion: handle => getQuestion(handle, allQuestions),
makeVoterInvitation: () => zcf.makeInvitation(vote, 'attestation vote'),
});

/** @type {ShareholdersCreatorFacet} */
const creatorFacet = Far('creatorFacet', {
getPoserInvitation: () => getPoserInvitation(zcf, addQuestion),
addQuestion,
getQuestionSubscription: () => subscription,
getPublicFacet: () => publicFacet,
});

return { publicFacet, creatorFacet };
};

harden(start);
export { start };
Loading

0 comments on commit 3acf78d

Please sign in to comment.