diff --git a/contracts/L1GlobalForkRequester.sol b/contracts/L1GlobalForkRequester.sol index eb6a1097..cb559578 100644 --- a/contracts/L1GlobalForkRequester.sol +++ b/contracts/L1GlobalForkRequester.sol @@ -26,61 +26,70 @@ import {IBridgeMessageReceiver} from "@RealityETH/zkevm-contracts/contracts/inte import {MoneyBox} from "./MoneyBox.sol"; import {MoneyBoxUser} from "./MoneyBoxUser.sol"; -contract L1GlobalForkRequester is IBridgeMessageReceiver, MoneyBoxUser { +contract L1GlobalForkRequester is MoneyBoxUser { struct FailedForkRequest { uint256 amount; - bool migratedY; - bool migratedN; + uint256 amountMigratedY; + uint256 amountMigratedN; } - // Token => Requester => ID => FailedForkRequest + // Token => Beneficiary => ID => FailedForkRequest mapping(address=>mapping(address=>mapping(bytes32=>FailedForkRequest))) public failedRequests; - // Any bridge (or any contract pretending to be a bridge) can call this. - // We'll look up its ForkingManager and ask it for a fork. - // TODO: It might make more sense if this contract requested the chain ID then kept a record of it. - // ...then the ForkingManager would be locked to only fork based on our request. + // Anybody can say, "Hey, you got a payment for this fork to happen" + // Normally this would only happen if the L2 contract send a payment but in theory someone else could fund it directly on L1. + function handlePayment(address token, address beneficiary, bytes32 requestId) external { - function onMessageReceived(address _originAddress, uint32 _originNetwork, bytes memory _data) external payable { + // Normally the "beneficiary" would be the sender on L2. + // But if some other kind person sent funds to this address we would still send it back to the beneficiary. - ForkableBridge bridge = ForkableBridge(msg.sender); - IForkingManager forkingManager = IForkingManager(bridge.forkmanager()); - IForkonomicToken forkonomicToken = IForkonomicToken(forkingManager.forkonomicToken()); + bytes32 salt = keccak256(abi.encodePacked(beneficiary, requestId)); - // The chain ID should always be the chain ID expected by the ForkingManager. - // It shouldn't be possible for it to be anything else but we'll check it anyhow. - IPolygonZkEVM zkevm = IPolygonZkEVM(forkingManager.zkEVM()); - require(uint64(_originNetwork) == zkevm.chainID(), "Bad _originNetwork, WTF"); + // Check the MoneyBox has funds + address moneyBox = _calculateMoneyBoxAddress(address(this), salt, token); - // We also check in the opposite direction to make sure the ForkingManager thinks the bridge is its bridge etc - require(address(forkingManager.bridge()) == msg.sender, "Bridge mismatch, WTF"); - require(address(forkonomicToken.forkmanager()) == address(forkingManager), "Token/manager mismatch, WTF"); + // If for some reason we've already got part of a payment, include it. + uint256 initialBalance = failedRequests[token][beneficiary][requestId].amount; - // TODO: Check if we should have anything else identifying the transfer - bytes32 salt = keccak256(abi.encodePacked(msg.sender)); - address moneyBox = _calculateMoneyBoxAddress(address(this), salt, address(forkonomicToken)); - uint256 transferredBalance = forkonomicToken.balanceOf(moneyBox); + uint256 transferredBalance = initialBalance + IForkonomicToken(token).balanceOf(moneyBox); if (moneyBox.code.length == 0) { - new MoneyBox{salt: salt}(address(forkonomicToken)); + new MoneyBox{salt: salt}(token); } - require(forkonomicToken.transferFrom(moneyBox, address(this), transferredBalance), "Preparing payment failed"); + require(IForkonomicToken(token).transferFrom(moneyBox, address(this), transferredBalance), "Preparing payment failed"); + + // If the token is already being or has already been forked, record the request as failed. + // Somebody can split the token after the fork, then send the failure message and the funds back on both the child forks. + // TODO: Replace this with an isForked() method in ForkingStructure.sol? - bool canFork = false; // TODO: Get this from the ForkingManager + IForkingManager forkingManager = IForkingManager(IForkonomicToken(token).forkmanager()); + + bool isForkGuaranteedNotToRevert = true; + if (transferredBalance < forkingManager.arbitrationFee()) { + isForkGuaranteedNotToRevert = false; + } + if (!forkingManager.canFork()) { + isForkGuaranteedNotToRevert = false; + } - if (canFork) { - forkonomicToken.approve(address(forkingManager), transferredBalance); + if (isForkGuaranteedNotToRevert) { + + if (initialBalance > 0) { + delete(failedRequests[token][beneficiary][requestId]); + } + + IForkonomicToken(token).approve(address(forkingManager), transferredBalance); // Assume the data contains the questionId and pass it directly to the forkmanager in the fork request IForkingManager.NewImplementations memory newImplementations; - IForkingManager.DisputeData memory disputeData = IForkingManager.DisputeData(false, _originAddress, bytes32(_data)); + IForkingManager.DisputeData memory disputeData = IForkingManager.DisputeData(false, beneficiary, requestId); forkingManager.initiateFork(disputeData, newImplementations); } else { // Store the request so we can return the tokens across the bridge // If the fork has already happened we may have to split them first and do this twice. - failedRequests[address(forkonomicToken)][msg.sender][salt] = FailedForkRequest(transferredBalance, false, false); + failedRequests[token][beneficiary][requestId].amount = transferredBalance; } @@ -92,40 +101,42 @@ contract L1GlobalForkRequester is IBridgeMessageReceiver, MoneyBoxUser { // TODO: We need to update ForkonomicToken to handle each side separately in case one bridge reverts maliciously. // Then handle only one side being requested, or only one side being left uint256 amount = failedRequests[token][requester][requestId].amount; - require(amount > 0, "Nothing to split"); + + // You need to call registerPayment before you call this. + require(amount > 0, "Nothing to split"); (address yesToken, address noToken) = IForkonomicToken(token).getChildren(); require(yesToken != address(0) && noToken != address(0), "Token not forked"); - // Current version with a single function + // Current version only has a single function so we have to migrate both sides IForkonomicToken(token).splitTokensIntoChildTokens(amount); - failedRequests[yesToken][requester][requestId] = FailedForkRequest(amount, false, false); - failedRequests[noToken][requester][requestId] = FailedForkRequest(amount, false, false); + failedRequests[yesToken][requester][requestId].amount += amount; + failedRequests[noToken][requester][requestId].amount += amount; delete(failedRequests[token][requester][requestId]); /* - // Should probably be something like: + // Probably need something like: - bool migratedY = failedRequests[token][requester][requestId].migratedY; - bool migratedN = failedRequests[token][requester][requestId].migratedN; + uint256 amountRemainingY = amount - failedRequests[token][requester][requestId].amountMigratedY; + uint256 amountRemainingN = amount - failedRequests[token][requester][requestId].amountMigratedN; if (doYesToken) { - require(!migratedY, "Already migrated Y"); - token.splitTokensIntoChildTokens(uint256 amount, 1); - migratedY = true; + require(amountRemainingY > 0, "Nothing to migrate for Y"); + token.splitTokensIntoChildTokens(amountRemainingY, 1); + amountRemainingY = 0; } if (doNoToken) { - require(!migratedN, "Already migrated N"); - token.splitTokensIntoChildTokens(uint256 amount, 0); - migratedN = true; + require(amountMigratedN > 0, "Nothing to migrate for N"); + token.splitTokensIntoChildTokens(amountMigratedN, 0); + amountRemainingN = 0; } - if (migratedY && migratedN) { + if (amountRemainingY == 0 && amountRemainingN == 0) { delete(failedRequests[token][requester][requestId]); } else { - failedRequests[token][requester][requestId] = migratedY; - failedRequests[token][requester][requestId] = migratedN; + failedRequests[token][requester][requestId].amountRemainingY = amountRemainingY; + failedRequests[token][requester][requestId].amountRemainingN = amountRemainingN; } */ } @@ -162,7 +173,7 @@ contract L1GlobalForkRequester is IBridgeMessageReceiver, MoneyBoxUser { bridge.bridgeMessage( uint32(chainId), requester, - true, + false, data ); diff --git a/contracts/L2ForkArbitrator.sol b/contracts/L2ForkArbitrator.sol index 94bff2e1..d6d7936a 100644 --- a/contracts/L2ForkArbitrator.sol +++ b/contracts/L2ForkArbitrator.sol @@ -131,7 +131,7 @@ contract L2ForkArbitrator is MoneyBoxUser, IBridgeMessageReceiver { // To differentiate our payment, we will use a dedicated MoneyBox contract controlled by l1globalForkRequester // The L1GlobalForkRequester will deploy this as and when it's needed. // TODO: For now we assume only 1 request is in-flight at a time. If there might be more, differentiate them in the salt. - bytes32 salt = keccak256(abi.encodePacked(address(this))); + bytes32 salt = keccak256(abi.encodePacked(address(this), question_id)); address moneyBox = _calculateMoneyBoxAddress(address(l1globalForkRequester), salt, address(forkonomicToken)); bytes memory permitData; @@ -144,13 +144,6 @@ contract L2ForkArbitrator is MoneyBoxUser, IBridgeMessageReceiver { permitData ); - bytes memory qdata = bytes.concat(question_id); - bridge.bridgeMessage( - uint32(chainInfo.originNetwork()), - address(l1globalForkRequester), - true, - qdata - ); isForkInProgress = true; } diff --git a/test/L1GlobalForkRequester.t.sol b/test/L1GlobalForkRequester.t.sol new file mode 100644 index 00000000..e99bc08f --- /dev/null +++ b/test/L1GlobalForkRequester.t.sol @@ -0,0 +1,347 @@ +pragma solidity ^0.8.20; + +/* solhint-disable not-rely-on-time */ + +import {Test} from "forge-std/Test.sol"; +import {ForkingManager} from "../contracts/ForkingManager.sol"; +import {ForkableBridge} from "../contracts/ForkableBridge.sol"; +import {ForkableZkEVM} from "../contracts/ForkableZkEVM.sol"; +import {ForkonomicToken} from "../contracts/ForkonomicToken.sol"; +import {ForkableGlobalExitRoot} from "../contracts/ForkableGlobalExitRoot.sol"; +import {IBasePolygonZkEVMGlobalExitRoot} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMGlobalExitRoot.sol"; +import {IForkingManager} from "../contracts/interfaces/IForkingManager.sol"; +import {IVerifierRollup} from "@RealityETH/zkevm-contracts/contracts/interfaces/IVerifierRollup.sol"; +import {IPolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {PolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/inheritedMainContracts/PolygonZkEVMBridge.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC20Upgradeable.sol"; +import {IPolygonZkEVM} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVM.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ChainIdManager} from "../contracts/ChainIdManager.sol"; +import {ForkableZkEVM} from "../contracts/ForkableZkEVM.sol"; + +import {L1GlobalForkRequester} from "../contracts/L1GlobalForkRequester.sol"; +import {ExampleMoneyBoxUser} from "./testcontract/ExampleMoneyBoxUser.sol"; + +contract L1GlobalForkRequesterTest is Test { + + L1GlobalForkRequester public l1GlobalForkRequester = new L1GlobalForkRequester(); + + ForkableBridge public bridge; + ForkonomicToken public forkonomicToken; + ForkingManager public forkmanager; + ForkableZkEVM public zkevm; + ForkableGlobalExitRoot public globalExitRoot; + + + address public bridgeImplementation; + address public forkmanagerImplementation; + address public zkevmImplementation; + address public forkonomicTokenImplementation; + address public globalExitRootImplementation; + address public chainIdManager; + bytes32 internal constant _IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + IBasePolygonZkEVMGlobalExitRoot public globalExitMock = + IBasePolygonZkEVMGlobalExitRoot( + 0x1234567890123456789012345678901234567892 + ); + bytes32 public genesisRoot = + bytes32( + 0x827a9240c96ccb855e4943cc9bc49a50b1e91ba087007441a1ae5f9df8d1c57c + ); + uint64 public forkID = 3; + uint64 public newForkID = 4; + uint64 public chainID = 4; + uint32 public networkID = 10; + uint64 public pendingStateTimeout = 123; + uint64 public trustedAggregatorTimeout = 124235; + address public hardAssetManger = + address(0x1234567890123456789012345678901234567891); + address public trustedSequencer = + address(0x1234567890123456789012345678901234567899); + address public trustedAggregator = + address(0x1234567890123456789012345678901234567898); + IVerifierRollup public rollupVerifierMock = + IVerifierRollup(0x1234567890123456789012345678901234567893); + uint256 public arbitrationFee = 1020; + bytes32[32] public depositTree; + address public admin = address(0xad); + uint64 public initialChainId = 1; + uint64 public firstChainId = 1; + uint64 public secondChainId = 2; + + // Setup new implementations for the fork + address public newBridgeImplementation = address(new ForkableBridge()); + address public newForkmanagerImplementation = address(new ForkingManager()); + address public newZkevmImplementation = address(new ForkableZkEVM()); + address public newVerifierImplementation = + address(0x1234567890123456789012345678901234567894); + address public newGlobalExitRootImplementation = + address(new ForkableGlobalExitRoot()); + address public newForkonomicTokenImplementation = + address(new ForkonomicToken()); + address public disputeContract = + address(0x1234567890123456789012345678901234567894); + bytes32 public disputeContent = "0x34567890129"; + bool public isL1 = true; + + event Transfer(address indexed from, address indexed to, uint256 tokenId); + + function bytesToAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b))); + } + + // TODO: This setup is duplicated with the ForkingManager tests + // It might be good to pull it out somewhere. + function setUp() public { + bridgeImplementation = address(new ForkableBridge()); + bridge = ForkableBridge( + address( + new TransparentUpgradeableProxy(bridgeImplementation, admin, "") + ) + ); + forkmanagerImplementation = address(new ForkingManager()); + forkmanager = ForkingManager( + address( + new TransparentUpgradeableProxy( + forkmanagerImplementation, + admin, + "" + ) + ) + ); + zkevmImplementation = address(new ForkableZkEVM()); + zkevm = ForkableZkEVM( + address( + new TransparentUpgradeableProxy(zkevmImplementation, admin, "") + ) + ); + forkonomicTokenImplementation = address(new ForkonomicToken()); + forkonomicToken = ForkonomicToken( + address( + new TransparentUpgradeableProxy( + forkonomicTokenImplementation, + admin, + "" + ) + ) + ); + globalExitRootImplementation = address(new ForkableGlobalExitRoot()); + globalExitRoot = ForkableGlobalExitRoot( + address( + new TransparentUpgradeableProxy( + globalExitRootImplementation, + admin, + "" + ) + ) + ); + + chainIdManager = address(new ChainIdManager(initialChainId)); + globalExitRoot.initialize( + address(forkmanager), + address(0x0), + address(zkevm), + address(bridge) + ); + bridge.initialize( + address(forkmanager), + address(0x0), + networkID, + globalExitMock, + address(zkevm), + address(forkonomicToken), + false, + hardAssetManger, + 0, + depositTree + ); + + IPolygonZkEVM.InitializePackedParameters + memory initializePackedParameters = IPolygonZkEVM + .InitializePackedParameters({ + admin: admin, + trustedSequencer: trustedSequencer, + pendingStateTimeout: pendingStateTimeout, + trustedAggregator: trustedAggregator, + trustedAggregatorTimeout: trustedAggregatorTimeout, + chainID: chainID, + forkID: forkID + }); + zkevm.initialize( + address(forkmanager), + address(0x0), + initializePackedParameters, + genesisRoot, + "trustedSequencerURL", + "test network", + "0.0.1", + globalExitRoot, + IERC20Upgradeable(address(forkonomicToken)), + rollupVerifierMock, + IPolygonZkEVMBridge(address(bridge)) + ); + forkmanager.initialize( + address(zkevm), + address(bridge), + address(forkonomicToken), + address(0x0), + address(globalExitRoot), + arbitrationFee, + chainIdManager + ); + forkonomicToken.initialize( + address(forkmanager), + address(0x0), + address(this), + "Fork", + "FORK" + ); + } + + function testReceivePayment() public { + + uint256 fee = forkmanager.arbitrationFee(); + + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + // Receive a payment from a MoneyBox + + address l2requester = address(0xbabe01); + bytes32 requestId = bytes32("0xc0ffee01"); + bytes32 salt = keccak256(abi.encodePacked(l2requester, requestId)); + address moneyBoxAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(address(l1GlobalForkRequester), salt, address(forkonomicToken)); + + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + + vm.prank(address(this)); + forkonomicToken.transfer(moneyBoxAddress, fee); + + assertEq(address(forkmanager.forkonomicToken()), address(forkonomicToken)); + assertTrue(forkmanager.canFork()); + assertFalse(forkmanager.isForkingInitiated()); + assertFalse(forkmanager.isForkingExecuted()); + + l1GlobalForkRequester.handlePayment(address(forkonomicToken), l2requester, requestId); + + assertTrue(forkmanager.isForkingInitiated()); + assertFalse(forkmanager.isForkingExecuted()); + + } + + function testReceiveInsufficientPayment() public { + + uint256 fee = forkmanager.arbitrationFee() - 1; + + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + // Receive a payment from a MoneyBox + + address l2requester = address(0xbabe01); + bytes32 requestId = bytes32("0xc0ffee01"); + bytes32 salt = keccak256(abi.encodePacked(l2requester, requestId)); + address moneyBoxAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(address(l1GlobalForkRequester), salt, address(forkonomicToken)); + + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + + vm.prank(address(this)); + forkonomicToken.transfer(moneyBoxAddress, fee); + + assertEq(address(forkmanager.forkonomicToken()), address(forkonomicToken)); + assertTrue(forkmanager.canFork()); + + l1GlobalForkRequester.handlePayment(address(forkonomicToken), l2requester, requestId); + assertFalse(forkmanager.isForkingInitiated()); + + (uint256 amount, uint256 amountRemainingY, uint256 amountRemainingN) = l1GlobalForkRequester.failedRequests(address(forkonomicToken), l2requester, requestId); + assertEq(amount, fee); + assertEq(amountRemainingY, 0); + assertEq(amountRemainingN, 0); + + } + + function testHandleOtherRequestForksFirst() public { + + uint256 fee = forkmanager.arbitrationFee(); + + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + // Receive a payment from a MoneyBox + + address l2requester = address(0xbabe01); + bytes32 requestId = bytes32("0xc0ffee01"); + bytes32 salt = keccak256(abi.encodePacked(l2requester, requestId)); + address moneyBoxAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(address(l1GlobalForkRequester), salt, address(forkonomicToken)); + + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + + vm.prank(address(this)); + forkonomicToken.transfer(moneyBoxAddress, fee); + + assertEq(address(forkmanager.forkonomicToken()), address(forkonomicToken)); + assertTrue(forkmanager.canFork()); + + { + // Someone else starts and executes a fork before we can handle our payment + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + vm.prank(address(this)); + forkonomicToken.approve(address(forkmanager), fee); + // Assume the data contains the questionId and pass it directly to the forkmanager in the fork request + IForkingManager.NewImplementations memory newImplementations; + IForkingManager.DisputeData memory disputeData = IForkingManager.DisputeData(false, address(this), requestId); + forkmanager.initiateFork(disputeData, newImplementations); + } + + // Our handlePayment will fail and leave our money sitting in failedRequests + uint256 balBeforeHandle = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + + l1GlobalForkRequester.handlePayment(address(forkonomicToken), l2requester, requestId); + (uint256 amount, uint256 amountRemainingY, uint256 amountRemainingN) = l1GlobalForkRequester.failedRequests(address(forkonomicToken), l2requester, requestId); + assertEq(amount, fee); + assertEq(amountRemainingY, 0); + assertEq(amountRemainingN, 0); + + uint256 balAfterHandle = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + assertEq(balBeforeHandle + amount, balAfterHandle); + + vm.expectRevert("Token not forked"); + l1GlobalForkRequester.splitTokensIntoChildTokens(address(forkonomicToken), l2requester, requestId); + + // Execute the other guy's fork + skip(forkmanager.forkPreparationTime() + 1); + forkmanager.executeFork(); + + { + uint256 balBeforeSplit = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + l1GlobalForkRequester.splitTokensIntoChildTokens(address(forkonomicToken), l2requester, requestId); + uint256 balAfterSplit = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + assertEq(balAfterSplit + amount, balBeforeSplit); + } + + // The children should now both have the funds we split + (address childToken1, address childToken2) = forkonomicToken.getChildren(); + assertEq(ForkonomicToken(childToken1).balanceOf(address(l1GlobalForkRequester)), amount); + assertEq(ForkonomicToken(childToken2).balanceOf(address(l1GlobalForkRequester)), amount); + + // Now we should be able to return the tokens on the child chain + l1GlobalForkRequester.returnTokens(address(childToken1), l2requester, requestId); + (uint256 amountChild1, , ) = l1GlobalForkRequester.failedRequests(childToken1, l2requester, requestId); + (uint256 amountChild2, , ) = l1GlobalForkRequester.failedRequests(childToken2, l2requester, requestId); + + assertEq(ForkonomicToken(childToken2).balanceOf(address(l1GlobalForkRequester)), amount); + + assertEq(amountChild1, 0); + assertEq(amountChild2, amount); + + // TODO: This breaks due to _CURRENT_SUPPORTED_NETWORKS which is capped at 2 + // Raise this if we need it, alternatively maybe it's unrelated to Chain ID and it doesn't need to change when the fork does. + + // l1GlobalForkRequester.returnTokens(address(childToken2), l2requester, requestId); + // (amountChild2, , ) = l1GlobalForkRequester.failedRequests(childToken2, l2requester, requestId); + // assertEq(amountChild2, 0); + + } + +}