Proposal Agreement technical specification

Hello everyone!

I wanted to share a technical specification we’ve been working on in AragonOne as part of the Aragon Court implementation we’ve been taking care of. As many of you may know ProposalAgreement is a forwarder that can be used to forward intents to Voting-like instances. Additionally, this new flow allows interacting with an Arbitrator (Aragon Court) to handle disputes in case parties of the organization do not agree on intents being proposed.

Here are all the interfaces and functionalities we’ve been thinking of. Feel free to drop any feedback, recommendations, opinions, or simply come and contribute!

IArbitrable

The IArbitrable interface MUST be implemented by contracts that can be arbitrated by any dispute resolutions oracle like IArbitrator (see an example here)

1. submitEvidence

function submitEvidence(uint256 disputeId, bytes evidence, bool finished) external;

Parameters:

  • disputeId: Identification number of the dispute submitting evidence for
  • evidence: Data submitted for the evidence related to the dispute
  • finished: Whether or not the submitter has finished submitting evidence

Authentication: Only accounts allowed by the IArbitrable instance to arbitrate a ruling for the given dispute. For example, an IArbitrator instance.

Action:

  • Save the evidence submitted by the sender for the given dispute
  • If both parties agree on finishing the evidence submission period, the closeEvidencePeriod method of the IArbitrator should be called

2. rule

function rule(uint256 disputeId, uint256 ruling) external;

Parameters:

  • disputeId: Identification number of the dispute to be ruled
  • ruling: Ruling given by the arbitrator, where 0 is reserved for “refused to make a decision”

Authentication: Only accounts allowed by the IArbitrable instance to arbitrate a ruling for the given dispute. For example, an IArbitrator instance.

Action:

  • Save the ruling adjudicated for the given dispute

3. supportsInterface

function supportsInterface(bytes4 interfaceId) external pure returns (bool)

Parameters:

  • interfaceId: interface identifier being queried, as specified in ERC-165

Authentication: Open

Action:

  • Return true if the queried interface is 0x88f3ee69, false otherwise

IArbitrator

The IArbitrator interface MUST be implemented by contracts that can be used as the arbitrator for ProposalAgreements (see an example here)

1. createDispute

function createDispute(uint256 rulings, bytes32 metadata) external returns (uint256)

Parameters:

  • rulings: Number of possible rulings allowed for the dispute
  • metadata: Optional metadata that can be used to provide additional information on the dispute to be created

Authentication: Open

Action:

  • Ensure that the msg.sender is compliant with the IArbitrable interface following EIP-165
  • Ensure that the msg.sender is up-to-date with the subscription fees
  • Ensure that the given number of possible rulings is allowed
  • Create a new dispute for the calling IArbitrable

2. closeEvidencePeriod

function closeEvidencePeriod(uint256 disputeId) external

Parameters:

  • disputeId: Identification number of the dispute to close its evidence submitting period

Authentication: Only the IArbitrable instance associated with the given dispute

Action:

  • Ensure that the evidence period is still open
  • Update the evidence period duration for the given dispute

3. executeRuling

function executeRuling(uint256 disputeId) external

Parameters:

  • disputeId: Identification number of the dispute to be ruled

Authentication: Open

Action:

  • Ensure that the requested dispute has already been solved
  • Rule the final ruling over its associated IArbitrable instance

4. getDisputeFees

function getDisputeFees() external view returns (address recipient, ERC20 feeToken, uint256 feeAmount)

Parameters: None

Authentication: Open

Action:

  • Return the address where the corresponding dispute fees must be transferred to
  • Return the ERC20 token used for the dispute fees
  • Return the total amount of fee tokens that must be allowed to the recipient

5. getSubscriptionFees

function getSubscriptionFees(address subscriber) external view returns (address recipient, ERC20 feeToken, uint256 feeAmount)

Parameters:

  • subscriber: Address of the account paying the subscription fees for

Authentication: Open

Action:

  • Return the address where the corresponding subscription fees must be transferred to
  • Return the ERC20 token used for the subscription fees
  • Return the total amount of fee tokens that must be allowed to the recipient

IDisputable

The IDisputable interface MUST be implemented by contracts that use ProposalAgreements for disputing intents.

