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 theIArbitrator
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 theIArbitrable
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 returnednextIntentId
by 1 or revert. In the case of Voting,nextIntentId
should return the same asvotesLength
.
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 ofVoting
, it will pause the vote with idintentId
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 ofVoting
, it will resume the vote with idintentId
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 ofVoting
, it will cancel the vote with idintentId
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 ofcollateralToken
that needs to be staked in order to forward a proposal or to challenge it -
arbitrator
: Address of theIArbitrator
that will be used to resolve disputes -
subscriptionsVault
: Address of aVault-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 aCallsScript
with just one call that creates an intent in anIDisputable
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 aCallsScript
with just one call that creates an intent in anIDisputable
instance) -
disputable
: Address of theIDisputable
instance where the intent is being created with thescript
-
lockId
: Identification number of the lock associated to the corresponding collateral amount inStaking
-
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 requiredcollateralAmount
ofcollateralToken
instaking
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)
.
- The lock manager needs to be set to the
Action:
The actual forwarding action has a tight coupling with the provided disputable
instance.
- Ensure that the
msg.sender
has a valid lock instaking
withlockId
:- 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
)
- The locked amount is
- Ensure that
H(script)
matches thescript
hash saved in the proposal. - Before executing the script, get the
nextIntentId
fromdisputable
and save it in the proposal object asintentId
- Execute the
script
. The script can only be aCallsScript
with only one call to avoid script execution from resolving disputes in adisputable
(this requires changes in aragonOSCallsScript
executor) - After the
script
has been executed, thenextIntentId
fordisputable
must have been increased by1
, revert otherwise
Comments:
- This way
ProposalAgreement
knows whatintentId
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 theCallsScript
can execute, we also prevent scripts from sending arbitrary calls todisputable
.
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 fromproposer
that the challenger would accept for resolving the dispute without involving thearbitrator
. If the dispute isn’t confirmed during thedisputeConfirmPeriod
, the settlement will be considered accepted. -
lockId
: Identification number of the lock created by thechallenger
instaking
-
evidenceURI
: Content-addressed link to human-readable text written by thechallenger
Authentication: Only accounts with the CHALLENGE_PROPOSALS_ROLE
can initiate a challenge for a proposal
Setup:
- Before calling
challenge
, thechallenger
must stake the requiredcollateralAmount
ofcollateralToken
instaking
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)
.
- The lock manager needs to be set to the
- On top of locking collateral, the challenger must pay half of the dispute fees for the first adjudication round. The
challenger
has to approveproposalAgreementAddress
to pull the amount of tokens required to pay half of thearbitrator
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 andcollateralAmount
- Ensure that
challenger
has valid a lock withlockId
:- 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
)
- The locked amount is
- 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 fromchallenger
, even if they are not themsg.sender
). - Save the current time as the
disputeChallengedTime
of the proposal - Execute
onDisputeCreation(intentId)
indisputable
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 theproposer
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()
) frommsg.sender
. If fees haven’t changed in thearbitrator
, 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. ThedisputeId
returned (andarbitrator
instance used) when creating the dispute needs to be stored in order to identifyIArbitrator
rulings (oneProposalAgreement
instance can have multiple ongoing disputes in differentIArbitrator
s 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
andchallenger
locks - Execute
onAcceptedDispute(intentId)
on thedisputable
of the proposal
7. submitEvidence
function submitEvidence(uint256 disputeId, bytes evidenceURI) external
Parameters:
-
disputeId
: Identification number of the dispute in thearbitrator
-
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 thearbitrator
-
ruling
: Numeric value (0
,1
or2
) that represents the final ruling decided by thearbitrator
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 thearbitrator
that will result inrule
being executed inProposalAgreements
. - Depending on
ruling
:- Invalid ruling (
0
): no jurors voted and no appeals happened.- Unlock both
proposer
andchallenger
locks (without redistribution) - Execute
onRejectedDispute(intentId)
on thedisputable
of the proposal (avoid locking organizations if jurors don’t vote)
- Unlock both
- Challenge rejected (
1
): thearbitrator
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 thedisputable
of the proposal
- Unlock
- Challenge accepted (
2
): thearbitrator
ruled to accept the challenge.- Unlock the
challenge
's lock - Transfer
collateralAmount
from theproposer
's lock to thechallenger
(withinstaking
) - Execute
onAcceptedDispute(intentId)
on thedisputable
of the proposal
- Unlock the
- Invalid ruling (
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 thelockId
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 theaccount
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 inputtednewCollateralToken
.
11. setCollateralAmount
function setCollateralAmount(uint256 newCollateralAmount) external
Parameters:
-
newCollateralAmount
: New amount ofcollateralToken
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 inputtednewCollateralAmount
.
12. setArbitrator
function setArbitrator(IArbitrator newArbitrator) external
Parameters:
-
newArbitrator
: Address of the newIArbitrator
that will resolve disputes
Authentication: Only accounts with the CHANGE_ARBITRATOR_ROLE
can change the arbitrator
Action:
- Change the
arbitrator
state variable to the inputtednewArbitrator
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 newVault
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 inputtednewSubscriptionsVault
.
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 inputtednewAgreementURI
.
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 inputtednewDisputeConfirmPeriod
.
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.