-
Notifications
You must be signed in to change notification settings - Fork 26
Multisig contracts #378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Multisig contracts #378
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
78196e0
add multisig compile as turbo task
andrew-fleming 3f13582
add init multisig modules
andrew-fleming 48330f1
add helpers
andrew-fleming 1252803
add basic multisig contract
andrew-fleming 7abe151
remove unused param
andrew-fleming 5def030
fix newCount var in _approveProposal
andrew-fleming e15782e
remove setup, remove ownable
andrew-fleming 8348bc9
fix isProposalApprovedBySigner
andrew-fleming 7db320b
add member check to _approveProposal
andrew-fleming 41d9888
replace cancelApproval with revokeApproval
andrew-fleming f13f3a0
fix isProposalApprovedBySigner
andrew-fleming 66cd730
add empty witnesses
andrew-fleming 22ca211
export compact types from mock
andrew-fleming b4e36f9
add SignerManager sim
andrew-fleming 700bdb2
add SignerManager tests
andrew-fleming 0fccc11
fix param order
andrew-fleming da3365d
move deposit
andrew-fleming 1b979e6
move file/improve contract name
andrew-fleming fdbb640
add comma to param
andrew-fleming e5b4e06
fix fmt
andrew-fleming f521b3d
add multisig script to contracts manifest
andrew-fleming c780647
normalize file names
andrew-fleming 2452867
update import path
andrew-fleming cd23631
Merge branch 'main' into add-multisig
pepebndc 75d36ea
validate threshold does not exceed signer count after initialization
pepebndc e26647e
Add multisig tests (#380)
pepebndc fd2d72b
Merge branch 'main' into add-multisig
pepebndc c4c0f77
Merge branch 'main' into add-multisig
pepebndc 01ed666
fix fmt
andrew-fleming 0d64f87
add stateless treasury and multisig v2
andrew-fleming 5aefaaf
fix mod name, add stateless mocj
andrew-fleming 71b8a6e
export circuit
andrew-fleming d1552a5
remove export
andrew-fleming 75ec231
clean up comment
andrew-fleming 0938974
fix domain
andrew-fleming eafb62a
fix title
andrew-fleming 65f3156
refactor: contracts/src/multisig/ProposalManager.compact
0xisk b8cb61a
Merge branch 'main' into add-multisig
0xisk dfcbe89
refactor(multisig): fix requested changes (#461)
0xisk 548a7e4
Merge branch 'post-release' into add-multisig
0xisk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,328 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // OpenZeppelin Compact Contracts v0.0.1-alpha.1 (multisig/ProposalManager.compact) | ||
|
|
||
| pragma language_version >= 0.21.0; | ||
|
|
||
| /** | ||
| * @module ProposalManager | ||
| * @description Token-agnostic proposal lifecycle management for multisig | ||
| * governance contracts. | ||
| * | ||
| * Supports shielded and unshielded proposals through a unified | ||
| * Recipient type with a RecipientKind tag. Typed helper circuits | ||
| * provide safe construction of recipients without exposing the | ||
| * internal Bytes<32> representation to consumers. | ||
| */ | ||
| module ProposalManager { | ||
| import CompactStandardLibrary; | ||
|
|
||
| // ─── Types ────────────────────────────────────────────────────── | ||
|
|
||
| export enum ProposalStatus { | ||
| Inactive, | ||
| Active, | ||
| Executed, | ||
| Cancelled | ||
| } | ||
|
|
||
| export enum RecipientKind { | ||
| ShieldedUser, | ||
| UnshieldedUser, | ||
| Contract | ||
| } | ||
|
|
||
| export struct Recipient { | ||
| kind: RecipientKind, | ||
| address: Bytes<32> | ||
| } | ||
|
|
||
| export struct Proposal { | ||
| to: Recipient, | ||
| color: Bytes<32>, | ||
| amount: Uint<128>, | ||
| status: ProposalStatus | ||
| } | ||
|
|
||
| // ─── State ────────────────────────────────────────────────────── | ||
|
|
||
| export ledger _nextProposalId: Counter; | ||
| export ledger _proposals: Map<Uint<64>, Proposal>; | ||
|
|
||
| // ─── Recipient Helpers ────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * @description Constructs a shielded user recipient. | ||
| * | ||
| * @param {ZswapCoinPublicKey} key - The shielded recipient's public key. | ||
| * | ||
| * @returns {Recipient} The typed recipient. | ||
| */ | ||
| export circuit shieldedUserRecipient(key: ZswapCoinPublicKey): Recipient { | ||
| return Recipient { kind: RecipientKind.ShieldedUser, address: key.bytes }; | ||
| } | ||
|
|
||
| /** | ||
| * @description Constructs an unshielded user recipient. | ||
| * | ||
| * @param {UserAddress} addr - The unshielded recipient's address. | ||
| * | ||
| * @returns {Recipient} The typed recipient. | ||
| */ | ||
| export circuit unshieldedUserRecipient(addr: UserAddress): Recipient { | ||
| return Recipient { kind: RecipientKind.UnshieldedUser, address: addr.bytes }; | ||
| } | ||
|
|
||
| /** | ||
| * @description Constructs a contract recipient. | ||
| * | ||
| * @param {ContractAddress} addr - The contract address. | ||
| * | ||
| * @returns {Recipient} The typed recipient. | ||
| */ | ||
| export circuit contractRecipient(addr: ContractAddress): Recipient { | ||
| return Recipient { kind: RecipientKind.Contract, address: addr.bytes }; | ||
| } | ||
|
|
||
| /** | ||
| * @description Converts a Recipient to a shielded send recipient. | ||
| * Handles both ShieldedUser and Contract kinds. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * - Recipient kind must be ShieldedUser or Contract. | ||
| * | ||
| * @param {Recipient} r - The recipient. | ||
| * | ||
| * @returns {Either<ZswapCoinPublicKey, ContractAddress>} The shielded recipient. | ||
| */ | ||
| export circuit toShieldedRecipient(r: Recipient): Either<ZswapCoinPublicKey, ContractAddress> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What will be the need for this API? |
||
| if (r.kind == RecipientKind.ShieldedUser) { | ||
| return left<ZswapCoinPublicKey, ContractAddress>( | ||
| ZswapCoinPublicKey { bytes: r.address } | ||
| ); | ||
| } | ||
| assert(r.kind == RecipientKind.Contract, "ProposalManager: invalid shielded recipient"); | ||
| return right<ZswapCoinPublicKey, ContractAddress>( | ||
| ContractAddress { bytes: r.address } | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @description Converts a Recipient to an unshielded send recipient. | ||
| * Handles both UnshieldedUser and Contract kinds. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * - Recipient kind must be UnshieldedUser or Contract. | ||
| * | ||
| * @param {Recipient} r - The recipient. | ||
| * | ||
| * @returns {Either<ContractAddress, UserAddress>} The unshielded recipient. | ||
| */ | ||
| export circuit toUnshieldedRecipient(r: Recipient): Either<ContractAddress, UserAddress> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What will be the need for this API? |
||
| if (r.kind == RecipientKind.Contract) { | ||
| return left<ContractAddress, UserAddress>( | ||
| ContractAddress { bytes: r.address } | ||
| ); | ||
| } | ||
| assert(r.kind == RecipientKind.UnshieldedUser, "ProposalManager: invalid unshielded recipient"); | ||
| return right<ContractAddress, UserAddress>( | ||
| UserAddress { bytes: r.address } | ||
| ); | ||
| } | ||
|
|
||
| // ─── Guards ───────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * @description Asserts that a proposal exists. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * - Proposal with `id` must have been created. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {[]} Empty tuple. | ||
|
0xisk marked this conversation as resolved.
|
||
| */ | ||
| export circuit assertProposalExists(id: Uint<64>): [] { | ||
| assert( | ||
| _proposals.member(disclose(id)), | ||
| "ProposalManager: proposal not found" | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @description Asserts that a proposal exists and is active. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * - Proposal must exist. | ||
| * - Proposal status must be Active. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {[]} Empty tuple. | ||
| */ | ||
| export circuit assertProposalActive(id: Uint<64>): [] { | ||
| assertProposalExists(id); | ||
| assert( | ||
| _proposals.lookup(disclose(id)).status == ProposalStatus.Active, | ||
| "ProposalManager: proposal not active" | ||
| ); | ||
| } | ||
|
|
||
| // ─── Proposal Lifecycle ───────────────────────────────────────── | ||
|
|
||
| /** | ||
| * @description Creates a new proposal. | ||
| * | ||
| * @notice Access control is NOT enforced here. | ||
| * The consuming contract must gate this behind its own | ||
| * authorization policy. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * - `amount` must be greater than 0. | ||
| * | ||
| * @param {Recipient} to - The recipient (constructed via helper circuits). | ||
| * @param {Bytes<32>} color - The token color. | ||
| * @param {Uint<128>} amount - The amount to transfer. | ||
| * | ||
| * @returns {Uint<64>} The new proposal ID. | ||
| */ | ||
| export circuit _createProposal( | ||
| to: Recipient, | ||
| color: Bytes<32>, | ||
| amount: Uint<128> | ||
| ): Uint<64> { | ||
| assert(amount > 0, "ProposalManager: zero amount"); | ||
|
|
||
| _nextProposalId.increment(1); | ||
| const id = _nextProposalId; | ||
|
|
||
| _proposals.insert(id, disclose(Proposal { | ||
| to: to, | ||
| color: color, | ||
| amount: amount, | ||
| status: ProposalStatus.Active | ||
| })); | ||
|
|
||
| return id; | ||
| } | ||
|
|
||
| /** | ||
| * @description Cancels a proposal. | ||
| * | ||
| * @notice Access control is NOT enforced here. | ||
| * The consuming contract must gate this behind its own | ||
| * authorization policy. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * - Proposal must be active. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {[]} Empty tuple. | ||
| */ | ||
| export circuit _cancelProposal(id: Uint<64>): [] { | ||
| assertProposalActive(id); | ||
|
|
||
| const proposal = _proposals.lookup(disclose(id)); | ||
| _proposals.insert(disclose(id), Proposal { | ||
| to: proposal.to, | ||
| color: proposal.color, | ||
| amount: proposal.amount, | ||
| status: ProposalStatus.Cancelled | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * @description Marks a proposal as executed. | ||
| * | ||
| * @notice Access control is NOT enforced here. | ||
| * The consuming contract must gate this behind its own | ||
| * authorization policy. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * - Proposal must be active. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {[]} Empty tuple. | ||
| */ | ||
| export circuit _markExecuted(id: Uint<64>): [] { | ||
| assertProposalActive(id); | ||
|
|
||
| const proposal = _proposals.lookup(disclose(id)); | ||
| _proposals.insert(disclose(id), Proposal { | ||
| to: proposal.to, | ||
| color: proposal.color, | ||
| amount: proposal.amount, | ||
| status: ProposalStatus.Executed | ||
| }); | ||
| } | ||
|
|
||
| // ─── View ─────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * @description Returns the full proposal data. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {Proposal} The proposal. | ||
| */ | ||
| export circuit getProposal(id: Uint<64>): Proposal { | ||
| assertProposalExists(id); | ||
| return _proposals.lookup(disclose(id)); | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the recipient of a proposal. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {Recipient} The recipient. | ||
| */ | ||
| export circuit getProposalRecipient(id: Uint<64>): Recipient { | ||
| assertProposalExists(id); | ||
| return _proposals.lookup(disclose(id)).to; | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the amount of a proposal. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {Uint<128>} The amount. | ||
| */ | ||
| export circuit getProposalAmount(id: Uint<64>): Uint<128> { | ||
| assertProposalExists(id); | ||
| return _proposals.lookup(disclose(id)).amount; | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the token color of a proposal. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {Bytes<32>} The token color. | ||
| */ | ||
| export circuit getProposalColor(id: Uint<64>): Bytes<32> { | ||
| assertProposalExists(id); | ||
| return _proposals.lookup(disclose(id)).color; | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the status of a proposal. | ||
| * | ||
| * @param {Uint<64>} id - The proposal ID. | ||
| * | ||
| * @returns {ProposalStatus} The proposal status. | ||
| */ | ||
| export circuit getProposalStatus(id: Uint<64>): ProposalStatus { | ||
| assertProposalExists(id); | ||
| return _proposals.lookup(disclose(id)).status; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.