In order to conform with IDisputable, a contract MUST also conform to IForwarder.

This interface should be implemented in aragonOS, so Aragon apps can easily conform to it.

1. nextIntentId

function nextIntentId() external view returns (uint256)

Parameters: None

Authentication: Open

Action:

  • Return a uint256 for the id that will be given to the next intent that is created.
  • A forward action in the contract should increase the returned nextIntentId by 1 or revert. In the case of Voting, nextIntentId should return the same as votesLength.

2. canDisputeIntent

function canDisputeIntent(uint256 intentId) external view returns (bool)

Parameters:

  • intentId: Identification number of the intent being queried

Authentication: Open

Action:

  • Return true if the given intent can be disputed, false otherwise

3. onDisputeCreation

function onDisputeCreation(uint256 intentId) external

Parameters:

  • intentId: Identification number of the intent associated with the created dispute

Authentication: Only accounts allowed by the IDisputable instance to dispute intents. For example, in the case of Voting, only the ProposalAgreement should be allowed to call this function.

Action:

  • Perform any action that should occur in the IDisputable instance when an intent is disputed. For example, in the case of Voting, it will pause the vote with id intentId until the dispute is solved.

4. onRejectedDispute

function onRejectedDispute(uint256 intentId) external

Parameters:

  • intentId: Identification number of the intent associated to the rejected dispute

Authentication: Only accounts allowed by the IDisputable instance to dispute intents. For example, in the case of Voting, only the ProposalAgreement should be allowed to call this function.

Action:

  • Perform any action that should occur in the IDisputable instance when a dispute associated to an intent is rejected. For example, in the case of Voting, it will resume the vote with id intentId until the dispute is solved.

5. onAcceptedDispute

function onAcceptedDispute(uint256 intentId) external

Parameters:

  • intentId: Identification number of the intent associated to the accepted dispute

Authentication: Only accounts allowed by the IDisputable instance to dispute intents. For example, in the case of Voting, only the ProposalAgreement should be allowed to call this function.

Action:

  • Perform any action that should occur in the IDisputable instance when a dispute associated to an intent is accepted. For example, in the case of Voting, it will cancel the vote with id intentId until the dispute is solved.

ProposalAgreement

1. initialize

function initialize(IStaking staking, address collateralToken, uint256 collateralAmount, IArbitrator arbitrator, Vault subscriptionsVault, bytes agreementURI, uint256 disputeConfirmPeriod) external

Parameters:

  • staking: The staking instance address that will be used for depositing collateral for the agreement
  • collateralToken: Address of the token to be used for the collateral
  • collateralAmount: The amount of collateralToken that needs to be staked in order to forward a proposal or to challenge it
  • arbitrator: Address of the IArbitrator that will be used to resolve disputes
  • subscriptionsVault: Address of a Vault-like contract in the organization that will be used to pay subscription fees. Allows changes protected by a role.
  • agreementURI: Link to the human-readable text that describes what types of proposals are valid. It should be a content-addressed link (IPFS) to avoid tampering
  • disputeConfirmPeriod: Duration in seconds during which a challenge can be confirmed. If it’s not confirmed on time, it will be considered a settlement

Authentication: Open

Action:

  • Save all the values as state variables

2. forward

function forward(bytes script) external

Parameters:

  • script: The script being forwarded (must be a CallsScript with just one call that creates an intent in an IDisputable instance)

Authentication: Only accounts with the CREATE_PROPOSALS_ROLE can use the forwarder to create a proposal. Similar to Voting only allowing accounts with the CREATE_VOTES_ROLE to forward intents.

Action:

  • Create a new proposal in a draft stage and assign it a new proposalId
  • The script won’t be automatically executed, it will be logged and its hash will be stored in the proposal

3. confirmProposal

function confirmProposal(uint256 proposalId, bytes script, IDisputable disputable, uint256 lockId, bytes contextURI) external

Description: Once a proposal has been created, it needs to be confirmed by providing the required collateral (staking lock). Confirming a proposal executes the script associated with it, which will create an intent (e.g. create a vote).

Parameters:

  • proposalId: Identification number of the proposal being confirmed
  • script: The script being forwarded (must be a CallsScript with just one call that creates an intent in an IDisputable instance)
  • disputable: Address of the IDisputable instance where the intent is being created with the script
  • lockId: Identification number of the lock associated to the corresponding collateral amount in Staking
  • contextURI: A content-addressed link to human-readable text written by the proposer providing context on the proposal

Authentication: Because proposal creation is protected by a role, confirming a proposal for execution doesn’t necessarily need to be protected. Anyone could confirm a pending proposal (and provide their own contextURI), taking on the risk of losing their own collateral if it were successfully challenged.

Setup:

  • Before calling confirmProposal, the sender must stake the required collateralAmount of collateralToken in staking and create a lock:
    • The lock manager needs to be set to the ProposalAgreement instance
    • The lock data of the lock is a unique identifier for the specific proposal: H(proposalAgreementAddress, H(script), disputable, contextURI).

Action:
The actual forwarding action has a tight coupling with the provided disputable instance.

  • Ensure that the msg.sender has a valid lock in staking with lockId:
    • The locked amount is collateralAmount tokens
    • The lock manager is the ProposalAgreement address
    • The lock data is H(proposalAgreementAddress, H(script), disputable, contextURI)
    • The lock isn’t being used in an unresolved proposal (see canUnlock)
  • Ensure that H(script) matches the script hash saved in the proposal.
  • Before executing the script, get the nextIntentId from disputable and save it in the proposal object as intentId
  • Execute the script. The script can only be a CallsScript with only one call to avoid script execution from resolving disputes in a disputable (this requires changes in aragonOS CallsScript executor)
  • After the script has been executed, the nextIntentId for disputable must have been increased by 1, revert otherwise

Comments:

  • This way ProposalAgreement knows what intentId it needs to interact with in case that there is a challenge and ensures that only one proposal can be created with one collateral lock. By limiting the number of calls that the CallsScript can execute, we also prevent scripts from sending arbitrary calls to disputable.

4. challenge

function challenge(uint256 proposalId, address challenger, uint256 settlementOffer, uint256 lockId, bytes evidenceURI) external

Parameters:

  • proposalId: Identification number of the proposal being challenged
  • challenger: Address that creates the dispute
  • settlementOffer: Amount of collateral from proposer that the challenger would accept for resolving the dispute without involving the arbitrator. If the dispute isn’t confirmed during the disputeConfirmPeriod, the settlement will be considered accepted.
  • lockId: Identification number of the lock created by the challenger in staking
  • evidenceURI: Content-addressed link to human-readable text written by the challenger

Authentication: Only accounts with the CHALLENGE_PROPOSALS_ROLE can initiate a challenge for a proposal

Setup:

  • Before calling challenge, the challenger must stake the required collateralAmount of collateralToken in staking and create a lock:
    • The lock manager needs to be set to the ProposalAgreement instance
    • The lock data of the lock is a unique identifier for the specific challenge: H(proposalAgreementAddress, proposalId,evidenceURI).
  • On top of locking collateral, the challenger must pay half of the dispute fees for the first adjudication round. The challenger has to approve proposalAgreementAddress to pull the amount of tokens required to pay half of the arbitrator dispute fees (the fee token may be different from the collateral token).

Action:

  • Ensure that the proposal has been confirmed
  • Ensure there is no on-going dispute for the same proposalId
  • Ensure that the proposal can still be challenged (disputable.canDisputeIntent(intentId))
  • Ensure that the settlementOffer is between 0 and collateralAmount
  • Ensure that challenger has valid a lock with lockId:
    • The locked amount is collateralAmount tokens
    • The lock manager is the ProposalAgreement address
    • The lock data is H(proposalAgreementAddress, proposalId, evidenceURI)
    • The lock isn’t being used in an unresolved proposal (see canUnlock)
  • Pull half of the dispute fee amount needed to create a proposal (arbitrator.getDisputeCreationFees()) from the challenger’s account (the lock data is an implicit authorization to transfer tokens from challenger, even if they are not the msg.sender).
  • Save the current time as the disputeChallengedTime of the proposal
  • Execute onDisputeCreation(intentId) in disputable of the proposal

5. confirmDispute

function confirmDispute(uint256 proposalId, bytes evidenceURI) external

Parameters:

  • proposalId: Identification number of the proposal for which the dispute is being confirmed
  • evidenceURI: Content-addressed link to human-readable text written by the proposer

