diff --git a/docs/interactions.md b/docs/interactions.md index 3de5342..ea0002d 100644 --- a/docs/interactions.md +++ b/docs/interactions.md @@ -1,43 +1,65 @@ -# Contract interactions, L2-governed design -### Edmund Edgar, 2023-04-13 +# Contract interactions +### Edmund Edgar, 2023-04-13, last updated 2024-02-23 -This document describes the interactions between actors (users and contracts) in the single-token L2-governed version of the BORG design. +This document describes the interactions between actors (users and contracts) in Backstop. -For simplicity some contract parameters are omitted. Details of the mechanism for sending messages between L1 and L2 will depend on the L2 mechanism. +For simplicity some contract parameters are omitted. See the tests under /tests for the exact parameters. + +### Notation + * L1. or L2. denotes the layer on which the token contract is deployed + * .L1Token denotes a token native to L1. .L2Token denotes a token native to L2. + * ...X denotes one of many versions (ie there may also be a "Y" and a "Z"). + * ...-1 and ...-2 denote multiple versions that exist after a fork. + * Alice, Bob are example users, each with a unique sender address (that may exist on both L1 and L2) ## Contracts: ### Tokens -* L1.GasToken: A normal Ethereum-native token, eg DAI or WETH. -* L2.GasToken: A representation of the L1.GasToken, bridged to it. -* L2.NativeGasToken: A forkable token native to L2. There may be many of these. -* L1.GovToken, aka L1.ForkManager: A dedicated governance token that can be forked on L1. There is only one per fork. In forks this must be committed to one fork or the other. Can also replace itself while preserving balances. + * L1.L1TokenX: A normal Ethereum-native token, eg DAI or WETH. + * L2.L1TokenX: A representation of the L1.SomeToken1, bridged to layer 2. + + * L2.L2TokenX: A forkable token issued on L2. There may be many of these. + * L1.L2TokenX: A representation of the L2.L2TokenX on L1. + + * L1.GovToken, An ERC20 token representing a dedicated governance token that can be forked on L1. There is only one per fork. + * L2.GovToken, The native token version of L1.GovToken bridged to L2. ### Reality.eth instances -* L2.Reality.eth: A normal ERC20-capable reality.eth instance on L2. Uses NativeGasToken. There may be other instances supporing other tokens. -* L1.Reality.eth: A forkable ERC20-capable reality.eth instance on L1, using GovToken for bonds. +* L2.RealityETH: A normal native reality.eth instance on L2. Uses L2.GovToken. There may be other instances supporing other tokens. This role could be played by a different escalation game contract. +* L1.ForkableRealityETH_ERC20: A forkable ERC20-capable reality.eth instance on L1, using GovToken for bonds. Changing this to a different escalation game contract would require a system upgrade. + +### Arbitrators +* L2.ArbitratorX: A reality.eth arbitrator instance on L2 +* L2.AdjudicationFrameworkRequests: A adjudication framework, ie a whitelist of arbitrators such as L2.ArbitratorX. "Requests" means it uses the pull pattern. +* L2.L2ForkArbitrator: An arbitrator contract used for escalations by the adjudication framework +* L1.L1ForkArbitrator: An arbitrator contract used for escalations in governance proposals. ### L1-L2 Bridges -* L2.BridgeToL1: A contract sending messages between ledgers. -* L1.BridgeToL2: -* L2.BridgeFromL1: -* L1.BridgeFromL2: +* L1.ForkableBridge: A bridge contract like zkEVMBridge but forkable and able to handle L2.GovToken as L2 the native token. +* L2.Bridge: A bridge like a normal zkEVMBridge, but with gas token handling. Does not need to be forkable, but may be to avoid deploying multiple contracts. -ForkArbitrator +### Admin contract +* L1.ForkableRollupGovernor: A contract with the permission to upgrade the L1 system. -## Operations +### Forkable system infrastructure +* L2.L2ChainInfo: A contract on L2 that reports information about which chain it is on, what it forked over etc. +* L1.L1GlobalChainInfoPublisher: A single contract (usable by all forks) used to send information about the fork to L2.L2ChainInfo. +### Example contracts +* L2.CrowdfunderX is an example of a crowdfunding contract you might deploy. We use this a general example of a contract that needs subjectivocratically-secure adjudication. -### Make a crowdfund +## Operations + +### Make a crowdfund payable if Bob completes a task ``` - Alice L2 question_id = RealityETH.askQuestion(recipient=Bob, arbitrator=AdjudicationFramework) - Alice L2 Crowdfunder.createCrowdFund(question_id, value=1234) + Alice L2.question_id = RealityETH.askQuestion(question="Did Bob finish his job?", arbitrator=AdjudicationFrameworkRequests) + Alice L2.CrowdfunderX.createCrowdFund(question_id, value=1234) ``` ### Report an answer (uncontested) ``` - Bob L2 RealityETH.submitAnswer(question_id, 100, value=100) + Bob L2.RealityETH.submitAnswer(question_id, 100, value=100) ``` Next step: @@ -46,36 +68,36 @@ Next step: ### Claim a payout ``` - Bob L2 RealityETH.claimWinnings(question_id) + Bob L2.RealityETH.claimWinnings(question_id) ``` ### Settle a crowdfund ``` - Bob L2 Crowdfunder.payOut(question_id) + Bob L2.Crowdfunder.payOut(question_id) RealityETH.resultFor(question_id) -> pays Bob ``` ### Report an answer (contested) ``` - Bob L2 RealityETH.submitAnswer(question_id, value=100) - Charlie L2 RealityETH.submitAnswer(question_id, value=200) - Bob L2 RealityETH.submitAnswer(question_id, value=400) - Charlie L2 RealityETH.submitAnswer(question_id, value=2000000) + Bob L2.RealityETH.submitAnswer(question_id, value=100) + Charlie L2.RealityETH.submitAnswer(question_id, value=200) + Bob L2.RealityETH.submitAnswer(question_id, value=400) + Charlie L2.RealityETH.submitAnswer(question_id, value=2000000) ``` ### Contest an answer ``` - Bob L2 AdjudicationFramework.requestArbitration(question_id, value=1000000) + Bob L2.AdjudicationFrameworkRequests.requestArbitration(question_id, value=1000000) ``` ### Handle an arbitration ``` - Dave L2 ArbitratorA.requestArbitration(question_id, value=500000) - AdjudicationFramework.notifyOfArbitrationRequest(question_id, Dave) + Dave L2.ArbitratorA.requestArbitration(question_id, value=500000) + AdjudicationFrameworkRequests.notifyOfArbitrationRequest(question_id, Dave) - Arby L2 ArbitratorA.submitAnswerByArbitrator(question_id, 1, Dave) - AdjudicationFramework.submitAnswerByArbitrator(question_id, 1, Bob) + Arby L2.ArbitratorA.submitAnswerByArbitrator(question_id, 1, Dave) + AdjudicationFrameworkRequests.submitAnswerByArbitrator(question_id, 1, Bob) ``` Next step: * Uncontested arbitration after 1 week? [Execute an arbitration](#execute-an-arbitration) @@ -83,22 +105,23 @@ Next step: ### Execute an arbitration ``` - Dave L2 AdjudicationFramework.executeArbitration(question_id) - NativeGasToken.transfer(Dave, 1000000) - RealityETH.submitAnswerByArbitrator(question_id, 1, Bob) + Dave L2.AdjudicationFrameworkRequests.executeArbitration(question_id) + RealityETH.submitAnswerByArbitrator(question_id, 1, Bob, value=1000000) ``` Next step: -* Arbitration contested? [Contest an arbitration](#contest-an-arbitration) -* Arbitration uncontested? [Settle a crowdfund](#settle-a-crowdfund) + * Arbitration contested? [Contest an arbitration](#contest-an-arbitration) + * Arbitration uncontested? [Settle a crowdfund](#settle-a-crowdfund) -### Contest an arbitration. (An AdjudicationFramework could do something different here.) +### Contest an arbitration. (An AdjudicationFrameworkRequests could do something different here.) ``` - Charlie L2 AdjudicationFramework.beginRemoveArbitrator(address arbitrator_to_remove) - contest_question_id = RealityETH.askQuestion("should we delist ArbitratorA?") - Charlie L2 RealityETH.submitAnswer(contest_question_id, 1, value=2000000) - Charlie L2 AdjudicationFramework.freezeArbitrator(contest_question_id) + Charlie L2.AdjudicationFrameworkRequests.beginRemoveArbitrator(address arbitrator_to_remove) + contest_question_id = RealityETH.askQuestion("should we delist ArbitratorA?", arbitrator=L2ForkArbitrator) + Charlie L2.RealityETH.submitAnswer(contest_question_id, 1, value=2000000) + Charlie L2.AdjudicationFrameworkRequests.freezeArbitrator(contest_question_id, ...) RealityETH.getBestAnswer(contest_question_id) RealityETH.getBond(contest_question_id) + (or if someone else added an answer provide the history and then do) + RealityETH.isHistoryOfUnfinalizedQuestionValid(contest_question_id, ...) ``` Next step: * Delist question finalizes as 1? [Execute an arbitrator removal](#execute-an-arbitrator-removal) @@ -107,7 +130,7 @@ Next step: ### Cancel an arbitrator removal ``` - Bob L2 ForkManager.unfreezeArbitrator(contest_question_id) + Bob L2.ForkingManager.clearFailedProposition(contest_question_id) RealityETH.resultFor(contest_question_id) ``` @@ -116,7 +139,7 @@ Next step: ### Execute an arbitrator removal ``` - Charlie L2 AdjudicationFramework.removeArbitrator(contest_question_id) + Charlie L2.AdjudicationFrameworkRequests.executeModificationArbitratorFromAllowList(contest_question_id) RealityETH.resultFor(contest_question_id) ``` Next step: @@ -124,9 +147,9 @@ Next step: ### Propose an arbitrator addition ``` - Charlie L2 ForkArbitrator.beginAddArbitratorToAllowlist(whitelist_arbitrator, ArbitratorA) - contest_question_id = RealityETH.askQuestion("should we add ArbitratorA to AdjudicationFramework?") - Charlie L2 RealityETH.submitAnswer(contest_question_id, 1, value=2000000) + Charlie L2.AdjudicationFrameworkRequests.requestModificationOfArbitrators(0, ArbitratorA) + contest_question_id = RealityETH.askQuestion("should we add ArbitratorA to AdjudicationFrameworkRequests?") + Charlie L2.RealityETH.submitAnswer(contest_question_id, 1, value=2000000) ``` Next step: * [Execute an arbitrator addition](#execute-an-arbitrator-addition) if it finalizes as 1 @@ -135,61 +158,88 @@ Next step: ### Execute an arbitrator addition ``` - Charlie L2 RealityETH.finalizeQuestion(add_question_id) - Charlie L2 ForkArbitrator.addArbitrator(add_question_id) + Charlie L2.ForkArbitrator.executeModificationArbitratorFromAllowList(add_question_id) RealityETH.resultFor(add_question_id) ``` +### Challenge an arbitrator addition or removal +``` + Bob L2.L2ForkArbitrator.requestArbitration(contest_question_id, uint256 max_previous, ...) + # Marks this question done and freezes everything else + L2.RealityETH.notifyOfArbitrationRequest(contest_question_id, msg.sender, max_previous, value=999999); + + # Optionally the L2ForkArbitrator waits for a delay specified by the AdjudicationFramework -[consumer] calls -> [reality.eth] requests arbitration from [adjudicationframework] request fulfillment from -> [arbitrator] (above) -[objector] calls -> [adjudicationframework] calls -> [reality.eth] requests arbitration from -> - [forkarbitrator] -> - - informs [reality.eth], freezing question - - informs [adjudicationframework], freezing arbitrator (also possible during escalation) - - calls L1.ForkingManager, transferring fee + [bot] L2.L2ForkArbitrator.requestActivateFork(...) + AdjudicationFrameworkRequests.getInvestigationDelay() # Check the delay has passed + ChainInfo.getForkFee() # Make sure the fee is high enough + # Calculates a moneyBox address of a contract representing this contract asking about this question + BridgeFromL2.bridgeAsset(forkFee, moneyBox) +``` +Next step +* [Execute a L2-created fork] or [Refund a failed L2-created fork attempt] + +### Execute a L2-created fork + +``` + [bot] L1.ForkableBridge.claimAsset(...) + + [bot] L1.L1GlobalForkRequester.handlePayment(...) + L1.ForkonomicToken.transferFrom(moneyBox, this) + L1.ForkonomicToken.approve(forkManager) + L1.forkManager.initiateFork(...) + L1.ForkonomicToken.transferFrom(L1GlobalForkRequester, fee) + # Assign the Chain ID for each chain + ChainIdManager.getNextUsableChainId() + ChainIdManager.getNextUsableChainId() + # TODO: createIncentivizedMarket() + + [bot] L1.ForkingManager.executeFork() + L1.ForkableBridge.createChildren() + L1.ForkableZkEVM.createChildren() + L1.ForkableGlobalExitRoot.createChildren() + L1.ForkonomicToken.createChildren() -### Challenge an arbitrator addition or removal ``` - Bob L2 ForkArbitrator.requestArbitration(contest_question_id, uint256 max_previous, ...) - # Marks this question done and freezes everything else - RealityETH.notifyOfArbitrationRequest(contest_question_id, msg.sender, max_previous, value=999999); - # TODO: Do we need to tell the AdjudicationFramework about this? Guess not as the freezing can be handled based on reality.eth bonds +### Refund a failed L2-created fork attempt - # TODO: If the contracts that can requestFork() are permissioned, this needs to be managed by some intermediate contract, or by the ForkingManager having its own list - BridgeFromL2.sendMessage(ForkingManager.requestFork(value=999999)) +If another fork has been started simultaneously or the fee was too low, the fork may not be possible. - [bot] L1 ForkManager.startFork(value=999999) - L2.IncentivizedMarket = createIncentivizedMarket() + [bot] L1.ForkableBridge.claimAsset(...) - Bob L1 ForkManager.deployFork(false, contested question data) - # Clones ForkManager - # Clones Bridge - # Copies contested question to child RealityETH + [bot] L1.L1GlobalForkRequester.handlePayment(...) + L1.returnTokens(...) + L1.ForkableBridge.bridgeAsset(L2.L2ForkArbitrator) + L1.ForkableBridge.bridgeMessage(L2.L2ForkArbitrator, ...) + + [bot] + L2.ForkableBridge.claimAsset() + L2.ForkableBridge.claimMessage() + L2.L2ForkArbitrator.onMessageReceived() + L2.RealityETH.cancelArbitration() - Charlie L1 ForkManager.deployFork(true, contested question data) - # Clones ForkManager - # Clones Bridge - # Copies contested question to child RealityETH + [Bob] L2.L2ForkArbitrator.claimRefund() # Transfers native token back ``` -Next step: -* Wait for the fork date, then anyone can [Execute an arbitrator removal](#execute-an-arbitrator-removal) on one chain and [Cancel an arbitrator removal](#cancel-an-arbitrator-removal) on the other. +#### Next step: + + * Wait for the fork date, then anyone can [Execute an arbitrator removal](#execute-an-arbitrator-removal) on one chain and [Cancel an arbitrator removal](#cancel-an-arbitrator-removal) on the other. -### Bid in the auction +### Bid in the auction [TODO: Build this] ``` - Bob L2 IncentivizedMarket.bid(uint256 yes_price_percent, value=tokens) + Bob L2. IncentivizedMarket.bid(uint256 yes_price_percent, value=tokens) # burns own tokens # fork.mint(msg.sender, tokens) # Wait 1 week IncentivizedMarket.calculateClearingPrice() - L2 .getYesNo() + L2. .getYesNo() withdraw() # check clearing price # decide which side the user is on by whether their price is above or below the clearing percent @@ -210,34 +260,24 @@ Next step: ### Propose a routine governance change ``` - Charlie L1 ForkManager.beginUpgradeBridge - gov_question_id = RealityETH.askQuestion("should we do a routine upgrade to ForkManager XYZ?") + Charlie L1.ForkableRollupGovernor.beginProposition + gov_question_id = RealityETH.askQuestion("should we execute the bytecode XYZ against contract ABC?") - Charlie L1 TokenX.approve(RealityETH, 2000000) - Charlie L1 RealityETH.submitAnswer(gov_question_id, 1, 2000000) + Charlie L1.GovToken.approve(RealityETH, 2000000) + Charlie L1.RealityETH.submitAnswer(gov_question_id, 1, 2000000) ``` Next step: * Upgrade question finalizes as 1? [Execute a governance change](#execute-a-governance-change) * Upgrade question finalizes as 0? No need to do anything -* May be contested: [Challenge an arbitration result](#challenge-an-arbitration-or-governance-result) - -### Finalize a completed governance change -``` - Charlie L1 RealityETH.finalizeQuestion(gov_question_id) -``` -Next step: -* [Execute a governance change](execute-a-governance-change) - -NB On the normal non-forkable version of Reality.eth finalization happens automatically without a transaction. -It's its own transaction on the forkable version because forking for one question affects whether others can finalize, even within the same timestamp. +* May be contested: [Challenge a governance result](#challenge-a-governance-result) -### Propose an urgent governance change +### Propose an urgent governance change - TODO check this ``` - Charlie L1 ForkManager.beginUpgradeBridge - gov_question_id = RealityETH.askQuestion("should we freeze exits and upgrade to ForkManager XYZ?") - Charlie L1 TokenX.approve(RealityETH, 2000000) - Charlie L1 RealityETH.submitAnswer(gov_question_id, 1, 2000000) - Charlie L1 ForkManager.freezeBridges(gov_question_id) + Charlie L1.ForkableRollupGovernor.beginProposition + gov_question_id = RealityETH.askQuestion("should we freeze exits and upgrade to ForkingManager XYZ?") + Charlie L1.GovToken.approve(realityETH, 2000000) + Charlie L1.RealityETH.submitAnswer(gov_question_id, 1, 2000000) + Charlie L1.ForkingManager.freezeBridges(gov_question_id) RealityETH.getBestAnswer(gov_question_id) RealityETH.getBond(gov_question_id) # Update self to say there are no available bridges @@ -248,64 +288,63 @@ It's its own transaction on the forkable version because forking for one questio ### Execute a governance change ``` - Charlie L1 ForkManager.executeBridgeUpdate(gov_question_id) - RealityETH.resultFor(gov_question_id) - # Update to reflect child forkmanager - # Has the effect of unfreezing bridges, may be new bridges + Charlie L1.ForkableRollupGovernor.executeProposition(gov_question_id, to, bytecode) + RealityETH.resultFor(gov_question_id) + call(to, bytecode) + (TODO: Handle freezing of bridges etc if the proposition is urgent and there is a high bond to that effect) ``` -### Clear a failed urgent governance proposal -``` - Bob L1 ForkManager.clearFailedGovernanceProposal(contest_question_id) - RealityETH.resultFor(contest_question_id) - # Update self to say the previous bridge is back in action +### Moving gov tokens L2->L1 ``` + Alice L2.Bridge.bridgeAsset(value=123) -### Return funds from a governance proposition or arbitrator change proposition when we fork over a different proposition + Alice L1.ForkableBridge.claimAsset() # mints erc20 value + L1.GovToken.mint(123) ``` - Bob L1 RealityETH.refund(history) - GovToken.transfer(Bob, 100) - GovToken.transfer(Charlie, 200) + +### Moving gov tokens L1->L2 ``` + Alice L1.ForkableBridge + GovToken.approve(L2.Bridge, 123) + L2.Bridge.bridgeAsset(123) [checks to make sure we're not already forked] -Next step: -* If the question is still relevant it can be begun again on either chain or both. + Alice L2.Bridge.claimAsset() +``` +### Moving L2-native ERC20 tokens L2->L1 +``` + Alice L2.L2TokenX.approve(L2.Bridge, 123) + L2.Bridge.bridgeAsset(L2TokenX, 123) -### Moving tokens to L2 + Alice L2.ForkableBridge.claimAsset() # + L1.L2TokenX.mint(Alice, 123) ``` - Alice L1 - GasToken.approve(GasTokenWrapper, 123) - GasTokenWrapper.sendToL2(123) - ForkManager.requiredBridges() - # for each bridge, usually 1 but during forks there are 2 - BridgeToL2.sendMessage("mint(Alice, 123)") - [bot] L2 BridgeFromL2.processQueue() # or similar - GasToken.mint(Alice, 123) +### Moving L2-native ERC20 tokens L1->L2 ``` + Alice L1.L2TokenX.approve(L2.Bridge, 123) + L1.ForkableBridge.bridgeAsset(L2TokenX, 123) [ checks to make sure we're not already forked ] -### Unlocking tokens on L1 + Alice L2.Bridge.claimAsset() + L2.L2TokenX.mint(Alice, 123) ``` - Bob L1 GasToken.sendToL1(123) - BridgeToL1.sendMessage("GasTokenWrapper.mint(Bob, 123")) - [bot] L1 BridgeFromL2.processQueue() or similar - GasTokenWrapper.receiveFromL2(Bob, 123) - ForkManager.requiredBridges() - # If we the transfer cannot be completed, we queue the message. - # This happens if need to hear from 2 bridges or wait for something to be updated/unfrozen - GasToken.transfer(Bob, 123) +### Moving L1-native tokens (ETH) L1->L2 ``` + Alice L1.L1TokenXWrapperWithForkChoice.approve(L1.Bridge, 123) + L1.ForkableBridge.bridgeAsset(L1TokenWrapperWithForkChoice, 123) -### Completing a move from L2 that resulted in a queued message because of a fork or governance freeze + Alice L2.Bridge.claimAsset() + L2.L1TokenX.mint(Alice, 123) - Bob L1 GasTokenWrapper.retryMessage(Bob, 123, bridge_contract) - ForkManager.requiredBridges() - GasToken.transfer(Bob, 123) +``` -### Notifying a token bridge after a fork +### Moving L1-native tokens (ETH) L2->L1 +``` + Alice L2.L1TokenWrapperWithForkChoice.approve(L1.Bridge, 123) # TODO: Make the L1TokenWrapperWithForkChoice + L2.ForkableBridge.bridgeAsset(L1TokenWrapperWithForkChoice, 123) - Alice L1 GasTokenWrapper.updateForkManager() - ForkManager.replacedByForkManager() + Alice L2.Bridge.claimAsset() TODO: Check this part + L1.L1TokenWrapperWithForkChoice.mint() +```