diff --git a/contracts/clearing/BrokerVault.sol b/contracts/clearing/BrokerVault.sol new file mode 100644 index 000000000..29aad8fdd --- /dev/null +++ b/contracts/clearing/BrokerVault.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {IVault} from '../interfaces/IVault.sol'; +import {ISettle} from '../interfaces/ISettle.sol'; +import {ITradingTypes} from '../interfaces/ITradingTypes.sol'; +import {TradingApp} from './TradingApp.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; +import {NitroUtils} from '../nitro/libraries/NitroUtils.sol'; +import {INitroTypes} from '../nitro/interfaces/INitroTypes.sol'; + +contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { + /// @dev Using SafeERC20 to support non fully ERC20-compliant tokens, + /// that may not return a boolean value on success. + using SafeERC20 for IERC20; + + // ====== Variables ====== + + address public broker; + TradingApp public tradingApp; + mapping(bytes32 channelId => bool done) public performedSettlements; + + mapping(address token => uint256 balance) internal _balances; + + // ====== Errors ====== + + error UnauthorizedWithdrawal(); + error SettlementAlreadyPerformed(bytes32 channelId); + error BrokerNotParticipant(address actual, address expectedBroker); + + // ====== Constructor ====== + + constructor(address owner, address broker_, TradingApp tradingApp_) Ownable(owner) { + broker = broker_; + tradingApp = tradingApp_; + } + + // ---------- View functions ---------- + + function balanceOf(address user, address token) external view returns (uint256) { + if (user != broker) { + return 0; + } + return _balances[token]; + } + + function balancesOfTokens( + address user, + address[] calldata tokens + ) external view returns (uint256[] memory) { + if (user != broker) { + return new uint256[](tokens.length); + } + + uint256[] memory balances = new uint256[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + balances[i] = _balances[tokens[i]]; + } + return balances; + } + + // ---------- Owner functions ---------- + + function setBroker(address broker_) external onlyOwner { + broker = broker_; + } + + // ---------- Write functions ---------- + + function deposit(address token, uint256 amount) external payable nonReentrant { + address account = msg.sender; + + if (token == address(0)) { + require(msg.value == amount, IncorrectValue()); + _balances[address(0)] += amount; + } else { + require(msg.value == 0, IncorrectValue()); + _balances[token] += amount; + IERC20(token).safeTransferFrom(account, address(this), amount); + } + + emit Deposited(account, token, amount); + } + + function withdraw(address token, uint256 amount) external nonReentrant { + address account = msg.sender; + require(account == broker, UnauthorizedWithdrawal()); + + uint256 currentBalance = _balances[token]; + require(currentBalance >= amount, InsufficientBalance(token, amount, currentBalance)); + + _balances[token] -= amount; + + if (token == address(0)) { + /// @dev using `call` instead of `transfer` to overcome 2300 gas ceiling that could make it revert with some AA wallets + (bool success, ) = account.call{value: amount}(''); + require(success, NativeTransferFailed()); + } else { + IERC20(token).safeTransfer(account, amount); + } + + emit Withdrawn(account, token, amount); + } + + function settle( + INitroTypes.FixedPart calldata fixedPart, + INitroTypes.RecoveredVariablePart[] calldata proof, + INitroTypes.RecoveredVariablePart calldata candidate + ) external nonReentrant { + bytes32 channelId = NitroUtils.getChannelId(fixedPart); + require(!performedSettlements[channelId], SettlementAlreadyPerformed(channelId)); + + require( + fixedPart.participants[0] == broker, + BrokerNotParticipant(fixedPart.participants[1], broker) + ); + address trader = fixedPart.participants[0]; + + (bool isStateValid, string memory reason) = tradingApp.stateIsSupported( + fixedPart, + proof, + candidate + ); + require(isStateValid, InvalidStateTransition(reason)); + + ITradingTypes.Settlement memory settlement = abi.decode( + candidate.variablePart.appData, + (ITradingTypes.Settlement) + ); + + for (uint256 i = 0; i < settlement.toTrader.length; i++) { + address token = settlement.toTrader[i].asset; + uint256 amount = settlement.toTrader[i].amount; + require( + _balances[token] >= amount, + InsufficientBalance(token, amount, _balances[token]) + ); + IERC20(token).safeTransfer(trader, amount); + _balances[token] -= amount; + emit Withdrawn(broker, token, amount); + } + + for (uint256 i = 0; i < settlement.toBroker.length; i++) { + address token = settlement.toBroker[i].asset; + uint256 amount = settlement.toBroker[i].amount; + IERC20(token).safeTransferFrom(trader, broker, amount); + _balances[token] += amount; + emit Deposited(trader, token, amount); + } + + performedSettlements[channelId] = true; + emit Settled(trader, broker, channelId, settlement.ordersChecksum); + } +} diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol new file mode 100644 index 000000000..0f96add42 --- /dev/null +++ b/contracts/clearing/TradingApp.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; + +import {StrictTurnTaking} from '../nitro/libraries/signature-logic/StrictTurnTaking.sol'; +import {Consensus} from '../nitro/libraries/signature-logic/Consensus.sol'; +import {IForceMoveApp} from '../nitro/interfaces/IForceMoveApp.sol'; +import {ITradingTypes} from '../interfaces/ITradingTypes.sol'; +import {NitroUtils} from '../nitro/libraries/NitroUtils.sol'; + +contract TradingApp is IForceMoveApp { + // TODO: add custom errors after contract logic is finalized + + function stateIsSupported( + FixedPart calldata fixedPart, + RecoveredVariablePart[] calldata proof, + RecoveredVariablePart calldata candidate + ) external pure override returns (bool, string memory) { + // TODO: refactor by extracting logic into several functions + // TODO: do we want to continue operating this channel after settlement? If so, we need to support such state change. Changes to liquidation validation are required. + // turn nums: + // 0 - prefund + // 1 - postfund + // 2 - order + // 2n+1 - order response + // 2n - order or settlement + + require(fixedPart.participants.length == 2, 'invalid number of participants, expected 2'); + + uint48 candTurnNum = candidate.variablePart.turnNum; + + // prefund or postfund + if (candTurnNum == 0 || candTurnNum == 1) { + // no proof, candidate consensus + Consensus.requireConsensus(fixedPart, proof, candidate); + return (true, ''); + } + + bytes memory candidateData = candidate.variablePart.appData; + + // order or orderResponse + if (proof.length == 1) { + _requireSingleAllocation(proof[0].variablePart.outcome); + _requireSingleAllocation(candidate.variablePart.outcome); + _requireNoAllocationAmountChange( + proof[0].variablePart.outcome, + candidate.variablePart.outcome + ); + + // first order + if (candidate.variablePart.turnNum == 2) { + require( + proof[0].variablePart.turnNum == 1, + 'invalid proof turn num on first order' + ); + // check consensus of postfund + Consensus.requireConsensus(fixedPart, new RecoveredVariablePart[](0), proof[0]); + StrictTurnTaking.isSignedByMover(fixedPart, candidate); + // NOTE: used just to check the data structure validity + ITradingTypes.Order memory _candOrder = abi.decode( + candidateData, + (ITradingTypes.Order) + ); + return (true, ''); + } + + // participant 0 signs even turns + // participant 1 signs odd turns + StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate); + VariablePart memory proof0 = proof[0].variablePart; + + // order + if (candTurnNum % 2 == 0) { + // NOTE: used just to check the data structure validity + ITradingTypes.OrderResponse memory _prevOrderResponse = abi.decode( + proof0.appData, + (ITradingTypes.OrderResponse) + ); + // NOTE: used just to check the data structure validity + ITradingTypes.Order memory _candOrder = abi.decode( + candidateData, + (ITradingTypes.Order) + ); + return (true, ''); + } + + // orderResponse + ITradingTypes.Order memory order = abi.decode(proof0.appData, (ITradingTypes.Order)); + ITradingTypes.OrderResponse memory orderResponse = abi.decode( + candidateData, + (ITradingTypes.OrderResponse) + ); + if (orderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) { + require(orderResponse.orderID == order.orderID, 'order and response IDs mismatch'); + } + return (true, ''); + } + // settlement or liquidation + else if (proof.length >= 2) { + // both can only happen after an OrderResponse + require(candTurnNum % 2 == 0, 'invalid candidate turn num'); + + // liquidation + if (NitroUtils.getClaimedSignersNum(candidate.signedBy) == 1) { + require(proof.length == 2, 'liquidation proof too long'); + // check proof[0] - order + StrictTurnTaking.isSignedByMover(fixedPart, proof[0]); + ITradingTypes.Order memory order = abi.decode( + proof[0].variablePart.appData, + (ITradingTypes.Order) + ); + + // check proof[1] - ACCEPT orderResponse + StrictTurnTaking.isSignedByMover(fixedPart, proof[1]); + require( + proof[1].variablePart.turnNum == proof[0].variablePart.turnNum + 1, + 'turns are not consecutive' + ); + ITradingTypes.OrderResponse memory orderResponse = abi.decode( + proof[1].variablePart.appData, + (ITradingTypes.OrderResponse) + ); + require(orderResponse.orderID == order.orderID, 'order and response IDs mismatch'); + require( + orderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT, + 'order not accepted' + ); + + // check candidate - liquidation state + require( + // NOTE: liquidation can be not a direct successor of the ACCEPT orderResponse to allow + // for liquidation after REJECT orderResponse + candidate.variablePart.turnNum > proof[1].variablePart.turnNum, + 'invalid liquidation turn num' + ); + require( + // trader is mover #0, broker is mover #1 + NitroUtils.isClaimedSignedOnlyBy(candidate.signedBy, 1), + 'not signed by broker' + ); + + // outcomes + _requireSingleAllocation(proof[0].variablePart.outcome); + _requireSingleAllocation(proof[1].variablePart.outcome); + _requireNoAllocationAmountChange( + proof[0].variablePart.outcome, + proof[1].variablePart.outcome + ); + _requireValidFundsSplit( + proof[1].variablePart.outcome, + candidate.variablePart.outcome + ); + return (true, ''); + } + // settlement + else { + require(proof.length % 2 == 0, 'settlement proof contains dangling values'); + // check consensus of candidate + Consensus.requireConsensus(fixedPart, new RecoveredVariablePart[](0), candidate); + // Check the settlement data structure validity + ITradingTypes.Settlement memory settlement = abi.decode( + candidateData, + (ITradingTypes.Settlement) + ); + _verifyProofForSettlement(fixedPart, settlement, proof); + return (true, ''); + } + } + + revert('invalid proof length'); + } + + function _requireSingleAllocation(Outcome.SingleAssetExit[] memory outcome) internal pure { + require(outcome.length == 1, 'not 1 asset'); + require(outcome[0].allocations.length == 1, 'not 1 allocation'); + } + + function _requireNoAllocationAmountChange( + Outcome.SingleAssetExit[] memory prevOutcome, + Outcome.SingleAssetExit[] memory nextOutcome + ) internal pure { + require( + prevOutcome[0].allocations[0].destination == nextOutcome[0].allocations[0].destination, + 'destination changed in allocation' + ); + require( + prevOutcome[0].allocations[0].amount == nextOutcome[0].allocations[0].amount, + 'amount changed in allocation' + ); + } + + function _requireValidFundsSplit( + Outcome.SingleAssetExit[] memory prevOutcome, + Outcome.SingleAssetExit[] memory nextOutcome + ) internal pure { + require( + prevOutcome[0].allocations[0].amount == + nextOutcome[0].allocations[0].amount + nextOutcome[0].allocations[1].amount, + 'amounts sum mismatch' + ); + } + + function _verifyProofForSettlement( + FixedPart calldata fixedPart, + ITradingTypes.Settlement memory settlement, + RecoveredVariablePart[] calldata proof + ) internal pure { + bytes32[] memory proofDataHashes = new bytes32[](proof.length); + uint256 prevTurnNum = 1; // postfund state + for (uint256 i = 0; i < proof.length - 1; i += 2) { + VariablePart memory currProof = proof[i].variablePart; + VariablePart memory nextProof = proof[i + 1].variablePart; + + StrictTurnTaking.isSignedByMover(fixedPart, proof[i]); + StrictTurnTaking.isSignedByMover(fixedPart, proof[i + 1]); + require(prevTurnNum + 1 == currProof.turnNum, 'turns are not consecutive'); + require(currProof.turnNum + 1 == nextProof.turnNum, 'turns are not consecutive'); + + // Verify validity of orders and responses + ITradingTypes.Order memory order = abi.decode(currProof.appData, (ITradingTypes.Order)); + ITradingTypes.OrderResponse memory orderResponse = abi.decode( + nextProof.appData, + (ITradingTypes.OrderResponse) + ); + + // If current proof contains an order, + // then the next one must contain a response + // with the same order ID + require(orderResponse.orderID == order.orderID, 'order and response IDs mismatch'); + + // outcomes + if (i != 0) { + _requireNoAllocationAmountChange( + proof[i - 1].variablePart.outcome, + proof[i].variablePart.outcome + ); + } + _requireSingleAllocation(proof[i].variablePart.outcome); + _requireSingleAllocation(proof[i + 1].variablePart.outcome); + _requireNoAllocationAmountChange( + proof[i].variablePart.outcome, + proof[i + 1].variablePart.outcome + ); + + proofDataHashes[i] = keccak256(currProof.appData); + proofDataHashes[i + 1] = keccak256(nextProof.appData); + prevTurnNum = nextProof.turnNum; + } + + bytes32 ordersChecksum = keccak256(abi.encode(proofDataHashes)); + require(ordersChecksum == settlement.ordersChecksum, 'settlement checksum mismatch'); + } +} diff --git a/contracts/interfaces/ISettle.sol b/contracts/interfaces/ISettle.sol new file mode 100644 index 000000000..1a1569ccf --- /dev/null +++ b/contracts/interfaces/ISettle.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {INitroTypes} from '../nitro/interfaces/INitroTypes.sol'; + +/** + * @title ISettle + * @notice Interface for a contract that allows users to settle a channel. + */ +interface ISettle { + // ========== Events ========== + + /** + * @notice Emitted when a channel is settled. + * @param trader The address of the trader. + * @param broker The address of the broker. + * @param channelId The ID of the channel. + */ + event Settled( + address indexed trader, + address indexed broker, + bytes32 indexed channelId, + bytes32 settlementId + ); + + // ========== Errors ========== + + error InvalidStateTransition(string reason); + + // ========== Functions ========== + + /** + * @notice Settle a channel. + * @param fixedPart The fixed part of the state. + * @param proof The proof of the state. + * @param candidate The candidate state. + */ + function settle( + INitroTypes.FixedPart calldata fixedPart, + INitroTypes.RecoveredVariablePart[] calldata proof, + INitroTypes.RecoveredVariablePart calldata candidate + ) external; +} diff --git a/contracts/interfaces/ITradingTypes.sol b/contracts/interfaces/ITradingTypes.sol new file mode 100644 index 000000000..d69eb70d7 --- /dev/null +++ b/contracts/interfaces/ITradingTypes.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +interface ITradingTypes { + struct Order { + bytes32 orderID; + } + + enum OrderResponseType { + ACCEPT, + REJECT + } + + struct OrderResponse { + OrderResponseType responseType; + bytes32 orderID; // orderID making the trade + } + + struct AssetAndAmount { + address asset; + uint256 amount; + } + + struct Settlement { + AssetAndAmount[] toTrader; + AssetAndAmount[] toBroker; + // ordersChecksum is to avoid tampering + // with the orders and order responses in the proof + bytes32 ordersChecksum; + } +} diff --git a/contracts/interfaces/IVault.sol b/contracts/interfaces/IVault.sol new file mode 100644 index 000000000..d03c49aea --- /dev/null +++ b/contracts/interfaces/IVault.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IVault + * @notice Interface for a vault contract that allows users to deposit, withdraw, and check balances of tokens and ETH. + */ +interface IVault { + /** + * @notice Error thrown when the address supplied with the function call is invalid. + */ + error InvalidAddress(); + + /** + * @notice Emitted when a user deposits tokens or ETH into the vault. + * @param user The address of the user that deposited the tokens. + * @param token The address of the token deposited or address(0) for ETH. + * @param amount The amount of tokens or ETH deposited. + */ + event Deposited(address indexed user, address indexed token, uint256 amount); + + /** + * @notice Emitted when a user withdraws tokens or ETH from the vault. + * @param user The address of the user that withdrew the tokens. + * @param token The address of the token withdrawn or address(0) for ETH. + * @param amount The amount of tokens or ETH withdrawn. + */ + event Withdrawn(address indexed user, address indexed token, uint256 amount); + + /** + * @notice Error thrown when the value supplied with the function call is incorrect. + */ + error IncorrectValue(); + + /** + * @notice Error thrown when the user has insufficient balance to perform an action. + * @param token The address of the token that user lacks. + * @param required The amount of tokens that is required to perform the action. + * @param available The amount of tokens that the user has. + */ + error InsufficientBalance(address token, uint256 required, uint256 available); + + /** + * @notice Error thrown when the transfer of Eth fails. + */ + error NativeTransferFailed(); + + /** + * @dev Returns the balance of a specified token for a user. + * @param user The address of the user. + * @param token The address of the token. Use address(0) for ETH. + * @return The balance of the specified token for the user. + */ + function balanceOf(address user, address token) external view returns (uint256); + + /** + * @dev Returns the balances of multiple tokens for a user. + * @param user The address of the user. + * @param tokens The addresses of the tokens. Use address(0) for ETH. + * @return The balances of the specified tokens for the user. + */ + function balancesOfTokens( + address user, + address[] calldata tokens + ) external view returns (uint256[] memory); + + /** + * @dev Deposits a specified amount of tokens or ETH into the vault. + * @param token The address of the token to deposit. Use address(0) for ETH. + * @param amount The amount of tokens or ETH to deposit. + */ + function deposit(address token, uint256 amount) external payable; + + /** + * @dev Withdraws a specified amount of tokens or ETH from the vault. + * @param token The address of the token to withdraw. Use address(0) for ETH. + * @param amount The amount of tokens or ETH to withdraw. + */ + function withdraw(address token, uint256 amount) external; +} diff --git a/foundry.toml b/foundry.toml index 637fb76a4..a00c9d261 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,6 @@ src = "contracts" out = "artifacts" test = 'test' -solc = "0.8.22" optimizer = true optimizer_runs = 100000 gas_price = 1000000000 diff --git a/test/clearing/TradingApp.t.sol b/test/clearing/TradingApp.t.sol new file mode 100644 index 000000000..66a54061e --- /dev/null +++ b/test/clearing/TradingApp.t.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Test, console} from 'forge-std/Test.sol'; + +import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; + +import {TradingApp} from '../../contracts/clearing/TradingApp.sol'; +import {ITradingTypes} from '../../contracts/interfaces/ITradingTypes.sol'; +import {INitroTypes} from '../../contracts/nitro/interfaces/INitroTypes.sol'; + +contract TradingAppTest_stateIsSupported is Test { + TradingApp public tradingApp; + INitroTypes.FixedPart public fixedPart; + address traderAddress = vm.createWallet('trader').addr; + address brokerAddress = vm.createWallet('broker').addr; + + uint256 marginAmount = 42; + + function setUp() public { + tradingApp = new TradingApp(); + address[] memory participants = new address[](2); + participants[0] = traderAddress; + participants[1] = brokerAddress; + + fixedPart = INitroTypes.FixedPart({ + participants: participants, + channelNonce: 42, + appDefinition: address(tradingApp), + challengeDuration: 42 + }); + } + + // NOTE: this is not a storage variable, as copying an array of structs from memory to storage is not yet supported in Solidity + function traderOutcome() public view returns (Outcome.SingleAssetExit[] memory) { + return createSingleOutcome(address(42), traderAddress, marginAmount); + } + + function createSingleOutcome( + address asset, + address destination, + uint256 amount + ) public pure returns (Outcome.SingleAssetExit[] memory) { + Outcome.Allocation[] memory allocations = new Outcome.Allocation[](1); + allocations[0] = Outcome.Allocation({ + destination: bytes20(destination), + amount: amount, + allocationType: 0, + metadata: new bytes(0) + }); + Outcome.SingleAssetExit[] memory outcome = new Outcome.SingleAssetExit[](1); + outcome[0] = Outcome.SingleAssetExit({ + asset: asset, + assetMetadata: Outcome.AssetMetadata({ + assetType: Outcome.AssetType.Default, + metadata: new bytes(0) + }), + allocations: allocations + }); + return outcome; + } + + function createDupleOutcome( + address asset, + address[2] memory addresses, + uint256[2] memory amounts + ) public pure returns (Outcome.SingleAssetExit[] memory) { + Outcome.Allocation[] memory allocations = new Outcome.Allocation[](2); + for (uint256 i = 0; i < 2; i++) { + allocations[i] = Outcome.Allocation({ + destination: bytes20(addresses[i]), + amount: amounts[i], + allocationType: 0, + metadata: new bytes(0) + }); + } + Outcome.SingleAssetExit[] memory outcome = new Outcome.SingleAssetExit[](1); + outcome[0] = Outcome.SingleAssetExit({ + asset: asset, + assetMetadata: Outcome.AssetMetadata({ + assetType: Outcome.AssetType.Default, + metadata: new bytes(0) + }), + allocations: allocations + }); + return outcome; + } + + function createRVP( + Outcome.SingleAssetExit[] memory outcome, + bytes memory appData, + uint48 turnNum, + bool isFinal, + uint8[] memory signedByIndices + ) public pure returns (INitroTypes.RecoveredVariablePart memory) { + INitroTypes.VariablePart memory variablePart = INitroTypes.VariablePart({ + outcome: outcome, + appData: appData, + turnNum: turnNum, + isFinal: isFinal + }); + uint256 signedBy; + for (uint8 i = 0; i < signedByIndices.length; i++) { + signedBy += 2 ** signedByIndices[i]; + } + return INitroTypes.RecoveredVariablePart({variablePart: variablePart, signedBy: signedBy}); + } + + function newUint8_1(uint8 num) public pure returns (uint8[] memory) { + uint8[] memory arr = new uint8[](1); + arr[0] = num; + return arr; + } + + function newUint8_2(uint8 num1, uint8 num2) public pure returns (uint8[] memory) { + uint8[] memory arr = new uint8[](2); + arr[0] = num1; + arr[1] = num2; + return arr; + } + + function test_supported_firstOrder() public view { + INitroTypes.RecoveredVariablePart[] memory proof = new INitroTypes.RecoveredVariablePart[]( + 1 + ); + // postfund + proof[0] = createRVP(traderOutcome(), new bytes(0), 1, false, newUint8_2(0, 1)); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + traderOutcome(), + abi.encode(ITradingTypes.Order({orderID: bytes32('order1')})), + 2, + false, + newUint8_1(0) + ); + + (bool supported, string memory reason) = tradingApp.stateIsSupported( + fixedPart, + proof, + candidate + ); + assertTrue(supported); + assertEq(reason, ''); + } + + function test_supported_orderResponsePair() public view { + INitroTypes.RecoveredVariablePart[] memory proof = new INitroTypes.RecoveredVariablePart[]( + 1 + ); + // order + proof[0] = createRVP( + traderOutcome(), + abi.encode(ITradingTypes.Order({orderID: bytes32('order1')})), + 2, + false, + newUint8_1(0) + ); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + traderOutcome(), + abi.encode( + ITradingTypes.OrderResponse({ + orderID: bytes32('order1'), + responseType: ITradingTypes.OrderResponseType.ACCEPT + }) + ), + 3, + false, + newUint8_1(1) + ); + + (bool supported, string memory reason) = tradingApp.stateIsSupported( + fixedPart, + proof, + candidate + ); + assertTrue(supported); + assertEq(reason, ''); + } + + function test_supported_secondOrder() public view { + INitroTypes.RecoveredVariablePart[] memory proof = new INitroTypes.RecoveredVariablePart[]( + 1 + ); + // order + proof[0] = createRVP( + traderOutcome(), + abi.encode( + ITradingTypes.OrderResponse({ + orderID: bytes32('order1'), + responseType: ITradingTypes.OrderResponseType.ACCEPT + }) + ), + 3, + false, + newUint8_1(1) + ); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + traderOutcome(), + abi.encode(ITradingTypes.Order({orderID: bytes32('order2')})), + 4, + false, + newUint8_1(0) + ); + + (bool supported, string memory reason) = tradingApp.stateIsSupported( + fixedPart, + proof, + candidate + ); + assertTrue(supported); + assertEq(reason, ''); + } + + function test_supported_liquidation() public view { + ITradingTypes.Order memory order1 = ITradingTypes.Order({orderID: bytes32('order1')}); + ITradingTypes.OrderResponse memory response1 = ITradingTypes.OrderResponse({ + orderID: bytes32('order1'), + responseType: ITradingTypes.OrderResponseType.ACCEPT + }); + + INitroTypes.RecoveredVariablePart[] memory proof = new INitroTypes.RecoveredVariablePart[]( + 2 + ); + proof[0] = createRVP(traderOutcome(), abi.encode(order1), 2, false, newUint8_1(0)); + proof[1] = createRVP(traderOutcome(), abi.encode(response1), 3, false, newUint8_1(1)); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + createDupleOutcome( + address(42), + [traderAddress, brokerAddress], + [uint256(1), marginAmount - 1] + ), + new bytes(0), + 4, + false, + newUint8_1(1) + ); + + (bool supported, string memory reason) = tradingApp.stateIsSupported( + fixedPart, + proof, + candidate + ); + assertTrue(supported); + assertEq(reason, ''); + } + + function test_supported_settlement() public view { + ITradingTypes.Order memory order1 = ITradingTypes.Order({orderID: bytes32('order1')}); + ITradingTypes.OrderResponse memory response1 = ITradingTypes.OrderResponse({ + orderID: bytes32('order1'), + responseType: ITradingTypes.OrderResponseType.ACCEPT + }); + ITradingTypes.Order memory order2 = ITradingTypes.Order({orderID: bytes32('order2')}); + ITradingTypes.OrderResponse memory response2 = ITradingTypes.OrderResponse({ + orderID: bytes32('order2'), + responseType: ITradingTypes.OrderResponseType.ACCEPT + }); + + ITradingTypes.AssetAndAmount[] memory toTrader = new ITradingTypes.AssetAndAmount[](2); + toTrader[0] = ITradingTypes.AssetAndAmount({asset: address(42), amount: 1}); + toTrader[1] = ITradingTypes.AssetAndAmount({asset: address(43), amount: 2}); + + ITradingTypes.AssetAndAmount[] memory toBroker = new ITradingTypes.AssetAndAmount[](2); + toBroker[0] = ITradingTypes.AssetAndAmount({asset: address(44), amount: 3}); + toBroker[1] = ITradingTypes.AssetAndAmount({asset: address(45), amount: 4}); + + // NOTE: dynamic array as it is used in TradingApp + bytes32[] memory proofDataHashes = new bytes32[](4); + proofDataHashes[0] = keccak256(abi.encode(order1)); + proofDataHashes[1] = keccak256(abi.encode(response1)); + proofDataHashes[2] = keccak256(abi.encode(order2)); + proofDataHashes[3] = keccak256(abi.encode(response2)); + + bytes32 checksum = keccak256(abi.encode(proofDataHashes)); + ITradingTypes.Settlement memory settlement = ITradingTypes.Settlement({ + toTrader: toTrader, + toBroker: toBroker, + ordersChecksum: checksum + }); + + INitroTypes.RecoveredVariablePart[] memory proof = new INitroTypes.RecoveredVariablePart[]( + 4 + ); + proof[0] = createRVP(traderOutcome(), abi.encode(order1), 2, false, newUint8_1(0)); + proof[1] = createRVP(traderOutcome(), abi.encode(response1), 3, false, newUint8_1(1)); + proof[2] = createRVP(traderOutcome(), abi.encode(order2), 4, false, newUint8_1(0)); + proof[3] = createRVP(traderOutcome(), abi.encode(response2), 5, false, newUint8_1(1)); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + traderOutcome(), + abi.encode(settlement), + 6, + false, + newUint8_2(0, 1) + ); + + (bool supported, string memory reason) = tradingApp.stateIsSupported( + fixedPart, + proof, + candidate + ); + assertTrue(supported); + assertEq(reason, ''); + } +}