Authentication: Only the creator of the dispute can confirm it

Setup:

  • The proposer has to create a token allowance for an amount equivalent to half of dispute fees that they will be paying to the arbitrator

Action:

  • Ensure that the dispute can still be confirmed (now - disputeChallengedTime <= disputeConfirmPeriod)
  • Pull the remaining amount of the dispute fee amount needed to create a proposal (arbitrator.getDisputeCreationFees()) from msg.sender. If fees haven’t changed in the arbitrator, it should be half of the total amount. If they have changed, the confirmer will take the risk and always pay the remaining amount (could be more or less than half of the total).
  • Create a dispute in the arbitrator about whether the proposal was valid according to the agreement. The disputeId returned (and arbitrator instance used) when creating the dispute needs to be stored in order to identify IArbitrator rulings (one ProposalAgreement instance can have multiple ongoing disputes in different IArbitrators at the same time)

6. settleUnconfirmedDispute

function settleUnconfirmedDispute(uint256 proposalId) external

Parameters:

  • proposalId: Identification number of the proposal for which the dispute hasn’t been confirmed

Authentication: Open

Action:

  • Ensure that the dispute can no longer be confirmed (now - disputeChallengedTime > disputeConfirmPeriod)
  • Ensure that the dispute hasn’t been settled yet
  • Transfer settlementOffer from the proposer’s lock to the challenger
  • Transfer the paid dispute fees to the challenger
  • Unlock both the proposer and challenger locks
  • Execute onAcceptedDispute(intentId) on the disputable of the proposal

7. submitEvidence

function submitEvidence(uint256 disputeId, bytes evidenceURI) external

Parameters:

  • disputeId: Identification number of the dispute in the arbitrator
  • evidenceURI: Content-addressed link to human-readable text written by the sender

Authentication: Open

Action:

  • Ensure the dispute has not been solved yet
  • Log the evidence submitted by the sender for the corresponding dispute

Comments:

  • Even though anyone is allowed to submit evidence for a certain dispute, participants in charge of evaluating the dispute should filter these evidence by the addresses of their submitters

8. rule

function rule(uint256 disputeId, uint256 ruling) external

Parameters:

  • disputeId: Identification number of the dispute in the arbitrator
  • ruling: Numeric value (0, 1 or 2) that represents the final ruling decided by the arbitrator

Authentication: Only the arbitrator instance saved in dispute with id disputeId can call this function

Action:

  • Once the arbitrator has decided on a final ruling, anyone can call a function in the arbitrator that will result in rule being executed in ProposalAgreements.
  • Depending on ruling:
    • Invalid ruling (0): no jurors voted and no appeals happened.
      • Unlock both proposer and challenger locks (without redistribution)
      • Execute onRejectedDispute(intentId) on the disputable of the proposal (avoid locking organizations if jurors don’t vote)
    • Challenge rejected (1): the arbitrator ruled to dismiss the challenge.
      • Unlock proposer's lock
      • Transfer collateralAmount from the challenger’s lock to the proposer (within Staking)
      • Execute onRejectedDispute(intentId) on the disputable of the proposal
    • Challenge accepted (2): the arbitrator ruled to accept the challenge.
      • Unlock the challenge's lock
      • Transfer collateralAmount from the proposer's lock to the challenger (within staking)
      • Execute onAcceptedDispute(intentId) on the disputable of the proposal

9. canUnlock

function canUnlock(address account, uint256 lockId, bytes lockData) external view returns (bool)

Parameters:

  • account: Address that owns the locked tokens
  • lockId: Identification number of the queried lock (only unique per account)
  • lockData: Data that specifies the purpose of the lock

Authentication: Open

Action:

  • Return false if the given lockId is associated to a proposal that can still be challenged or if the lockId is associated to a dispute that has not been solved yet, false otherwise

Comments:

  • This function shouldn’t perform any state modifications and assume it is called with a STATICCALL.
  • Staking will perform calls to this function to check if the lockId can be unlocked and allow the account to move their tokens.
  • Given that proposers and challengers need to create locks before creating a proposal or challenging, a lock could be created by a user which then isn’t used (e.g. someone challenged before, they changed their mind, etc).

