Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 120
}
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ src = 'src'
out = 'out'
libs = ['lib']
test = 'test'
solc = "0.8.26"
auto_detect_solc = true
evm_version = "cancun"
optimizer = true
optimizer_runs = 1000000
Expand Down
22 changes: 22 additions & 0 deletions src/interfaces/IAuthorizableV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IAuthorizeV2} from "./IAuthorizeV2.sol";

/**
* @title IAuthorizable
* @notice Interface for a contract that is using Authorize logic.
*/
interface IAuthorizableV2 {
/**
* @notice Emitted when the authorizer contract is changed.
* @param newAuthorizer The address of the new authorizer contract.
*/
event AuthorizerChanged(IAuthorizeV2 indexed newAuthorizer);

/**
* @dev Sets the authorizer contract.
* @param newAuthorizer The address of the authorizer contract.
*/
function setAuthorizer(IAuthorizeV2 newAuthorizer) external;
}
12 changes: 12 additions & 0 deletions src/interfaces/IAuthorizeV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* @title IAuthorize
* @notice Interface for an authorization contract that validates if certain actions are allowed.
*/
interface IAuthorizeV2 {
error Unauthorized(bytes authData);

function authorize(bytes calldata authData) external view returns (bool);
}
114 changes: 114 additions & 0 deletions src/interfaces/ITradingVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// interface IFundingSource {
// allowance(address token, address owner, address spender) external view returns (uint256);
// setAllowanceFor(address token, address spender, uint256 amount) external;
// pullFrom(address otherFundingSource, address token, address owner, uint256 amount) external;
// transferTo(address token, address to, uint256 amount) external;
// }

interface ITradingStructs {
/// @notice Defines where account's funds are taken from and sent to.
enum FundingLocation {
// TODO: maybe rename to "External", that will need to implement a specific interface, that allows pulling funds
// This way:
// 1. both TradingVault and LiquidityMiningVault are the same funding source.
// 2. other External funding sources can be easily added.
// This also requires passing an additional "address" parameter, to know which source to pull funds from.

// What if the "FundingSource" is an address? This way, if the "trader" == "fundingSource", the funds are taken from the trader's balance.
// Otherwise, the contract at the "fundingSource" address is called to pull funds from the trader's balance.

// Continuing this idea, "pulling funds" is basically calling "asset.TransferFrom(from, to, amount)",
// and means there is no need to differentiate between `Account` and `External` funding sources.
// NOTE: ETH does not have "transferFrom" method, so it must be handled separately or used Wrapped.
// Having said that, all funding sources must support an "approval" feature, including TradingVault and LiquidityMiningVault.
// Also, in case trader and broker settle between different EFSs, each of them must be aware when funds are transferred to them.
// This is impossible to achieve with a simple ERC20 transfer, therefore, EFSs must implement a specific interface that
// not only transfer funds, but if a destination is another EFS, it must notify the destination about the transfer.
// This can be done by calling a destination EFS and asking it to pull funds.

// even if the funding source of both actors is the same, it will pull funds from itself successfully.

// The problem is that in such case users in each EFS must approve funds to all possible other EFSs, which is not convenient.
// A move convenient method would be for the TradingVault to orchestrate this process like an intermediary.
// This way, users will need to approve funds in their respective EFSs only to the TradingVault, and the TradingVault will
// handle the exchange.

// This also solves the transfer from an Address to an EFS, as the Address will approve the TradingVault to pull funds from it,
// and the TradingVault will handle the transfer to the EFS.

/// @dev funds are taken from the TradingVault account's balance
TradingVault,
/// @dev funds are pulled from the account's token balance
Address
}

struct Session {
bool isActive;
uint256 nonce;
}

struct Allocation {
address asset;
uint256 amount;
}

struct Funding {
Allocation allocation;
FundingLocation source;
}

struct Intent {
address trader;
Allocation[] allocations;
uint256 nonce;
}

struct Outcome {
address trader;
// NOTE: can be extended to a funding source per position
FundingLocation traderFundingDestination;
FundingLocation brokerFundingDestination;
Funding[] traderGives;
Funding[] brokerGives;
uint256 nonce;
}
}

/**
* @title ITradingTerminal
* @author nksazonov
* @notice An ownerful implementation of a trading terminal that allows to start and end trading sessions, settle traders and liquidate.
*/
interface ITradingVault {
event Deposited(address indexed user, address indexed token, uint256 amount);
event Withdrawn(address indexed user, address indexed token, uint256 amount);

event Settled(address indexed trader, uint256 nonce);
event Liquidated(address indexed trader, uint256 nonce);

error InvalidAddress();
error IncorrectValue();
error InvalidAmount();
error InsufficientBalance(address token, uint256 required, uint256 available);
error NativeTransferFailed();

error NonceMismatch(uint256 expected, uint256 actual);
error InvalidFundingLocation();
error InvalidAssetOutcome();

function balanceOf(address user, address token) external view returns (uint256);

function balancesOfTokens(address user, address[] calldata tokens) external view returns (uint256[] memory);

// NOTE: added a possibility to batch-deposit
function deposit(ITradingStructs.Intent calldata intent) external payable;

function withdraw(ITradingStructs.Intent calldata intent, bytes calldata additionalAuthData) external;

function settle(ITradingStructs.Outcome calldata outcome, bytes calldata additionalAuthData) external;

function liquidate(ITradingStructs.Outcome calldata outcome, bytes calldata additionalAuthData) external;
}
59 changes: 59 additions & 0 deletions src/vault/TradingVaultAuthorizer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";

import {IAuthorizeV2} from "../interfaces/IAuthorizeV2.sol";
import {ITradingVault, ITradingStructs} from "../interfaces/ITradingVault.sol";

contract TradingVaultAuthorizer is IAuthorizeV2 {
using MessageHashUtils for bytes32;
using ECDSA for bytes32;

error InvalidSignature();

address public immutable broker;

constructor(address broker_) {
broker = broker_;
}

function authorize(bytes calldata authData) external view returns (bool) {
bytes8 mode = bytes8(authData[:8]);
bytes memory authData_ = authData[8:];
bytes memory data;
bytes memory signature;

if (mode == ITradingVault.withdraw.selector) {
ITradingStructs.Intent memory intent;
(intent, signature) = abi.decode(authData_, (ITradingStructs.Intent, bytes));
data = abi.encode(intent);
} else if (mode == ITradingVault.settle.selector) {
ITradingStructs.Outcome memory outcome;
(outcome, signature) = abi.decode(authData_, (ITradingStructs.Outcome, bytes));
data = abi.encode(outcome, ITradingVault.settle.selector);
} else if (mode == ITradingVault.liquidate.selector) {
ITradingStructs.Outcome memory outcome;
(outcome, signature) = abi.decode(authData_, (ITradingStructs.Outcome, bytes));
data = abi.encode(outcome, ITradingVault.liquidate.selector);
} else {
revert IAuthorizeV2.Unauthorized(authData);
}

_requireValidSigner(broker, data, signature);
return true;
}

function _requireValidSigner(address expectedSigner, bytes memory message, bytes memory sig) internal view {
bytes32 hash = keccak256(message);
if (expectedSigner.code.length == 0) {
address recovered = hash.toEthSignedMessageHash().recover(sig);
require(recovered == expectedSigner, InvalidSignature());
} else {
bytes4 value = IERC1271(expectedSigner).isValidSignature(hash, sig);
require(value == IERC1271.isValidSignature.selector, InvalidSignature());
}
}
}
Loading
Loading