From 116ea8daabc73647004d187dfe0ed1191c31a631 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 25 Nov 2024 17:57:03 +0200 Subject: [PATCH 01/14] feat(tradingapp): add suggested TradingStructs, ISettle iface, TradingApp rules --- contracts/clearing/TradingApp.sol | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 contracts/clearing/TradingApp.sol diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol new file mode 100644 index 000000000..3649e4954 --- /dev/null +++ b/contracts/clearing/TradingApp.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.22; + +import '../nitro/interfaces/IForceMoveApp.sol'; +import '../nitro/libraries/NitroUtils.sol'; +import '../nitro/interfaces/INitroTypes.sol'; +import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; + +/* + - both Order and Trade contain sigs of user and broker respectively + - Order and Trade are coupled by some id (that may be calculated based on the order itself) + - State invariant: for each Order there is a respective Trade, and both are valid (not 0 amounts, etc) + - State transition rules: + - amount of Order-Trade pairs can increase without restriction + - amount of Order-Trade pairs can decrease only after the settlement state + - settlement state is a state with `settlementData` not + - to settle using the TradingVault, settlement state must be signed by the Broker and *maybe* by the user + */ + +// TODO: optimize for storage slots +interface TradingStructs { + enum Side { + BUY, + SELL + } + + struct Order { + Side side; + address token; + uint256 amount; + uint256 ts; // timestamp in seconds + bytes signature; // user signature + } + + // NOTE: there is no need in `tradeId` as it is not checked in any way. + // If you want to signal Trade settlement, then use settlementId in the event instead + struct Trade { + bytes32 orderId; // keccak256(abi.encode(Order)) + uint256 amount; // amount executed + uint256 ts; // timestamp in seconds + bytes signature; // broker signature + } + + enum FundingLocation { + /// @dev funds are taken from the TradingVault account's balance + TradingVault, + /// @dev funds are pulled from the account's token balance + Address + } + + struct CounterpartiesFundingLocations { + FundingLocation[] user; + FundingLocation[] broker; + } + + struct OrderTradePair { + Order order; + Trade trade; + } + + struct Settlement { + bytes32 id; // keccak256(abi.encode(OrderTradePair[])); + CounterpartiesFundingLocations sourceFLs; + CounterpartiesFundingLocations destinationFLs; + } + + // is encoded into the `state` field of the variablePart + struct State { + OrderTradePair[] pairs; + bytes settlementData; // 0x if state is not settlement, encoded `Settlement` if it is + } +} + +interface ISettle { + function settle( + INitroTypes.FixedPart calldata fixedPart, + INitroTypes.RecoveredVariablePart[] calldata proof, + INitroTypes.RecoveredVariablePart calldata candidate + ) external; +} + +contract TradingApp is IForceMoveApp { + function stateIsSupported( + FixedPart calldata fixedPart, + RecoveredVariablePart[] calldata proof, + RecoveredVariablePart calldata candidate + ) external pure override returns (bool, string memory) {} +} From 641612e89775e237284dcbf2c5d2226852512172 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Tue, 26 Nov 2024 18:01:01 +0200 Subject: [PATCH 02/14] feat(trading app): update structs, implement tradingApp --- contracts/clearing/TradingApp.sol | 159 +++++++++++++++++++----------- 1 file changed, 104 insertions(+), 55 deletions(-) diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index 3649e4954..2714e8602 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -1,76 +1,41 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.22; -import '../nitro/interfaces/IForceMoveApp.sol'; -import '../nitro/libraries/NitroUtils.sol'; -import '../nitro/interfaces/INitroTypes.sol'; import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; -/* - - both Order and Trade contain sigs of user and broker respectively - - Order and Trade are coupled by some id (that may be calculated based on the order itself) - - State invariant: for each Order there is a respective Trade, and both are valid (not 0 amounts, etc) - - State transition rules: - - amount of Order-Trade pairs can increase without restriction - - amount of Order-Trade pairs can decrease only after the settlement state - - settlement state is a state with `settlementData` not - - to settle using the TradingVault, settlement state must be signed by the Broker and *maybe* by the user - */ - -// TODO: optimize for storage slots -interface TradingStructs { - enum Side { - BUY, - SELL - } +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 {NitroUtils} from '../nitro/libraries/NitroUtils.sol'; +import {INitroTypes} from '../nitro/interfaces/INitroTypes.sol'; +interface ITradingStructs { struct Order { - Side side; - address token; - uint256 amount; - uint256 ts; // timestamp in seconds - bytes signature; // user signature - } - - // NOTE: there is no need in `tradeId` as it is not checked in any way. - // If you want to signal Trade settlement, then use settlementId in the event instead - struct Trade { - bytes32 orderId; // keccak256(abi.encode(Order)) - uint256 amount; // amount executed - uint256 ts; // timestamp in seconds - bytes signature; // broker signature + bytes32 orderID; // tradeID } - enum FundingLocation { - /// @dev funds are taken from the TradingVault account's balance - TradingVault, - /// @dev funds are pulled from the account's token balance - Address + enum OrderResponseType { + ACCEPT, + REJECT } - struct CounterpartiesFundingLocations { - FundingLocation[] user; - FundingLocation[] broker; + struct OrderResponse { + OrderResponseType responseType; + bytes32 orderID; // orderID making the trade } - struct OrderTradePair { - Order order; - Trade trade; + struct AssetAndAmount { + address asset; + uint256 amount; } struct Settlement { - bytes32 id; // keccak256(abi.encode(OrderTradePair[])); - CounterpartiesFundingLocations sourceFLs; - CounterpartiesFundingLocations destinationFLs; - } - - // is encoded into the `state` field of the variablePart - struct State { - OrderTradePair[] pairs; - bytes settlementData; // 0x if state is not settlement, encoded `Settlement` if it is + AssetAndAmount[] toTrader; + AssetAndAmount[] toBroker; } } +// FIXME: should Vault support multiple brokers? interface ISettle { function settle( INitroTypes.FixedPart calldata fixedPart, @@ -84,5 +49,89 @@ contract TradingApp is IForceMoveApp { FixedPart calldata fixedPart, RecoveredVariablePart[] calldata proof, RecoveredVariablePart calldata candidate - ) external pure override returns (bool, string memory) {} + ) external pure override returns (bool, string memory) { + // FIXME: does the Broker deposit to the Adjudicator? + // turn nums: + // 0 - prefund + // 1 - postfund + // 2 - order + // 2n+1 - order response + // 2n - order or settlement + + uint48 candTurnNum = candidate.variablePart.turnNum; + + // prefund or postfund + if (candTurnNum == 0 || candTurnNum == 1) { + Consensus.requireConsensus(fixedPart, proof, candidate); + return (true, ''); + } + + bytes memory candidateData = candidate.variablePart.appData; + // settlement + // TODO: unsure whether we should check the proof when consensus is reached + if (candTurnNum % 2 == 0 && proof.length == 0) { + Consensus.requireConsensus(fixedPart, proof, candidate); + // NOTE: used just to check the data structure validity + ITradingStructs.Settlement memory _unused = abi.decode( + candidateData, + (ITradingStructs.Settlement) + ); + return (true, ''); + } + + // participant 0 signs even turns + // participant 1 signs odd turns + StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate); + require(proof.length == 2, 'proof.length < 2'); + (VariablePart memory proof0, VariablePart memory proof1) = ( + proof[0].variablePart, + proof[1].variablePart + ); + require(proof0.turnNum == candTurnNum - 2, 'proof0.turnNum != candTurnNum - 1'); + require(proof1.turnNum == candTurnNum - 1, 'proof1.turnNum != candTurnNum - 1'); + + // order + if (candTurnNum % 2 == 0) { + ITradingStructs.Order memory prevOrder = abi.decode( + proof0.appData, + (ITradingStructs.Order) + ); + ITradingStructs.OrderResponse memory prevOrderResponse = abi.decode( + proof1.appData, + (ITradingStructs.OrderResponse) + ); + if (prevOrderResponse.responseType == ITradingStructs.OrderResponseType.ACCEPT) { + require( + prevOrderResponse.orderID == prevOrder.orderID, + 'orderResponse.orderID != prevOrder.orderID, candidate is order' + ); + } + // NOTE: used just to check the data structure validity + ITradingStructs.Order memory _candOrder = abi.decode( + candidateData, + (ITradingStructs.Order) + ); + return (true, ''); + } + + // orderResponse + // NOTE: used just to check the data structure validity + ITradingStructs.OrderResponse memory _prevOrderResponse = abi.decode( + proof0.appData, + (ITradingStructs.OrderResponse) + ); + + ITradingStructs.Order memory order = abi.decode(proof1.appData, (ITradingStructs.Order)); + ITradingStructs.OrderResponse memory orderResponse = abi.decode( + candidateData, + (ITradingStructs.OrderResponse) + ); + if (orderResponse.responseType == ITradingStructs.OrderResponseType.ACCEPT) { + require( + orderResponse.orderID == order.orderID, + 'orderResponse.orderID != order.orderID, candidate is orderResponse' + ); + } + return (true, ''); + } } From 5bc116d0ff6ed6c5ae328d15daff2580d558903d Mon Sep 17 00:00:00 2001 From: Max Pushkarov Date: Thu, 19 Dec 2024 20:28:28 +0200 Subject: [PATCH 03/14] feat(contracts): add Broker Vault contract (#456) * feat(contracts): add broker vault contract (WIP) * fix(contracts): init TradingApp in ctor * refactor(contracts): add underscore to performedSettlements mapping name to adhere code style * refactor(contracts): resolve review comments * refactor(contracts): handle edge case for balanceOf methods & use `require` instead of `if`s * refactor(contracts): add support for native assets in deposit and withdraw methods * fix(contracts): add nonReentract modifier on deposit, withdraw, and settle methods * refactor: split TradingApp to adhere 'one interface per file' rule * feat: verify full order history on settlement * fix: reverse conditions * fix(contracts): fix compiler errors and warnings * refactor: move a settlement proof validation to a separate function * feat: verify proof hash to avoid accepting modified proof * fix: proceed with order/response validation if settlement parsing fails * refactor: inline settlement decoding * refactor: rename proofHash settlement field to ordersChecksum; resolve review comments * refactor: rename `orderChecksum` field of settlement struct to plural form * fix: resolve review comments * refactor: process proofs in pairs on each iteration * refactor: move pre/post-fund check out of settlement verification loop to save gas * refactor: drop redundant verification --------- Co-authored-by: nksazonov --- contracts/clearing/BrokerVault.sol | 148 +++++++++++++++++++++++++ contracts/clearing/TradingApp.sol | 123 ++++++++++---------- contracts/interfaces/ISettle.sol | 38 +++++++ contracts/interfaces/ITradingTypes.sol | 31 ++++++ contracts/interfaces/IVault.sol | 75 +++++++++++++ 5 files changed, 359 insertions(+), 56 deletions(-) create mode 100644 contracts/clearing/BrokerVault.sol create mode 100644 contracts/interfaces/ISettle.sol create mode 100644 contracts/interfaces/ITradingTypes.sol create mode 100644 contracts/interfaces/IVault.sol diff --git a/contracts/clearing/BrokerVault.sol b/contracts/clearing/BrokerVault.sol new file mode 100644 index 000000000..a1a6e25b2 --- /dev/null +++ b/contracts/clearing/BrokerVault.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {IVault} from '../interfaces/IVault.sol'; +import {TradingApp, ISettle} from './TradingApp.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; +import {NitroUtils} from '../nitro/libraries/NitroUtils.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 InsufficientBalance(address token, uint256 required, uint256 available); + error InvalidAddress(); + error InvalidAmount(uint256 amount); + 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 { + 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(msg.sender, address(this), amount); + } + + emit Deposited(msg.sender, token, amount); + } + + function withdraw(address token, uint256 amount) external nonReentrant { + 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, ) = msg.sender.call{value: amount}(''); + require(success, NativeTransferFailed()); + } else { + IERC20(token).safeTransfer(msg.sender, amount); + } + + emit Withdrawn(msg.sender, token, amount); + } + + function settle( + INitroTypes.FixedPart calldata fixedPart, + INitroTypes.RecoveredVariablePart[] calldata proof, + INitroTypes.RecoveredVariablePart calldata candidate + ) external nonReentrant { + uint256 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.isStateTransitionValid( + fixedPart, + proof, + candidate + ); + require(isStateValid, InvalidStateTransition(reason)); + + ITradingStructs.Settlement memory settlement = abi.decode( + candidate.variablePart.appData, + (ITradingStructs.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; + } + + 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; + } + + performedSettlements[channelId] = true; + emit Settled(trader, broker, channelId); + } +} diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index 2714e8602..fbdeaa20c 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -6,45 +6,11 @@ import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFo 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 {NitroUtils} from '../nitro/libraries/NitroUtils.sol'; -import {INitroTypes} from '../nitro/interfaces/INitroTypes.sol'; - -interface ITradingStructs { - struct Order { - bytes32 orderID; // tradeID - } - - 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; - } -} - -// FIXME: should Vault support multiple brokers? -interface ISettle { - function settle( - INitroTypes.FixedPart calldata fixedPart, - INitroTypes.RecoveredVariablePart[] calldata proof, - INitroTypes.RecoveredVariablePart calldata candidate - ) external; -} +import {ITradingTypes} from '../interfaces/ITradingTypes.sol'; contract TradingApp is IForceMoveApp { + // TODO: add errors + function stateIsSupported( FixedPart calldata fixedPart, RecoveredVariablePart[] calldata proof, @@ -67,22 +33,30 @@ contract TradingApp is IForceMoveApp { } bytes memory candidateData = candidate.variablePart.appData; + // settlement - // TODO: unsure whether we should check the proof when consensus is reached - if (candTurnNum % 2 == 0 && proof.length == 0) { + uint8 signaturesNum = NitroUtils.getClaimedSignersNum(candidate.signedBy); + if ( + candTurnNum % 2 == 0 /* is either order or settlement */ && + signaturesNum == 2 /* is settlement */ && + proof.length >= 2 /* contains at least one order+response pair */ && + proof.length % 2 == 0 /* contains full pairs only, no dangling values */ + ) { Consensus.requireConsensus(fixedPart, proof, candidate); - // NOTE: used just to check the data structure validity - ITradingStructs.Settlement memory _unused = abi.decode( + // Check the settlement data structure validity + ITradingTypes.Settlement memory settlement = abi.decode( candidateData, - (ITradingStructs.Settlement) + (ITradingTypes.Settlement) ); + verifyProofForSettlement(settlement, proof); return (true, ''); } // participant 0 signs even turns // participant 1 signs odd turns StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate); - require(proof.length == 2, 'proof.length < 2'); + require(signaturesNum == 1, 'signaturesNum != 1'); + require(proof.length == 2, 'proof.length != 2'); (VariablePart memory proof0, VariablePart memory proof1) = ( proof[0].variablePart, proof[1].variablePart @@ -92,41 +66,41 @@ contract TradingApp is IForceMoveApp { // order if (candTurnNum % 2 == 0) { - ITradingStructs.Order memory prevOrder = abi.decode( + ITradingTypes.Order memory prevOrder = abi.decode( proof0.appData, - (ITradingStructs.Order) + (ITradingTypes.Order) ); - ITradingStructs.OrderResponse memory prevOrderResponse = abi.decode( + ITradingTypes.OrderResponse memory prevOrderResponse = abi.decode( proof1.appData, - (ITradingStructs.OrderResponse) + (ITradingTypes.OrderResponse) ); - if (prevOrderResponse.responseType == ITradingStructs.OrderResponseType.ACCEPT) { + if (prevOrderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) { require( prevOrderResponse.orderID == prevOrder.orderID, 'orderResponse.orderID != prevOrder.orderID, candidate is order' ); } // NOTE: used just to check the data structure validity - ITradingStructs.Order memory _candOrder = abi.decode( + ITradingTypes.Order memory _candOrder = abi.decode( candidateData, - (ITradingStructs.Order) + (ITradingTypes.Order) ); return (true, ''); } // orderResponse // NOTE: used just to check the data structure validity - ITradingStructs.OrderResponse memory _prevOrderResponse = abi.decode( + ITradingTypes.OrderResponse memory _prevOrderResponse = abi.decode( proof0.appData, - (ITradingStructs.OrderResponse) + (ITradingTypes.OrderResponse) ); - ITradingStructs.Order memory order = abi.decode(proof1.appData, (ITradingStructs.Order)); - ITradingStructs.OrderResponse memory orderResponse = abi.decode( + ITradingTypes.Order memory order = abi.decode(proof1.appData, (ITradingTypes.Order)); + ITradingTypes.OrderResponse memory orderResponse = abi.decode( candidateData, - (ITradingStructs.OrderResponse) + (ITradingTypes.OrderResponse) ); - if (orderResponse.responseType == ITradingStructs.OrderResponseType.ACCEPT) { + if (orderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) { require( orderResponse.orderID == order.orderID, 'orderResponse.orderID != order.orderID, candidate is orderResponse' @@ -134,4 +108,41 @@ contract TradingApp is IForceMoveApp { } return (true, ''); } + + function verifyProofForSettlement( + 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; + + 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 do not match' + ); + + proofDataHashes[i] = keccak256(currProof.appData); + proofDataHashes[i + 1] = keccak256(nextProof.appData); + prevTurnNum = nextProof.turnNum; + } + + bytes32 ordersChecksum = keccak256(abi.encode(proofDataHashes)); + require(ordersChecksum == settlement.ordersChecksum, 'proof has been tampered with'); + } } diff --git a/contracts/interfaces/ISettle.sol b/contracts/interfaces/ISettle.sol new file mode 100644 index 000000000..a71a32d4f --- /dev/null +++ b/contracts/interfaces/ISettle.sol @@ -0,0 +1,38 @@ +// 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); + + // ========== 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..23b685589 --- /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..45527e45d --- /dev/null +++ b/contracts/interfaces/IVault.sol @@ -0,0 +1,75 @@ +// 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 token The address of the token. Use address(0) for ETH. + * @return The balance of the specified token for the user. + */ + function balanceOf(address token) external view returns (uint256); + + /** + * @dev Returns the balances of multiple tokens for a 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[] 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; +} From 3c071a267a724c0f0218a6ac7b1e17c12a307da0 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Tue, 24 Dec 2024 12:22:43 +0200 Subject: [PATCH 04/14] fix(tradingapp): remove compiler error, comment --- contracts/clearing/TradingApp.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index fbdeaa20c..07eb89768 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -7,6 +7,7 @@ import {StrictTurnTaking} from '../nitro/libraries/signature-logic/StrictTurnTak 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 errors @@ -16,7 +17,6 @@ contract TradingApp is IForceMoveApp { RecoveredVariablePart[] calldata proof, RecoveredVariablePart calldata candidate ) external pure override returns (bool, string memory) { - // FIXME: does the Broker deposit to the Adjudicator? // turn nums: // 0 - prefund // 1 - postfund @@ -132,10 +132,7 @@ contract TradingApp is IForceMoveApp { // 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 do not match' - ); + require(orderResponse.orderID == order.orderID, 'order and response IDs do not match'); proofDataHashes[i] = keccak256(currProof.appData); proofDataHashes[i + 1] = keccak256(nextProof.appData); From dd9bcd19c79a1983edf81131410dc4b53109172f Mon Sep 17 00:00:00 2001 From: nksazonov Date: Tue, 24 Dec 2024 16:43:20 +0200 Subject: [PATCH 05/14] fix(vault): remove compiler errors --- contracts/clearing/BrokerVault.sol | 17 ++++++++++------- contracts/interfaces/IVault.sol | 9 +++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/contracts/clearing/BrokerVault.sol b/contracts/clearing/BrokerVault.sol index a1a6e25b2..2638f611d 100644 --- a/contracts/clearing/BrokerVault.sol +++ b/contracts/clearing/BrokerVault.sol @@ -2,11 +2,16 @@ pragma solidity 0.8.27; import {IVault} from '../interfaces/IVault.sol'; -import {TradingApp, ISettle} from './TradingApp.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, @@ -23,8 +28,6 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { // ====== Errors ====== - error InsufficientBalance(address token, uint256 required, uint256 available); - error InvalidAddress(); error InvalidAmount(uint256 amount); error SettlementAlreadyPerformed(bytes32 channelId); error BrokerNotParticipant(address actual, address expectedBroker); @@ -103,7 +106,7 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { INitroTypes.RecoveredVariablePart[] calldata proof, INitroTypes.RecoveredVariablePart calldata candidate ) external nonReentrant { - uint256 channelId = NitroUtils.getChannelId(fixedPart); + bytes32 channelId = NitroUtils.getChannelId(fixedPart); require(!performedSettlements[channelId], SettlementAlreadyPerformed(channelId)); require( @@ -112,16 +115,16 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { ); address trader = fixedPart.participants[0]; - (bool isStateValid, string memory reason) = tradingApp.isStateTransitionValid( + (bool isStateValid, string memory reason) = tradingApp.stateIsSupported( fixedPart, proof, candidate ); require(isStateValid, InvalidStateTransition(reason)); - ITradingStructs.Settlement memory settlement = abi.decode( + ITradingTypes.Settlement memory settlement = abi.decode( candidate.variablePart.appData, - (ITradingStructs.Settlement) + (ITradingTypes.Settlement) ); for (uint256 i = 0; i < settlement.toTrader.length; i++) { diff --git a/contracts/interfaces/IVault.sol b/contracts/interfaces/IVault.sol index 45527e45d..d03c49aea 100644 --- a/contracts/interfaces/IVault.sol +++ b/contracts/interfaces/IVault.sol @@ -47,17 +47,22 @@ interface IVault { /** * @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 token) external view returns (uint256); + 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[] calldata tokens) external view returns (uint256[] memory); + function balancesOfTokens( + address user, + address[] calldata tokens + ) external view returns (uint256[] memory); /** * @dev Deposits a specified amount of tokens or ETH into the vault. From c46d39c88a58c2ee648b0acbac73f6cbdcd435c8 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Tue, 24 Dec 2024 16:43:59 +0200 Subject: [PATCH 06/14] fix(contracts): unlock compiler version --- contracts/clearing/TradingApp.sol | 4 ++-- contracts/interfaces/ISettle.sol | 2 +- contracts/interfaces/ITradingTypes.sol | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index 07eb89768..736725058 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.22; +pragma solidity ^0.8.22; import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; @@ -10,7 +10,7 @@ import {ITradingTypes} from '../interfaces/ITradingTypes.sol'; import {NitroUtils} from '../nitro/libraries/NitroUtils.sol'; contract TradingApp is IForceMoveApp { - // TODO: add errors + // TODO: add custom errors after contract logic is finalized function stateIsSupported( FixedPart calldata fixedPart, diff --git a/contracts/interfaces/ISettle.sol b/contracts/interfaces/ISettle.sol index a71a32d4f..627ae33da 100644 --- a/contracts/interfaces/ISettle.sol +++ b/contracts/interfaces/ISettle.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.22; +pragma solidity ^0.8.22; import {INitroTypes} from '../nitro/interfaces/INitroTypes.sol'; diff --git a/contracts/interfaces/ITradingTypes.sol b/contracts/interfaces/ITradingTypes.sol index 23b685589..d69eb70d7 100644 --- a/contracts/interfaces/ITradingTypes.sol +++ b/contracts/interfaces/ITradingTypes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.22; +pragma solidity ^0.8.22; interface ITradingTypes { struct Order { From f1768f3cdc7036c857cf8678e93b999603b2b169 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Fri, 27 Dec 2024 10:53:13 +0200 Subject: [PATCH 07/14] feat(tradingapp): reduce proof size for order-response to 1 --- contracts/clearing/TradingApp.sol | 106 +++++++++++++----------------- 1 file changed, 46 insertions(+), 60 deletions(-) diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index 736725058..35b6eb43e 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -17,6 +17,7 @@ contract TradingApp is IForceMoveApp { RecoveredVariablePart[] calldata proof, RecoveredVariablePart calldata candidate ) external pure override returns (bool, string memory) { + // TODO: add liquidation state (proof.length == 0, signedBy Broker, contains Liquidation struct with Trader margin amount that goes to the Broker) // turn nums: // 0 - prefund // 1 - postfund @@ -24,6 +25,8 @@ contract TradingApp is IForceMoveApp { // 2n+1 - order response // 2n - order or settlement + // TODO: add outcome (includes only Trader's margin) validation logic + uint48 candTurnNum = candidate.variablePart.turnNum; // prefund or postfund @@ -33,79 +36,62 @@ contract TradingApp is IForceMoveApp { } bytes memory candidateData = candidate.variablePart.appData; - - // settlement uint8 signaturesNum = NitroUtils.getClaimedSignersNum(candidate.signedBy); - if ( - candTurnNum % 2 == 0 /* is either order or settlement */ && - signaturesNum == 2 /* is settlement */ && - proof.length >= 2 /* contains at least one order+response pair */ && - proof.length % 2 == 0 /* contains full pairs only, no dangling values */ - ) { - Consensus.requireConsensus(fixedPart, proof, candidate); - // Check the settlement data structure validity - ITradingTypes.Settlement memory settlement = abi.decode( - candidateData, - (ITradingTypes.Settlement) - ); - verifyProofForSettlement(settlement, proof); - return (true, ''); - } - // participant 0 signs even turns - // participant 1 signs odd turns - StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate); - require(signaturesNum == 1, 'signaturesNum != 1'); - require(proof.length == 2, 'proof.length != 2'); - (VariablePart memory proof0, VariablePart memory proof1) = ( - proof[0].variablePart, - proof[1].variablePart - ); - require(proof0.turnNum == candTurnNum - 2, 'proof0.turnNum != candTurnNum - 1'); - require(proof1.turnNum == candTurnNum - 1, 'proof1.turnNum != candTurnNum - 1'); - - // order - if (candTurnNum % 2 == 0) { - ITradingTypes.Order memory prevOrder = abi.decode( - proof0.appData, - (ITradingTypes.Order) - ); - ITradingTypes.OrderResponse memory prevOrderResponse = abi.decode( - proof1.appData, + // order or orderResponse + if (proof.length == 1) { + // participant 0 signs even turns + // participant 1 signs odd turns + StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate); + require(signaturesNum == 1, 'signaturesNum != 1'); + VariablePart memory proof0 = proof[0].variablePart; + require(proof0.turnNum == candTurnNum - 1, 'proof1.turnNum != candTurnNum - 1'); + + // 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 (prevOrderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) { + if (orderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) { require( - prevOrderResponse.orderID == prevOrder.orderID, - 'orderResponse.orderID != prevOrder.orderID, candidate is order' + orderResponse.orderID == order.orderID, + 'orderResponse.orderID != order.orderID, candidate is orderResponse' ); } - // NOTE: used just to check the data structure validity - ITradingTypes.Order memory _candOrder = abi.decode( - candidateData, - (ITradingTypes.Order) - ); return (true, ''); } - // orderResponse - // NOTE: used just to check the data structure validity - ITradingTypes.OrderResponse memory _prevOrderResponse = abi.decode( - proof0.appData, - (ITradingTypes.OrderResponse) + // settlement + require( + candTurnNum % 2 == 0 /* is either order or settlement */ && + signaturesNum == 2 /* is settlement */ && + proof.length >= 2 /* contains at least one order+response pair */ && + proof.length % 2 == 0 /* contains full pairs only, no dangling values */, + 'settlement conditions not met' ); - - ITradingTypes.Order memory order = abi.decode(proof1.appData, (ITradingTypes.Order)); - ITradingTypes.OrderResponse memory orderResponse = abi.decode( + Consensus.requireConsensus(fixedPart, proof, candidate); + // Check the settlement data structure validity + ITradingTypes.Settlement memory settlement = abi.decode( candidateData, - (ITradingTypes.OrderResponse) + (ITradingTypes.Settlement) ); - if (orderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) { - require( - orderResponse.orderID == order.orderID, - 'orderResponse.orderID != order.orderID, candidate is orderResponse' - ); - } + verifyProofForSettlement(settlement, proof); return (true, ''); } From e48cbb0d88ea3653b3839dddece12f06f6944ee4 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Fri, 27 Dec 2024 12:33:31 +0200 Subject: [PATCH 08/14] feat(tradingapp): add postfund in proof to first order --- contracts/clearing/TradingApp.sol | 38 +++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index 35b6eb43e..c5eb6f354 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -31,19 +31,31 @@ contract TradingApp is IForceMoveApp { // 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; - uint8 signaturesNum = NitroUtils.getClaimedSignersNum(candidate.signedBy); // order or orderResponse if (proof.length == 1) { + // first order + if (candidate.variablePart.turnNum == 2) { + require(proof[0].variablePart.turnNum == 1, 'proof[0].turnNum != 1'); + _requireStateConsensus(fixedPart, 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); - require(signaturesNum == 1, 'signaturesNum != 1'); VariablePart memory proof0 = proof[0].variablePart; require(proof0.turnNum == candTurnNum - 1, 'proof1.turnNum != candTurnNum - 1'); @@ -79,23 +91,33 @@ contract TradingApp is IForceMoveApp { // settlement require( - candTurnNum % 2 == 0 /* is either order or settlement */ && - signaturesNum == 2 /* is settlement */ && + candTurnNum % 2 == 0 /* is settlement */ && proof.length >= 2 /* contains at least one order+response pair */ && proof.length % 2 == 0 /* contains full pairs only, no dangling values */, 'settlement conditions not met' ); - Consensus.requireConsensus(fixedPart, proof, candidate); + _requireStateConsensus(fixedPart, candidate); // Check the settlement data structure validity ITradingTypes.Settlement memory settlement = abi.decode( candidateData, (ITradingTypes.Settlement) ); - verifyProofForSettlement(settlement, proof); + _verifyProofForSettlement(fixedPart, settlement, proof); return (true, ''); } - function verifyProofForSettlement( + function _requireStateConsensus( + FixedPart calldata fixedPart, + RecoveredVariablePart calldata candidate + ) internal pure { + require( + NitroUtils.getClaimedSignersNum(candidate.signedBy) == fixedPart.participants.length, + '!unanimous' + ); + } + + function _verifyProofForSettlement( + FixedPart calldata fixedPart, ITradingTypes.Settlement memory settlement, RecoveredVariablePart[] calldata proof ) internal pure { @@ -105,6 +127,8 @@ contract TradingApp is IForceMoveApp { 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'); From b4f325200b756c465b49b57795a6a6358cab96b8 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Fri, 27 Dec 2024 14:51:37 +0200 Subject: [PATCH 09/14] test(tradingapp): add success tests --- contracts/clearing/TradingApp.sol | 31 ++--- foundry.toml | 1 - test/clearing/TradingApp.t.sol | 210 ++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 21 deletions(-) create mode 100644 test/clearing/TradingApp.t.sol diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index c5eb6f354..8591620d6 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -42,8 +42,12 @@ contract TradingApp is IForceMoveApp { if (proof.length == 1) { // first order if (candidate.variablePart.turnNum == 2) { - require(proof[0].variablePart.turnNum == 1, 'proof[0].turnNum != 1'); - _requireStateConsensus(fixedPart, proof[0]); + 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( @@ -57,7 +61,6 @@ contract TradingApp is IForceMoveApp { // participant 1 signs odd turns StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate); VariablePart memory proof0 = proof[0].variablePart; - require(proof0.turnNum == candTurnNum - 1, 'proof1.turnNum != candTurnNum - 1'); // order if (candTurnNum % 2 == 0) { @@ -81,10 +84,7 @@ contract TradingApp is IForceMoveApp { (ITradingTypes.OrderResponse) ); if (orderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) { - require( - orderResponse.orderID == order.orderID, - 'orderResponse.orderID != order.orderID, candidate is orderResponse' - ); + require(orderResponse.orderID == order.orderID, 'order and response IDs mismatch'); } return (true, ''); } @@ -96,7 +96,8 @@ contract TradingApp is IForceMoveApp { proof.length % 2 == 0 /* contains full pairs only, no dangling values */, 'settlement conditions not met' ); - _requireStateConsensus(fixedPart, candidate); + // check consensus of candidate + Consensus.requireConsensus(fixedPart, new RecoveredVariablePart[](0), candidate); // Check the settlement data structure validity ITradingTypes.Settlement memory settlement = abi.decode( candidateData, @@ -106,16 +107,6 @@ contract TradingApp is IForceMoveApp { return (true, ''); } - function _requireStateConsensus( - FixedPart calldata fixedPart, - RecoveredVariablePart calldata candidate - ) internal pure { - require( - NitroUtils.getClaimedSignersNum(candidate.signedBy) == fixedPart.participants.length, - '!unanimous' - ); - } - function _verifyProofForSettlement( FixedPart calldata fixedPart, ITradingTypes.Settlement memory settlement, @@ -142,7 +133,7 @@ contract TradingApp is IForceMoveApp { // 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 do not match'); + require(orderResponse.orderID == order.orderID, 'order and response IDs mismatch'); proofDataHashes[i] = keccak256(currProof.appData); proofDataHashes[i + 1] = keccak256(nextProof.appData); @@ -150,6 +141,6 @@ contract TradingApp is IForceMoveApp { } bytes32 ordersChecksum = keccak256(abi.encode(proofDataHashes)); - require(ordersChecksum == settlement.ordersChecksum, 'proof has been tampered with'); + require(ordersChecksum == settlement.ordersChecksum, 'settlement checksum mismatch'); } } 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..fcb2c74b5 --- /dev/null +++ b/test/clearing/TradingApp.t.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Test, console} from 'forge-std/Test.sol'; + +import {TradingApp} from '../../contracts/clearing/TradingApp.sol'; +import {ITradingTypes} from '../../contracts/interfaces/ITradingTypes.sol'; +import {INitroTypes} from '../../contracts/nitro/interfaces/INitroTypes.sol'; +import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.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; + + 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 + }); + } + + function createRVP( + bytes memory appData, + uint48 turnNum, + bool isFinal, + uint8[] memory signedByIndices + ) public pure returns (INitroTypes.RecoveredVariablePart memory) { + Outcome.SingleAssetExit[] memory outcome = new Outcome.SingleAssetExit[](0); + 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(new bytes(0), 1, false, newUint8_2(0, 1)); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + 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( + abi.encode(ITradingTypes.Order({orderID: bytes32('order1')})), + 2, + false, + newUint8_1(0) + ); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + 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( + abi.encode( + ITradingTypes.OrderResponse({ + orderID: bytes32('order1'), + responseType: ITradingTypes.OrderResponseType.ACCEPT + }) + ), + 3, + false, + newUint8_1(1) + ); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + 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_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(abi.encode(order1), 2, false, newUint8_1(0)); + proof[1] = createRVP(abi.encode(response1), 3, false, newUint8_1(1)); + proof[2] = createRVP(abi.encode(order2), 4, false, newUint8_1(0)); + proof[3] = createRVP(abi.encode(response2), 5, false, newUint8_1(1)); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + abi.encode(settlement), + 6, + false, + newUint8_2(0, 1) + ); + + (bool supported, string memory reason) = tradingApp.stateIsSupported( + fixedPart, + proof, + candidate + ); + assertTrue(supported); + assertEq(reason, ''); + } +} From 01bd771498542bc629c4cd9ef6d50720495a22d2 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Fri, 27 Dec 2024 14:56:03 +0200 Subject: [PATCH 10/14] feat(brokervault): add msg.sender = broker req for withdrawal --- contracts/clearing/BrokerVault.sol | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/clearing/BrokerVault.sol b/contracts/clearing/BrokerVault.sol index 2638f611d..35cd86901 100644 --- a/contracts/clearing/BrokerVault.sol +++ b/contracts/clearing/BrokerVault.sol @@ -28,7 +28,7 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { // ====== Errors ====== - error InvalidAmount(uint256 amount); + error UnauthorizedWithdrawal(); error SettlementAlreadyPerformed(bytes32 channelId); error BrokerNotParticipant(address actual, address expectedBroker); @@ -72,19 +72,24 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { // ---------- 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(msg.sender, address(this), amount); + IERC20(token).safeTransferFromaccount, address(this), amount); } - emit Deposited(msg.sender, token, 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)); @@ -92,13 +97,13 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { 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, ) = msg.sender.call{value: amount}(''); + (bool success, ) = account.call{value: amount}(''); require(success, NativeTransferFailed()); } else { - IERC20(token).safeTransfer(msg.sender, amount); + IERC20(token).safeTransfer(account, amount); } - emit Withdrawn(msg.sender, token, amount); + emit Withdrawn(account, token, amount); } function settle( From e19ee390141d1198cf98d680e71fde3b8ef3561e Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 30 Dec 2024 11:52:15 +0200 Subject: [PATCH 11/14] fix(brokervault): compiler error --- contracts/clearing/BrokerVault.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/clearing/BrokerVault.sol b/contracts/clearing/BrokerVault.sol index 35cd86901..cbb80c737 100644 --- a/contracts/clearing/BrokerVault.sol +++ b/contracts/clearing/BrokerVault.sol @@ -80,7 +80,7 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { } else { require(msg.value == 0, IncorrectValue()); _balances[token] += amount; - IERC20(token).safeTransferFromaccount, address(this), amount); + IERC20(token).safeTransferFrom(account, address(this), amount); } emit Deposited(account, token, amount); From 09c0e70e3bc04ad5a7ad84d6c9369ce618c35ffa Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 30 Dec 2024 11:52:19 +0200 Subject: [PATCH 12/14] feat(tradingvault): add liqudiation state --- contracts/clearing/TradingApp.sol | 87 +++++++++++++++++++++++++------ test/clearing/TradingApp.t.sol | 34 +++++++++++- 2 files changed, 103 insertions(+), 18 deletions(-) diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index 8591620d6..44fdf87d0 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -17,7 +17,8 @@ contract TradingApp is IForceMoveApp { RecoveredVariablePart[] calldata proof, RecoveredVariablePart calldata candidate ) external pure override returns (bool, string memory) { - // TODO: add liquidation state (proof.length == 0, signedBy Broker, contains Liquidation struct with Trader margin amount that goes to the Broker) + // 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 @@ -27,6 +28,8 @@ contract TradingApp is IForceMoveApp { // TODO: add outcome (includes only Trader's margin) validation logic + require(fixedPart.participants.length == 2, 'invalid number of participants, expected 2'); + uint48 candTurnNum = candidate.variablePart.turnNum; // prefund or postfund @@ -40,6 +43,8 @@ contract TradingApp is IForceMoveApp { // order or orderResponse if (proof.length == 1) { + // TODO: validate outcome does not change + // first order if (candidate.variablePart.turnNum == 2) { require( @@ -88,23 +93,70 @@ contract TradingApp is IForceMoveApp { } 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' + ); + + // TODO: validate 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, ''); + } + } - // settlement - require( - candTurnNum % 2 == 0 /* is settlement */ && - proof.length >= 2 /* contains at least one order+response pair */ && - proof.length % 2 == 0 /* contains full pairs only, no dangling values */, - 'settlement conditions not met' - ); - // 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 _verifyProofForSettlement( @@ -112,6 +164,7 @@ contract TradingApp is IForceMoveApp { ITradingTypes.Settlement memory settlement, RecoveredVariablePart[] calldata proof ) internal pure { + // TODO: validate outcome does not change bytes32[] memory proofDataHashes = new bytes32[](proof.length); uint256 prevTurnNum = 1; // postfund state for (uint256 i = 0; i < proof.length - 1; i += 2) { diff --git a/test/clearing/TradingApp.t.sol b/test/clearing/TradingApp.t.sol index fcb2c74b5..b37997bde 100644 --- a/test/clearing/TradingApp.t.sol +++ b/test/clearing/TradingApp.t.sol @@ -3,10 +3,11 @@ 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'; -import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; contract TradingAppTest_stateIsSupported is Test { TradingApp public tradingApp; @@ -150,6 +151,37 @@ contract TradingAppTest_stateIsSupported is Test { 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(abi.encode(order1), 2, false, newUint8_1(0)); + proof[1] = createRVP(abi.encode(response1), 3, false, newUint8_1(1)); + + INitroTypes.RecoveredVariablePart memory candidate = createRVP( + new bytes(0), + 4, + false, + newUint8_1(1) + ); + + // console.log(NitroUtils.getClaimedSignersNum(candidate.signedBy)); + + (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({ From fe991a3b7d03a314e4baadbc795339f36069cca3 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 30 Dec 2024 12:56:28 +0200 Subject: [PATCH 13/14] feat(tradingapp): add outcome, validation and tests --- contracts/clearing/TradingApp.sol | 67 +++++++++++++++++++++--- test/clearing/TradingApp.t.sol | 86 +++++++++++++++++++++++++++---- 2 files changed, 137 insertions(+), 16 deletions(-) diff --git a/contracts/clearing/TradingApp.sol b/contracts/clearing/TradingApp.sol index 44fdf87d0..0f96add42 100644 --- a/contracts/clearing/TradingApp.sol +++ b/contracts/clearing/TradingApp.sol @@ -26,8 +26,6 @@ contract TradingApp is IForceMoveApp { // 2n+1 - order response // 2n - order or settlement - // TODO: add outcome (includes only Trader's margin) validation logic - require(fixedPart.participants.length == 2, 'invalid number of participants, expected 2'); uint48 candTurnNum = candidate.variablePart.turnNum; @@ -43,7 +41,12 @@ contract TradingApp is IForceMoveApp { // order or orderResponse if (proof.length == 1) { - // TODO: validate outcome does not change + _requireSingleAllocation(proof[0].variablePart.outcome); + _requireSingleAllocation(candidate.variablePart.outcome); + _requireNoAllocationAmountChange( + proof[0].variablePart.outcome, + candidate.variablePart.outcome + ); // first order if (candidate.variablePart.turnNum == 2) { @@ -137,8 +140,17 @@ contract TradingApp is IForceMoveApp { 'not signed by broker' ); - // TODO: validate outcome - + // 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 @@ -159,12 +171,41 @@ contract TradingApp is IForceMoveApp { 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 { - // TODO: validate outcome does not change bytes32[] memory proofDataHashes = new bytes32[](proof.length); uint256 prevTurnNum = 1; // postfund state for (uint256 i = 0; i < proof.length - 1; i += 2) { @@ -188,6 +229,20 @@ contract TradingApp is IForceMoveApp { // 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; diff --git a/test/clearing/TradingApp.t.sol b/test/clearing/TradingApp.t.sol index b37997bde..66a54061e 100644 --- a/test/clearing/TradingApp.t.sol +++ b/test/clearing/TradingApp.t.sol @@ -15,6 +15,8 @@ contract TradingAppTest_stateIsSupported is Test { 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); @@ -29,13 +31,68 @@ contract TradingAppTest_stateIsSupported is Test { }); } + // 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) { - Outcome.SingleAssetExit[] memory outcome = new Outcome.SingleAssetExit[](0); INitroTypes.VariablePart memory variablePart = INitroTypes.VariablePart({ outcome: outcome, appData: appData, @@ -67,9 +124,10 @@ contract TradingAppTest_stateIsSupported is Test { 1 ); // postfund - proof[0] = createRVP(new bytes(0), 1, false, newUint8_2(0, 1)); + 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, @@ -91,6 +149,7 @@ contract TradingAppTest_stateIsSupported is Test { ); // order proof[0] = createRVP( + traderOutcome(), abi.encode(ITradingTypes.Order({orderID: bytes32('order1')})), 2, false, @@ -98,6 +157,7 @@ contract TradingAppTest_stateIsSupported is Test { ); INitroTypes.RecoveredVariablePart memory candidate = createRVP( + traderOutcome(), abi.encode( ITradingTypes.OrderResponse({ orderID: bytes32('order1'), @@ -124,6 +184,7 @@ contract TradingAppTest_stateIsSupported is Test { ); // order proof[0] = createRVP( + traderOutcome(), abi.encode( ITradingTypes.OrderResponse({ orderID: bytes32('order1'), @@ -136,6 +197,7 @@ contract TradingAppTest_stateIsSupported is Test { ); INitroTypes.RecoveredVariablePart memory candidate = createRVP( + traderOutcome(), abi.encode(ITradingTypes.Order({orderID: bytes32('order2')})), 4, false, @@ -161,18 +223,21 @@ contract TradingAppTest_stateIsSupported is Test { INitroTypes.RecoveredVariablePart[] memory proof = new INitroTypes.RecoveredVariablePart[]( 2 ); - proof[0] = createRVP(abi.encode(order1), 2, false, newUint8_1(0)); - proof[1] = createRVP(abi.encode(response1), 3, false, newUint8_1(1)); + 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) ); - // console.log(NitroUtils.getClaimedSignersNum(candidate.signedBy)); - (bool supported, string memory reason) = tradingApp.stateIsSupported( fixedPart, proof, @@ -219,12 +284,13 @@ contract TradingAppTest_stateIsSupported is Test { INitroTypes.RecoveredVariablePart[] memory proof = new INitroTypes.RecoveredVariablePart[]( 4 ); - proof[0] = createRVP(abi.encode(order1), 2, false, newUint8_1(0)); - proof[1] = createRVP(abi.encode(response1), 3, false, newUint8_1(1)); - proof[2] = createRVP(abi.encode(order2), 4, false, newUint8_1(0)); - proof[3] = createRVP(abi.encode(response2), 5, false, newUint8_1(1)); + 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, From a55a6a36bf4fefbfc8e749a1c4b9be6d40ba1368 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 6 Jan 2025 12:44:10 +0200 Subject: [PATCH 14/14] feat(brokervault): change ISettle event, add Deposit, Withdrawal in settle --- contracts/clearing/BrokerVault.sol | 4 +++- contracts/interfaces/ISettle.sol | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/contracts/clearing/BrokerVault.sol b/contracts/clearing/BrokerVault.sol index cbb80c737..29aad8fdd 100644 --- a/contracts/clearing/BrokerVault.sol +++ b/contracts/clearing/BrokerVault.sol @@ -141,6 +141,7 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { ); IERC20(token).safeTransfer(trader, amount); _balances[token] -= amount; + emit Withdrawn(broker, token, amount); } for (uint256 i = 0; i < settlement.toBroker.length; i++) { @@ -148,9 +149,10 @@ contract BrokerVault is IVault, ISettle, Ownable2Step, ReentrancyGuard { 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); + emit Settled(trader, broker, channelId, settlement.ordersChecksum); } } diff --git a/contracts/interfaces/ISettle.sol b/contracts/interfaces/ISettle.sol index 627ae33da..1a1569ccf 100644 --- a/contracts/interfaces/ISettle.sol +++ b/contracts/interfaces/ISettle.sol @@ -16,7 +16,12 @@ interface ISettle { * @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); + event Settled( + address indexed trader, + address indexed broker, + bytes32 indexed channelId, + bytes32 settlementId + ); // ========== Errors ==========