10. setCollateralToken

function setCollateralToken(address newCollateralToken) external

Parameters:

  • newCollateralToken: Address of the new token to be used for the collateral

Authentication: Only accounts with the CHANGE_COLLATERAL_TOKEN_ROLE can change the collateral token

Action:

  • Change the collateralToken state variable to the inputted newCollateralToken.

11. setCollateralAmount

function setCollateralAmount(uint256 newCollateralAmount) external

Parameters:

  • newCollateralAmount: New amount of collateralToken to be used for the proposals

Authentication: Only accounts with the CHANGE_COLLATERAL_AMOUNT_ROLE can change the collateral amount

Action:

  • Change the collateralAmount state variable to the inputted newCollateralAmount.

12. setArbitrator

function setArbitrator(IArbitrator newArbitrator) external

Parameters:

  • newArbitrator: Address of the new IArbitrator that will resolve disputes

Authentication: Only accounts with the CHANGE_ARBITRATOR_ROLE can change the arbitrator

Action:

  • Change the arbitrator state variable to the inputted newArbitrator

Comments:
All proposals that are challenged (challenge is called for a proposalId) from this point on must use this newArbitrator instance for disputes (until it is changed again).

13. setSubscriptionsVault

function setSubscriotionsVault(Vault newSubscriptionsVault) external

Parameters:

  • newSubscriptionsVault: Address of the new Vault used to pay for subscription fees

Authentication: Only accounts with the CHANGE_VAULT_ROLE can change the subscriptions vault

Action:

  • Change the subscriptionsVault state variable to the inputted newSubscriptionsVault.

14. setAgreementURI

function setAgreementURI(bytes newAgreementURI) external

Parameters:

  • newAgreementURI: New link to the human-readable text that describes what types of proposals are valid

Authentication: Only accounts with the CHANGE_AGREEMENT_URI_ROLE can change the agreement URI

Action:

  • Change the agreementURI state variable to the inputted newAgreementURI.

15. setDisputeConfirmPeriod

function setDisputeConfirmPeriod(uint256 newDisputeConfirmPeriod) external

Parameters:

  • newDisputeConfirmPeriod: New duration in seconds during which a challenge can be confirmed. If it’s not confirmed on time, it will be considered a settlement

Authentication: Only accounts with the CHANGE_DISPUTE_CONFIRM_PERIOD_ROLE can change the dispute confirm period value

Action:

  • Change the disputeConfirmPeriod state variable to the inputted newDisputeConfirmPeriod.

16. triggerSubscriptionPayment

function triggerSubscriptionPayment() external

Parameters: None

Authentication: Open

Action:

  • Transfer the required amount of subscription fee tokens to the subscriptions’ recipient from subscriptionsVault (arbitrator.getSubscriptionFees() )
  • Execute subscription payment in the returned address by arbitrator.getSubscriptionFees()

Appendix A

The alternatives listed below can be used to simplify part of the flow described by the functions of the ProposalAgreement interface. Although these require changing part of the stack already provided in aragonOS, we could consider implementing some changes to improve the user experience and reduce the number of transactions.

1. Forward with input

Having a way to forward actions with arbitrary input would allow creating proposals in one transaction and potentially allow creating challenges via forwarding as well (see issue https://github.com/aragon/aragon/issues/452).

We could use it to simplify the forward (create proposal) + stake + confirmProposal in one single transaction, making the ProposalAgreement instance creating the lock for us while avoiding all the checks to ensure that the locks have been created correctly for example.

2. Recurring payments

The subscriptionsVault reference and the triggerSubscriptionPayment could be avoided in case we add support to execute recurring payments in the Finance app. Having that, we could be allow to program in the Finance app the amount of money to be transferred periodically to a certain address to ensure the arbitrator subscriptions are always up-to-date.


Appendix B

The evidence submission, storage, and validation will be held in the IArbitrable instances, in this case, the ProposalAgreement ones. Instead of restricting the way evidence must be submitted in the IArbitrator, this can be done off-chain. In the end, there is no way to guarantee that parties or jurors won’t be exchanging information off-chain. The evidence can be managed at a UI level.

There will be only one evidence period per dispute, it will be a time window before the draft of the first round, that can be advanced in case both parties agree.

6 Likes