Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tradingapp): TradingApp nitro force move app #455

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions contracts/clearing/BrokerVault.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
148 changes: 148 additions & 0 deletions contracts/clearing/TradingApp.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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';

contract TradingApp is IForceMoveApp {
// TODO: add errors

function stateIsSupported(
FixedPart calldata fixedPart,
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
// 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
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,
(ITradingTypes.OrderResponse)
);
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
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)
);

ITradingTypes.Order memory order = abi.decode(proof1.appData, (ITradingTypes.Order));
ITradingTypes.OrderResponse memory orderResponse = abi.decode(
candidateData,
(ITradingTypes.OrderResponse)
);
if (orderResponse.responseType == ITradingTypes.OrderResponseType.ACCEPT) {
require(
orderResponse.orderID == order.orderID,
'orderResponse.orderID != order.orderID, candidate is orderResponse'
);
}
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');
}
}
38 changes: 38 additions & 0 deletions contracts/interfaces/ISettle.sol
Original file line number Diff line number Diff line change
@@ -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;
}
31 changes: 31 additions & 0 deletions contracts/interfaces/ITradingTypes.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading