After a crowdfund has acquired its NFTs, it creates a new governance Party
, where the NFTs are transferred. Contributors are minted NFT memberships in the new Party, containing voting power equivalent to their contribution during the crowdfund. Voting power can be used to vote on proposals, which contain possible actions for the Party to execute.
- Precious: A set of ERC-721 tokens custodied by the governance contract (
Party
), conventionally acquired in the crowdfund phase. These are protected assets and are subject to extra restrictions in proposals vs other assets. - Governance NFT: An NFT (ERC721) representing a membership with voting power within the governance Party.
- Party: The governance contract itself, which custodies the Precious, tracks voting power, manages the lifecycle of proposals, and is simultaneously the token contract for Governance NFTs.
- Proposals: On-chain actions that the Party wil execute, which must progress through the entire governance lifecycle in order to do so.
- Distributions: An (ungoverned) mechanism by which parties can distribute ETH and ERC-20 tokens held by the Party to members, proportional to their relative voting power (Governance NFTs).
- Party Hosts: Predefined accounts that can unilaterally veto proposals in the Party. Conventionally defined when the crowdfund is created.
- Globals: A single contract that holds configuration values, referenced by several ecosystem contracts.
- Proxies: All
Party
instances are deployed as simpleProxy
contracts that forward calls to aParty
implementation contract. - ProposalExecutionEngine: An upgradable contract the
Party
contract delegatecalls into that implements the logic for executing specific proposal types.
The main contracts involved in this phase are:
PartyFactory
(code)- Creates new proxified
Party
instances.
- Creates new proxified
Party
(code)- The governance contract that also custodies the precious NFTs. This is also the ERC-721 contract for the Governance NFTs.
ProposalExecutionEngine
(code)- An upgradable logic (and some state) contract for executing each proposal type from the context of the
Party
.
- An upgradable logic (and some state) contract for executing each proposal type from the context of the
TokenDistributor
(code)- Escrow contract for distributing deposited ETH and ERC20 tokens to members of parties.
Globals
(code)- A contract that defines global configuration values referenced by other contracts across the entire protocol.
Parties are created through the PartyFactory
contract. This is typically automatically done
by a crowdfund instance after it wins, but it is also a valid use case to interact with the PartyFactory
contract directly to, for example, form a governance party around an NFT you already own.
The sequence of events is:
- Call
PartyFactory.createParty()
defined as:function createParty( address authority, Party.PartyOptions memory opts, IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds )
authority
will be the address that can mint tokens on the created Party. In typical flow, the crowdfund contract will set this to itself.opts
are (mostly) immutable configuration parameters for the Party, defining the Party name, symbol, and customization preset (the Party instance will also be an ERC721) along with governance parameters.preciousTokens
andpreciousTokenIds
together define the NFTs the Party will custody and enforce extra restrictions on so they are not easily transferred out of the Party. This list cannot be changed after Party creation. Note that this list is never stored on-chain (only the hash is) and will need to be passed into theexecute()
call when executing proposals.- This will deploy a new
Proxy
instance with an implementation pointing to the Party contract defined by in theGlobals
contract by the keyGLOBAL_PARTY_IMPL
.
- Transfer assets to the created Party, which will typically be the precious NFTs.
- As the
authority
, mint Governance NFTs to members of the party by callingParty.mint()
.- In typical flow, the crowdfund contract will call this when contributors burn their contribution NFTs.
- Optionally, call
Party.abdicate()
, as theauthority
, to revoke minting privilege once all Governance NFTs have been minted. - At any step after the party creation, members with Governance NFTs can perform governance actions, though they may not be able to reach consensus if the total supply of voting power hasn't been minted/distributed yet.
When created through the PartyFactory
directly, the creator of a party can customize how they want their party's governance NFT card to look. For most parties, however, they will be created from successful crowdfunds and so will inherit the customization options set during initialization of that crowdfund.
The way that a crowdfund indicate to the parties they create to inherit their customization options is by passing in an ID of 0 to customizationPresetId
when creating the party. When rendering the tokenURI()
, this tells the PartyNFTRenderer
to read and use the customizationPresetId
of the Party's mintAuthority
(which should be the crowdfund that created it for parties created conventionally) for rendering its card SVG.
Parties can change their customization preset by executing an arbitrary call proposal that calls RendererStorage.useCustomizationPreset()
to chose another customization preset.
If an invalid customizationPresetID
(e.g. an ID that doesn't exist) is chosen or preset ID 0 is used but mintAuthority
has no preset ID chosen, it will fallback to the default design.
Parties are initialized with fixed governance options which will (mostly) never change for the Party's lifetime. They are defined in the PartyGovernance.GovernanceOpts
struct with the fields:
hosts
: Array of initial party hosts. This is the only configuration that can change because hosts can transfer their privilege to other accounts.voteDuration
: After being a proposal has been proposed, this is how long (in seconds) members can vote for it to pass. If this window expires before the proposal passes, it will be considered defeated.executionDelay
: Duration in seconds a proposal must wait after being passed before it can be executed. This gives hosts time to veto malicious proposals that have passed.passThresholdBps
: Minimum ratio of votes vstotalVotingPower
supply to consider a proposal passed. This is expressed in basis points, i.e.100 = 1%
totalVotingPower
: Total voting power of the Party. This should be the sum of weights of all (possible) Governance NFTs given to members. Note that nowhere is this assumption enforced, as there may be use-cases for minting more than 100% of voting power, but the logic in crowdfund contracts cannot mint more thantotalVotingPower
.feeBps
: The fee taken out of this Party's distributions to reserve forfeeRecipient
to claim. Typically this will be set to an address controlled by PartyDAO.feeRecipient
: The address that can claim distribution fees for this Party.
Voting power within the governance Party is represented and held by Governance NFTs, which are ERC721s minted for each member of the Party. Each Governance NFT has a distinct voting power/weight associated with it. These cards can never be broken up or combined, but a user may own multiple Governance NFTs within a Party. Within a Party, the total intrinsic voting power that a member has is the sum of all the voting power in all the Governance NFTs they possess at a given timestamp.
Owners of Governance NFTs can call Party.delegateVotingPower()
to delegate their total intrinsic voting power at the time of the call to another account. The minter of the Governance NFT can also set an initial delegate for the owner, meaning any Governance NFTs held by the owner will be delegated by default. If a user transfers their Governance NFT, the voting power will be delegated to the recipient's existing delegate.
The chosen delegate does not need to own a Governance NFT. Delegating voting power strips the owner of their total intrinsic voting power until they redelegate to themselves, meaning they will not be able to use these votes on proposals created in the meantime (because votes cast rely on snapshots). Governance NFT owners can recover their voting power for future proposals if they delegate to themselves or to the zero address. Even while delegating votes to another account, it's possible for a member to receive delegated votes from a separate address. Delegated votes are not forwarded beyond a single hop.
The effective voting power of a user is the sum of all undelegated (or self-delegated) voting power from their Governance NFTs plus the sum of all voting power delegated to them by other members.
The effective voting power of a user at a given time can be found by calling Party.getVotingPowerAt()
.
The voting power applied when a user votes on a proposal is their effective voting power at the time the proposal was proposed. This prevents people from acquiring large amounts of Governance NFTs to influence the outcome of an active proposal. The Party
contract appends a record of a user's total delegated (to them) and intrinsic voting power each time any of the following occurs:
- A user receives a Governance NFT (transfer or minting).
- A user transfers their Governance NFT to another user.
- A user delegates (or undelegates) their voting power.
- A user gets voting power delegated (or undelegated) to them.
When determining the effective voting power of a user, the protocol binary searches an address's voting power records for the most recent record that was created before the proposal's creation time.
Distributions allow parties to distribute fungible tokens and ETH to party members, proportional to the voting power of their Governance NFTs.
Unlike proposals, distributions do not require any votes to pass. Any member of the party can call distribute
to distribute any ETH or ERC-20 tokens held by the party.
Upon distribute
being called, the entire balance of the specified token will be transfered to the canonical TokenDistributor
contract, and a new distribution will be created.
Each distribution has a unique id. To interact with a distribution, you need to send in an entire DistributionInfo
object. The DistributionInfo
object can be found in the DistributionCreated
event that is emitted upon the distribution being created.
Once a distribution has been created, NFT holders can call claim
, sending in the DistributionInfo
as well as the PartyGovernance token id that they'd like to claim for. Users can also leverage batchClaim
if they'd like to claim multiple distributions in one transaction.
Every distribution can have a feeRecipient
and feeBps
set. The feeRecipient
and feeBps
for a party's distribution will be determined by the feeRecipient
and feeBps
set in GovernanceOpts
when the party was created. In order for the feeRecipient
to claim their fee, the fee recipient must call claimFee
. If the feeRecipient
is a smart contract that is unable to call claimFee
, the fee could be unretrievable, locked in the contract forever.
Consider the example:
A party is constructed with a `feeRecipient` of Bob and a `feeBps` of 250 basis points (2.5%).
Keith has 20% ownership of the party
Donna has 30% ownership of the party
Jerry has 50% ownership of the party
1000 DAI is deposited into the party contract, and a new distribution is created for this DAI.
When Bob calls `claimFee`, they receive 25.00 DAI (1000*0.025)
When Keith calls `claim`, they receive 195.00 DAI (1000*0.975)*0.2
When Donna calls `claim`, they receive 292.50 DAI (1000*0.975)*0.3
When Jerry calls `claim`, they receive 487.50 DAI (1000*0.975)*0.5
Party Protocol's TokenDistributor
contract was designed to work with Parties, but a Distribution
can be created for any contract that implements the ITokenDistributorParty
interface. Implementors of the ITokenDistributorParty
must implement getDistributionShareOf(uint256 tokenId)
which returns how much of a distribution a particular tokenId should receive. Denominated in proportion to 1e18
(i.e. 0.5e18
represents 50%), as well as ownerOf(uint256 tokenId)
which returns the owner of a tokenId. In the case of a PartyGovernanceNFT
, the getDistributionShareOf(uint256 tokenId)
defers to the ratio of the voting power of the specific tokenId
against the totalVotingPower
.
When creating a distribution, implementing contracts are expected to transfer the tokens prior to calling the accompanying create{Erc20Distribution,createNativeDistribution}
method in the same transaction.
TokenDistributor
contains an emergencyExecute
function that can perform an arbitrary delegatecall. This will only be called in an emergency scenario where the TokenDistributor
must be decommissioned. It is restricted to the PartyDAO Multisig. The ability to call this function will be disabled after emergencyExecuteDisabledTimestamp
has passed.
Governance in Parties revolves around creating, passing, and executing proposals. Proposals have the following properties:
maxExecutableTime
: A timestamp beyond which the proposal can no longer be executed. In the case of multi-step proposals, this only restricts the first time the proposal is executed.cancelDelay
: Number of seconds after a proposal enters theInProgress
state after which it can be forcibly interrupted and marked complete so another proposal can be executed.proposalData
: Encoded data needed to execute the proposal.- Proposal ID: A unique identifier (counter) assigned to a proposal when it it is first proposed.
An important thing to note is that none of these proposal properties (aside from the proposal ID) are ever stored on-chain. Instead, only the hash of these fields are stored on-chain (keyed by the proposal ID) to optimize for gas usage and enforce that the properties do not change between lifecycle operations.
The stages of a proposal are defined in PartyGovernance.ProposalStatus
:
Invalid
: The proposal does not exist.Voting
: The proposal has been proposed (viapropose()
), has not been vetoed by a party host, and is within the voting window. Members can vote on the proposal and party hosts can veto the proposal.Defeated
: The proposal has either exceeded its voting window without reachingpassThresholdBps
of votes or was vetoed by a party host.Passed
: The proposal reached at leastpassThresholdBps
of votes but is still waiting forexecutionDelay
to pass before it can be executed. Members can continue to vote on the proposal and party hosts can veto at this time.Ready
: Same asPassed
but nowexecutionDelay
has been satisfied or the proposal passed unanimously. Any member may execute the proposal viaexecute()
, unlessmaxExecutableTime
has arrived.InProgress
: The proposal has been executed at least once but has further steps to complete so it needs to be executed again. No other proposals may be executed while a proposal is in theInProgress
status, and therefore only a single proposal may ever be in theInProgress
status. No voting or vetoing of anInProgress
proposal is allowed, however it may be forcibly cancelled viacancel()
if thecancelDelay
has arrived.Complete
: The proposal was executed and completed all its steps. No voting or vetoing can occur and it cannot be cancelled nor executed again.Cancelled
: The proposal was executed at least once but did not complete beforecancelDelay
seconds passed since the first execute and was forcibly cancelled.
A proposer should choose an appropriate maxExecutableTime
and cancelDelay
. The proposalData
should be prefixed (like a function call) with a 4-byte IProposalExecutionEngine.ProposalType
value followed by the ABI-encoded data specific to that proposal type (see Proposal Types), e.g., abi.encodeWithSelector(bytes4(ProposalType.ListOnZoraProposal), abi.encode(ZoraProposalData(...)))
.
Once ready, any member or delegate (someone with nonzero effective voting power) can call propose()
with the proposal properties, which will assign a unique, nonzero proposal ID and put the proposal in the Voting
status. Creating a proposal will also automatically cast the proposer's votes for it.
Any proposal in the Voting
, Passed
, or Ready
status can be voted on by members and delegates via Party.accept()
. The accept()
function casts the caller's total effective voting power at the time the proposal was created. Once the total voting power cast for the proposal meets or exceeds the passThresholdBps
ratio, given by total cast voting power / totalVotingPower
, the proposal will enter the Passed
state.
Members can continue to vote even beyond the Passed
state in order to achieve a unanimous vote, which allows the proposal to bypass the executionDelay
and unlocks specific behavior for certain proposal types. A unanimous vote condition is met when 99.99% of totalVotingPower
has been cast for a proposal. We do not check for 100% because of possible rounding errors during minting from crowdfunds.
During the Voting
, Passed
, and Ready
phases of a proposal, a Party host may unilaterally veto that proposal by calling Party.veto()
, immediately putting the proposal in the Defeated
state. At that point, no further action can be taken on the proposal.
The rationale behind the veto power that if voting power in a Party becomes so consolidated that a bad actor can pass a malicious proposal, the party host can act as the final backstop. On the other hand, a party host can also stall a Party by vetoing every legitimate proposal, so Parties need to be extremely careful with who their hosts are.
After a proposal has achieved enough votes to pass and the executionDelay
window has expired, or if the proposal reached unanimous consensus, the proposal can be executed by any member with currently nonzero effective voting power. This occurs via the Party.execute()
function.
The call to execute()
will fail if:
- The proposal has already been executed and completed (in the
Complete
status). - The proposal has not been executed but its
maxExecutableTime
has passed. - The proposal's execution reverts.
- There exists another proposal that has been executed but did not complete (more steps to go).
If the proposal is atomic, meaning it is a single-step proposal, it will immediately enter the Complete
status.
Some proposal types require multiple steps and transactions to be completed. An example is the ListOnZoraProposal
type. This proposal will first list an NFT on Zora as an auction then if the auction does not receive any bids after some amount of time or if the auction completes with a winning bid, the auction will need to be cancelled or finalized by the Party. To accomplish this, the proposal must be executed multiple times until it is considered complete and can enter the Complete
status.
Usually further steps in a multi-step proposal require some state to be remembered between steps. For example, the ListOnZoraProposal
type will need to recall the ID of the Zora auction it created so it can cancel or finalize it as a final step. Rather than storing this (potentially complex) data on-chain, executing a proposal will emit a ProposalExecuted
event with an arbitrary bytes nextProgressData
parameter, which should be passed into the next call to execute()
to advance the proposal. The Party
will only store the hash of the nextProgressData
and confirm it matches the hash of what is passed in. This data blob holds any encoded state necessary to progress the proposal to the next step.
Once the proposal has executed its final step, it will emit an empty nextProgressData
in the ProposalExecuted
event.
There is a risk of multi-step proposals never being able to complete because they may continue to revert. Since no other proposals can be executed if another proposal is InProgress
, a Party can become permanently stuck, unable to execute any other proposal. To prevent this scenario, proposals have a cancelDelay
property. After a proposal has been in the InProgress
status after this many seconds, it can be forced into a Complete
state by calling cancel()
. There is also a global (defined in the Globals
contract) configuration value (GLOBAL_PROPOSAL_MAX_CANCEL_DURATION
) which limits the cancelDelay
to a duration not too far in the future.
Cancelling a proposal should be considered a last resort, as it can potentially leave the Party in a broken state (e.g., assets are stuck in another protocol) because the proposal was not able to properly clean up after itself. With this in mind, Parties should be careful not to pass proposals that have too short a cancelDelay
unless they fully trust all other members.
The Party
contract does not actually understand how to execute the different proposal types, and only perceives them as opaque binary data, proposalData
. This proposalData
, along with progressData
(which is also opaque), is passed into ProposalExecutionEngine.executeProposal()
by delegatecall. From there, the ProposalExecutionEngine will:
- Check that there isn't a different proposal that hasn't completed its steps.
- If this proposal is the outstanding incomplete proposal, check that the hash of the
progressData
it receives matches the hash of theprogressData
emitted the last time the proposal was executed. - Decode the first 4 bytes of the
proposalData
to determine the proposal type. - Decode
proposalData
andprogressData
to execute the next step in the proposal. - If the proposal is not complete, return non-empty
nextProgressData
.
The rationale for separating the ProposalExecutionEngine
from the Party
instance is to narrow the concern of the Party
contract with governance (which is already sufficiently complex) and, more importantly, so Parties can upgrade their ProposalExecutionEngine
contracts to support more proposal types as the protocol matures.
ProposalExecutionEngine
is not a pure logic contract, it does define, own, and maintain its own "private" storage state which exists in the context of the Party
:
- The proposal ID of the outstanding/incomplete (
InProgress
) proposal. - The hash of the
progressData
to be passed into the next call toexecuteProposal()
to advance the incomplete proposal.
These storage variables begin at a constant, non-overlapping slot index to avoid collisions and simplify explicit migrations to a new storage schema if necessary. It does not access any inline storage fields defined in the Party
contract, nor does the Party
contract access these storage variables.
The Party
contract can communicate whether the proposal passed unanimously by passing in flags
along with other proposal details. Currently the only flag supported is the LibProposal.PROPOSAL_FLAG_UNANIMOUS
flag, which indicates the proposal reached unanimous consensus before it was first executed.
The Party protocol will support 5 proposal types at launch:
- ArbitraryCalls Proposals
- ListOnZora Proposals
- ListOnOpensea Proposals
- Fractionalize Proposals
- UpgradeProposalEngineImpl Proposals
This proposal makes arbitrary contract calls as the Party. There are restrictions the types of calls that can be made in order to make a best effort to prevent precious NFTs from being moved out of the Party.
The proposalData
should be encoded as:
abi.encodeWithSelector(
// Prefix identifying this proposal type.
bytes4(ProposalType.ArbitraryCallsProposal),
// Array of ArbitraryCall structs.
[
ArbitraryCall(
// The call target.
/* address payable */ target,
// Amount of ETH to attach to the call (from executor's wallet).
/* uint256 */ value,
// Calldata.
/* bytes */ data,
// If true, the call is allowed to fail.
/* bool */ optional,
// Hash of the successful return data of the call.
// If 0x0, no return data checking will occur for this call.
/* bytes32 */ expectedResultHash
),
...
]
);
This proposal is atomic, completing in 1 step (aka. 1 execute()
call):
- Each call is executed in the order declared.
- ETH to attach to each call must be provided by the caller of the
Party.execute()
call. If the sum of all successful calls try to consume more thanmsg.value
, the entire proposal will revert. - If a call has a non-zero
expectedResultHash
then the result of the call will be hashed and matched against this value. If they do not match, then the entire proposal will revert. - If the call is to the
Party
itself, the entire proposal will revert. - If the call is to the
IERC721.onERC721Received()
function, the entire proposal will revert.- Recall that the
Party
contract is also the Governance NFT contract so calling this function can trick someone into thinking they received a Governance NFT.
- Recall that the
- If the proposal did not pass unanimously, extra checks are made to prevent moving a precious NFT:
- Before executing all the calls, check which precious NFTs the Party possesses. Then after executing all the calls, ensure we still possess them or else the entire proposal will revert.
- If the call is to
IERC721.approve()
, the target is a precious NFT token, and the token ID is a matching precious token ID, revert the entire proposal unless the operator would be set to the zero address. - If the call is to
IERC721.setApprovalForAll()
and the target is a precious NFT token, revert the entire proposal unless the approval status would be set tofalse
.
- Unanimous proposals will not have restrictions on moving precious tokens or setting allowances for them.
This proposal type lists an NFT held by the Party on a Zora V1 auction.
The proposalData
should be encoded as:
abi.encodeWithSelector(
// Prefix identifying this proposal type.
bytes4(ProposalType.ListOnZoraProposal),
ZoraProposalData(
// The minimum bid (ETH) for the NFT.
/* uint256 */ listPrice,
// How long before the auction can be cancelled if no one bids.
/* uint40 */ timeout,
// How long the auction lasts once a person bids on it.
/* uint40 */ duration,
// The token contract of the NFT being listed.
/* IERC721 */ token,
// The token ID of the NFT being listed.
/* uint256 */ tokenId
)
);
This proposal always has 2 steps (aka. 2 execute()
calls):
- Transfer the token to the Zora auction house contract and create an auction with
listPrice
reserve price andduration
auction duration (which starts after someone places a bid).- This will emit the next
progressData
:
abi.encode( // The current step. ListOnZoraStep.ListedOnZora, ZoraProposalData( // The Zora auction ID. /* uint256 */ auctionId, // The minimum time when the auction can be cancelled. /* minExpiry */ minExpiry ) );
- This will emit the next
- Either cancel or finalize the auction.
- Cancel the auction if the auction was never bid on and
minExpiry
time has passed. This will also return the NFT to the party. - Finalize the auction if someone has bid on it and the auction
duration
has passed. This will transfer the top bid amount (in ETH) to the Party. It is also possible someone else finalized the auction already, in which case the Party already has the ETH and this step becomes a no-op.
- Cancel the auction if the auction was never bid on and
This proposal type ultimately tries to list an NFT held by the Party on OpenSea (Seaport 1.1). Because OpenSea listings are limit orders, there is no mechanism for on-chain price discovery (unlike a Zora auction). To mitigate a malicious proposal listing a precious NFT for far below its actual worth this proposal type will first place the NFT in a Zora auction that must end without receiving any bids before creating an OpenSea listing if the proposal was not passed unanimously. If it was passed unanimously or the NFT listed is not a precious, this step is skipped. The durations for this Zora step are defined by the global values GLOBAL_OS_ZORA_AUCTION_TIMEOUT
and GLOBAL_OS_ZORA_AUCTION_DURATION
.
The proposalData
should be encoded as:
abi.encodeWithSelector(
// Prefix identifying this proposal type.
bytes4(ProposalType.ListOnOpenseaProposal),
OpenseaProposalData(
// The price (in ETH) to sell the NFT.
// This is also the reserve bid for the Zora auction.
/* uint256 */ listPrice,
// How long the listing is valid for.
/* uint40 */ duration,
// The NFT token contract.
/* IERC721 */ token,
// the NFT token ID.
/* uint256 */ tokenId,
// Fees the taker must pay when filling the listing.
/* uint256[] */ fees,
// Respective recipients for each fee.
/* address payable[] */ feeRecipients
)
);
This proposal has between 2-3 steps (aka. 2-3 execute()
calls), depending on whether the proposal was passed unanimously and whether the lisetd NFT is precious or not:
- If the proposal did not pass unanimously AND the
token
+tokenId
is precious, the proposal starts here. Otherwise, if either of those conditions are false, skip to 2b.- Transfer the token to the Zora auction house contract and create an auction with
listPrice
reserve price andGLOBAL_OS_ZORA_AUCTION_DURATION
auction duration (which starts after someone places a bid).- This will emit the next
progressData
:
abi.encode( // The current step. ListOnOpenseaStep.ListedOnZora, ZoraProposalData( // The Zora auction ID. /* uint256 */ auctionId, // The minimum time when the auction can be cancelled. /* minExpiry */ minExpiry ) );
- This will emit the next
- Transfer the token to the Zora auction house contract and create an auction with
- Now it branches off into one of two paths:
- If a bid was placed and an auction happened, finalize the auction.
- Finalize the auction if someone has bid on it and the auction duration has passed. This will transfer the top bid amount (in ETH) to the Party. It is also possible someone else finalized the auction already, in which case the Party already has the ETH and this step becomes a no-op. The proposal will be complete at this point with no further steps.
- If the proposal passed unanimously, or the
token
+tokenId
is not precious, ortoken
+tokenId
is precious but no bid was placed during the safety Zora auction period:- If the item was listed for safety auction, was never bid on, and
progressData.minExpiry
has passed, cancel the auction. This will also return the NFT to the party. - Grant OpenSea an allowance for the NFT and create a non-custodial OpenSea listing for the NFT with price
listPrice
+ any extrafees
that is valid forduration
seconds.- This will emit the next
progressData
:
abi.encode( // The current step. ListOnOpenseaStep.ListedOnOpenSea, OpenseaProgressData( // Hash of the OS order that was listed. /* bytes32 */ orderHash, // Expiration timestamp of the listing. /* uint40 */ expiry ) );
- This will emit the next
- If the item was listed for safety auction, was never bid on, and
- If a bid was placed and an auction happened, finalize the auction.
- Clean up the OpenSea listing, emitting an event with the outcome, and:
- If the order was filled, the Party has the
listPrice
ETH, the NFT allowance was consumed, and there is nothing left to do. - If the order expired, no one bought the listing and the Party still owns the NFT. Revoke OpenSea's token allowance.
- If the order was filled, the Party has the
This proposal type fractionalizes an NFT on Fractional V1, minting Fractional ERC20 tokens claimable by all party members via distributions.
The proposalData
should be encoded as:
abi.encodeWithSelector(
// Prefix identifying this proposal type.
bytes4(ProposalType.FractionalizeProposal),
FractionalizeProposalData(
// The ERC721 token contract to fractionalize.
/* IERC721 */ token;
// The ERC721 token ID to fractionalize.
/* uint256 */ tokenId;
// The starting reserve price for the fractional vault.
/* uint256 */ listPrice;
)
);
This proposal is atomic, completing in 1 step (aka. 1 execute()
call):
- Create a new Fractional V1 vault around
token
+tokenId
.- Reserve price will be set to the proposal's
listPrice
. - Curator will be set to
address(0)
. totalVotingPower
fractional ERC20 tokens will be minted and held by the Party, which can later be claimed through an ERC20 distribution.
- Reserve price will be set to the proposal's
This proposal type upgrades the ProposalExecutionEngine
instance for a party to the latest version defined in the GLOBAL_PROPOSAL_ENGINE_IMPL
global value.
The proposalData
should be encoded as:
abi.encodeWithSelector(
// Prefix identifying this proposal type.
bytes4(ProposalType.UpgradeProposalEngineImpl),
// Arbitrary data needed by the new ProposalExecutionEngine to migrate.
/* bytes */ initData
);
This proposal is atomic, completing in 1 step (aka. 1 execute()
call):
- The current
ProposalExecutionEngine
implementation address is looked up in theGlobals
contract, keyed byGLOBAL_PROPOSAL_ENGINE_IMPL
. - The current
ProposalExecutionEngine
implementation address used by the Party is kept at an explicit, non-overlapping storage slot and will be overwritten with the new implementation address (see ProposalStorage). - The Party will
delegatecall
into the newProposalExecutionEngine
'sinitialize()
function, passing in the old implementation's address andinitData
migration data.- This gives new implementations some ability to perform state migrations. E.g., if the storage schema or semantics changes.
initData
is provided by the proposer as a possible hint to the migration process.
By default when a party is created, there is an emergencyExecute()
function that can be used by the PartyDAO multisig in the case of an emergency. This function can execute arbitrary bytecode and withdraw ETH. This emergency power can be revoked by any party host, or by the PartyDAO multisig itself.