From 04de04e32c84f561e8018341895f270861baf603 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Nov 2024 12:28:28 +0200 Subject: [PATCH 1/5] build(config): turn on auto-detect-solc option --- foundry.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index e30dc2f..4c3d885 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,8 +3,8 @@ src = 'src' out = 'out' libs = ['lib'] test = 'test' -solc = "0.8.26" -evm_version = "cancun" +auto_detect_solc = true +evm_version = "paris" optimizer = true optimizer_runs = 1000000 gas_price = 1000000000 From f80238217979ba488b2c6f4ab2f8b9c7689304a8 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Nov 2024 12:29:14 +0200 Subject: [PATCH 2/5] feat(tradingvault): add TradingVault, ITradingVault --- src/interfaces/ITradingVault.sol | 143 ++++++++++++++ src/vault/TravingVault.sol | 307 +++++++++++++++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 src/interfaces/ITradingVault.sol create mode 100644 src/vault/TravingVault.sol diff --git a/src/interfaces/ITradingVault.sol b/src/interfaces/ITradingVault.sol new file mode 100644 index 0000000..a91ea86 --- /dev/null +++ b/src/interfaces/ITradingVault.sol @@ -0,0 +1,143 @@ +// 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 InvalidSignature(); + 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 brokerSig + ) external; + + function settle( + ITradingStructs.Outcome calldata outcome, + bytes calldata brokerSig + ) external; + + /// @param brokerSig Broker signature over the incremented nonce of a latest settlement (either completed or liquidated) + function liquidate( + ITradingStructs.Outcome calldata outcome, + bytes calldata brokerSig + ) external; +} diff --git a/src/vault/TravingVault.sol b/src/vault/TravingVault.sol new file mode 100644 index 0000000..2d4eb80 --- /dev/null +++ b/src/vault/TravingVault.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + +import {ITradingStructs, ITradingVault} from "../interfaces/ITradingVault.sol"; + +contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { + /// @dev Using SafeERC20 to support non fully ERC20-compliant tokens, + /// that may not return a boolean value on success. + using SafeERC20 for IERC20; + using MessageHashUtils for bytes32; + using ECDSA for bytes32; + using EnumerableSet for EnumerableSet.AddressSet; + + mapping(address user => mapping(address token => uint256 balance)) + internal _balances; + mapping(address user => uint256 session) internal _nonces; + + address public broker; + + modifier notZeroAddress(address addr) { + require(addr != address(0), InvalidAddress()); + _; + } + + constructor(address owner, address broker_) Ownable(owner) { + broker = broker_; + } + + // ---------- View functions ---------- + + function balanceOf( + address user, + address token + ) external view returns (uint256) { + return _balances[user][token]; + } + + function balancesOfTokens( + address user, + address[] calldata tokens + ) external view returns (uint256[] memory) { + uint256[] memory balances = new uint256[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + balances[i] = _balances[user][tokens[i]]; + } + return balances; + } + + // ---------- Write functions ---------- + + function setBroker(address broker_) external onlyOwner { + broker = broker_; + } + + function deposit( + ITradingStructs.Intent calldata intent + ) external payable notZeroAddress(intent.trader) { + address sender = msg.sender; + address recipient = intent.trader; + uint256 nonce = _nonces[recipient]; + require(nonce == intent.nonce, NonceMismatch(nonce, intent.nonce)); + + _nonces[recipient]++; + + for (uint256 i = 0; i < intent.allocations.length; i++) { + address token = intent.allocations[i].asset; + uint256 amount = intent.allocations[i].amount; + require(amount > 0, InvalidAmount()); + + if (token == address(0)) { + require(msg.value == amount, IncorrectValue()); + _balances[recipient][address(0)] += amount; + } else { + require(msg.value == 0, IncorrectValue()); + _balances[recipient][token] += amount; + IERC20(token).safeTransferFrom(sender, address(this), amount); + } + + emit Deposited(recipient, token, amount); + } + } + + function withdraw( + ITradingStructs.Intent calldata intent, + bytes calldata brokerSig + ) external notZeroAddress(intent.trader) { + address account = intent.trader; + uint256 nonce = _nonces[account]; + require(nonce == intent.nonce, NonceMismatch(nonce, intent.nonce)); + _requireValidSigner(broker, abi.encode(intent), brokerSig); + // TODO: do we need seasons support here? + // if ( + // !_isWithdrawalGracePeriodActive( + // latestSetAuthorizerTimestamp, uint64(block.timestamp), WITHDRAWAL_GRACE_PERIOD + // ) && !authorizer.authorize(msg.sender, token, amount) + // ) { + // revert IAuthorize.Unauthorized(msg.sender, token, amount); + // } + + _nonces[account]++; + + for (uint256 i = 0; i < intent.allocations.length; i++) { + address asset = intent.allocations[i].asset; + uint256 amount = intent.allocations[i].amount; + uint256 currentBalance = _balances[account][asset]; + require( + currentBalance >= amount, + InsufficientBalance(asset, amount, currentBalance) + ); + + _balances[account][asset] -= amount; + + if (asset == address(0)) { + /// @dev using `call` instead of `transfer` to overcome 2300 gas ceiling that could make it revert with some AA wallets + (bool success, ) = account.call{value: amount}(""); + require(success, NativeTransferFailed()); + } else { + IERC20(asset).safeTransfer(account, amount); + } + + emit Withdrawn(account, asset, amount); + } + } + + function settle( + ITradingStructs.Outcome calldata outcome, + bytes calldata brokerSig + ) external { + uint256 nonce = _nonces[outcome.trader]; + require(nonce == outcome.nonce, NonceMismatch(nonce, outcome.nonce)); + _requireValidSigner( + broker, + abi.encode(outcome, ITradingVault.settle.selector), + brokerSig + ); + + _nonces[outcome.trader]++; + + _sendAssets( + outcome.trader, + broker, + outcome.brokerFundingDestination, + outcome.traderGives + ); + _sendAssets( + broker, + outcome.trader, + outcome.traderFundingDestination, + outcome.brokerGives + ); + + emit Settled(outcome.trader, nonce - 1); + } + + function liquidate( + ITradingStructs.Outcome calldata outcome, + bytes calldata brokerSig + ) external notZeroAddress(outcome.trader) { + require( + outcome.traderFundingDestination == + ITradingStructs.FundingLocation.TradingVault, + InvalidFundingLocation() + ); + require(outcome.brokerGives.length == 0, InvalidAssetOutcome()); + uint256 nonce = _nonces[outcome.trader]; + require(nonce == outcome.nonce, NonceMismatch(nonce, outcome.nonce)); + _requireValidSigner( + broker, + abi.encode(outcome, ITradingVault.liquidate.selector), + brokerSig + ); + + _nonces[outcome.trader]++; + + _sendAssets( + outcome.trader, + broker, + outcome.brokerFundingDestination, + outcome.traderGives + ); + + emit Liquidated(outcome.trader, nonce - 1); + } + + // ---------- Internal functions ---------- + function _requireValidSigner( + address expectedSigner, + bytes memory message, + bytes calldata 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() + ); + } + } + + function _checkAndVaultSwap( + address sender, + address receiver, + ITradingStructs.Allocation memory alloc + ) internal virtual { + uint256 balance = _balances[sender][alloc.asset]; + require( + balance >= alloc.amount, + InsufficientBalance(alloc.asset, alloc.amount, balance) + ); + + _balances[sender][alloc.asset] -= alloc.amount; + _balances[receiver][alloc.asset] += alloc.amount; + } + + function _checkAndVaultSendAccount( + address sender, + address receiver, + ITradingStructs.Allocation memory alloc + ) internal virtual { + uint256 balance = _balances[sender][alloc.asset]; + require( + balance >= alloc.amount, + InsufficientBalance(alloc.asset, alloc.amount, balance) + ); + + _balances[sender][alloc.asset] -= alloc.amount; + _balances[receiver][alloc.asset] += alloc.amount; + } + + function _accountSendVault( + address sender, + address receiver, + ITradingStructs.Allocation memory alloc + ) internal virtual { + IERC20(alloc.asset).safeTransferFrom( + sender, + address(this), + alloc.amount + ); + _balances[receiver][alloc.asset] += alloc.amount; + } + + function _accountSwap( + address sender, + address receiver, + ITradingStructs.Allocation memory alloc + ) internal virtual { + IERC20(alloc.asset).safeTransferFrom(sender, receiver, alloc.amount); + } + + function _sendAssets( + address sender, + address receiver, + ITradingStructs.FundingLocation receiverFD, + ITradingStructs.Funding[] memory senderGives + ) internal { + for (uint256 i = 0; i < senderGives.length; i++) { + ITradingStructs.Funding memory senderFunding = senderGives[i]; + if ( + senderFunding.source == + ITradingStructs.FundingLocation.TradingVault + ) { + if ( + receiverFD == ITradingStructs.FundingLocation.TradingVault + ) { + _checkAndVaultSwap( + sender, + receiver, + senderFunding.allocation + ); + } else { + _checkAndVaultSendAccount( + sender, + receiver, + senderFunding.allocation + ); + } + } else { + if ( + receiverFD == ITradingStructs.FundingLocation.TradingVault + ) { + _accountSendVault( + sender, + receiver, + senderFunding.allocation + ); + } else { + _accountSwap(sender, receiver, senderFunding.allocation); + } + } + } + } +} From 75e0e785065c6d488490cc403f9fac24403f5bbf Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Nov 2024 12:33:08 +0200 Subject: [PATCH 3/5] refactor(prettier): add prettier, run on TradingVault contracts --- .prettierrc | 3 + src/interfaces/ITradingVault.sol | 43 ++------ src/vault/TravingVault.sol | 173 ++++++++----------------------- 3 files changed, 56 insertions(+), 163 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..963354f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} diff --git a/src/interfaces/ITradingVault.sol b/src/interfaces/ITradingVault.sol index a91ea86..689c7b8 100644 --- a/src/interfaces/ITradingVault.sol +++ b/src/interfaces/ITradingVault.sol @@ -83,16 +83,8 @@ interface ITradingStructs { * @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 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); @@ -100,11 +92,7 @@ interface ITradingVault { error InvalidAddress(); error IncorrectValue(); error InvalidAmount(); - error InsufficientBalance( - address token, - uint256 required, - uint256 available - ); + error InsufficientBalance(address token, uint256 required, uint256 available); error NativeTransferFailed(); error InvalidSignature(); @@ -112,32 +100,17 @@ interface ITradingVault { error InvalidFundingLocation(); error InvalidAssetOutcome(); - function balanceOf( - address user, - address token - ) external view returns (uint256); + function balanceOf(address user, address token) external view returns (uint256); - function balancesOfTokens( - address user, - address[] calldata tokens - ) external view returns (uint256[] memory); + 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 brokerSig - ) external; + function withdraw(ITradingStructs.Intent calldata intent, bytes calldata brokerSig) external; - function settle( - ITradingStructs.Outcome calldata outcome, - bytes calldata brokerSig - ) external; + function settle(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) external; /// @param brokerSig Broker signature over the incremented nonce of a latest settlement (either completed or liquidated) - function liquidate( - ITradingStructs.Outcome calldata outcome, - bytes calldata brokerSig - ) external; + function liquidate(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) external; } diff --git a/src/vault/TravingVault.sol b/src/vault/TravingVault.sol index 2d4eb80..10bdafe 100644 --- a/src/vault/TravingVault.sol +++ b/src/vault/TravingVault.sol @@ -21,8 +21,7 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { using ECDSA for bytes32; using EnumerableSet for EnumerableSet.AddressSet; - mapping(address user => mapping(address token => uint256 balance)) - internal _balances; + mapping(address user => mapping(address token => uint256 balance)) internal _balances; mapping(address user => uint256 session) internal _nonces; address public broker; @@ -38,17 +37,11 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { // ---------- View functions ---------- - function balanceOf( - address user, - address token - ) external view returns (uint256) { + function balanceOf(address user, address token) external view returns (uint256) { return _balances[user][token]; } - function balancesOfTokens( - address user, - address[] calldata tokens - ) external view returns (uint256[] memory) { + function balancesOfTokens(address user, address[] calldata tokens) external view returns (uint256[] memory) { uint256[] memory balances = new uint256[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { balances[i] = _balances[user][tokens[i]]; @@ -62,9 +55,7 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { broker = broker_; } - function deposit( - ITradingStructs.Intent calldata intent - ) external payable notZeroAddress(intent.trader) { + function deposit(ITradingStructs.Intent calldata intent) external payable notZeroAddress(intent.trader) { address sender = msg.sender; address recipient = intent.trader; uint256 nonce = _nonces[recipient]; @@ -90,10 +81,10 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { } } - function withdraw( - ITradingStructs.Intent calldata intent, - bytes calldata brokerSig - ) external notZeroAddress(intent.trader) { + function withdraw(ITradingStructs.Intent calldata intent, bytes calldata brokerSig) + external + notZeroAddress(intent.trader) + { address account = intent.trader; uint256 nonce = _nonces[account]; require(nonce == intent.nonce, NonceMismatch(nonce, intent.nonce)); @@ -113,16 +104,13 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { address asset = intent.allocations[i].asset; uint256 amount = intent.allocations[i].amount; uint256 currentBalance = _balances[account][asset]; - require( - currentBalance >= amount, - InsufficientBalance(asset, amount, currentBalance) - ); + require(currentBalance >= amount, InsufficientBalance(asset, amount, currentBalance)); _balances[account][asset] -= amount; if (asset == address(0)) { /// @dev using `call` instead of `transfer` to overcome 2300 gas ceiling that could make it revert with some AA wallets - (bool success, ) = account.call{value: amount}(""); + (bool success,) = account.call{value: amount}(""); require(success, NativeTransferFailed()); } else { IERC20(asset).safeTransfer(account, amount); @@ -132,133 +120,81 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { } } - function settle( - ITradingStructs.Outcome calldata outcome, - bytes calldata brokerSig - ) external { + function settle(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) external { uint256 nonce = _nonces[outcome.trader]; require(nonce == outcome.nonce, NonceMismatch(nonce, outcome.nonce)); - _requireValidSigner( - broker, - abi.encode(outcome, ITradingVault.settle.selector), - brokerSig - ); + _requireValidSigner(broker, abi.encode(outcome, ITradingVault.settle.selector), brokerSig); _nonces[outcome.trader]++; - _sendAssets( - outcome.trader, - broker, - outcome.brokerFundingDestination, - outcome.traderGives - ); - _sendAssets( - broker, - outcome.trader, - outcome.traderFundingDestination, - outcome.brokerGives - ); + _sendAssets(outcome.trader, broker, outcome.brokerFundingDestination, outcome.traderGives); + _sendAssets(broker, outcome.trader, outcome.traderFundingDestination, outcome.brokerGives); emit Settled(outcome.trader, nonce - 1); } - function liquidate( - ITradingStructs.Outcome calldata outcome, - bytes calldata brokerSig - ) external notZeroAddress(outcome.trader) { + function liquidate(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) + external + notZeroAddress(outcome.trader) + { require( - outcome.traderFundingDestination == - ITradingStructs.FundingLocation.TradingVault, - InvalidFundingLocation() + outcome.traderFundingDestination == ITradingStructs.FundingLocation.TradingVault, InvalidFundingLocation() ); require(outcome.brokerGives.length == 0, InvalidAssetOutcome()); uint256 nonce = _nonces[outcome.trader]; require(nonce == outcome.nonce, NonceMismatch(nonce, outcome.nonce)); - _requireValidSigner( - broker, - abi.encode(outcome, ITradingVault.liquidate.selector), - brokerSig - ); + _requireValidSigner(broker, abi.encode(outcome, ITradingVault.liquidate.selector), brokerSig); _nonces[outcome.trader]++; - _sendAssets( - outcome.trader, - broker, - outcome.brokerFundingDestination, - outcome.traderGives - ); + _sendAssets(outcome.trader, broker, outcome.brokerFundingDestination, outcome.traderGives); emit Liquidated(outcome.trader, nonce - 1); } // ---------- Internal functions ---------- - function _requireValidSigner( - address expectedSigner, - bytes memory message, - bytes calldata sig - ) internal view { + function _requireValidSigner(address expectedSigner, bytes memory message, bytes calldata 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() - ); + require(value == IERC1271.isValidSignature.selector, InvalidSignature()); } } - function _checkAndVaultSwap( - address sender, - address receiver, - ITradingStructs.Allocation memory alloc - ) internal virtual { + function _checkAndVaultSwap(address sender, address receiver, ITradingStructs.Allocation memory alloc) + internal + virtual + { uint256 balance = _balances[sender][alloc.asset]; - require( - balance >= alloc.amount, - InsufficientBalance(alloc.asset, alloc.amount, balance) - ); + require(balance >= alloc.amount, InsufficientBalance(alloc.asset, alloc.amount, balance)); _balances[sender][alloc.asset] -= alloc.amount; _balances[receiver][alloc.asset] += alloc.amount; } - function _checkAndVaultSendAccount( - address sender, - address receiver, - ITradingStructs.Allocation memory alloc - ) internal virtual { + function _checkAndVaultSendAccount(address sender, address receiver, ITradingStructs.Allocation memory alloc) + internal + virtual + { uint256 balance = _balances[sender][alloc.asset]; - require( - balance >= alloc.amount, - InsufficientBalance(alloc.asset, alloc.amount, balance) - ); + require(balance >= alloc.amount, InsufficientBalance(alloc.asset, alloc.amount, balance)); _balances[sender][alloc.asset] -= alloc.amount; _balances[receiver][alloc.asset] += alloc.amount; } - function _accountSendVault( - address sender, - address receiver, - ITradingStructs.Allocation memory alloc - ) internal virtual { - IERC20(alloc.asset).safeTransferFrom( - sender, - address(this), - alloc.amount - ); + function _accountSendVault(address sender, address receiver, ITradingStructs.Allocation memory alloc) + internal + virtual + { + IERC20(alloc.asset).safeTransferFrom(sender, address(this), alloc.amount); _balances[receiver][alloc.asset] += alloc.amount; } - function _accountSwap( - address sender, - address receiver, - ITradingStructs.Allocation memory alloc - ) internal virtual { + function _accountSwap(address sender, address receiver, ITradingStructs.Allocation memory alloc) internal virtual { IERC20(alloc.asset).safeTransferFrom(sender, receiver, alloc.amount); } @@ -270,34 +206,15 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { ) internal { for (uint256 i = 0; i < senderGives.length; i++) { ITradingStructs.Funding memory senderFunding = senderGives[i]; - if ( - senderFunding.source == - ITradingStructs.FundingLocation.TradingVault - ) { - if ( - receiverFD == ITradingStructs.FundingLocation.TradingVault - ) { - _checkAndVaultSwap( - sender, - receiver, - senderFunding.allocation - ); + if (senderFunding.source == ITradingStructs.FundingLocation.TradingVault) { + if (receiverFD == ITradingStructs.FundingLocation.TradingVault) { + _checkAndVaultSwap(sender, receiver, senderFunding.allocation); } else { - _checkAndVaultSendAccount( - sender, - receiver, - senderFunding.allocation - ); + _checkAndVaultSendAccount(sender, receiver, senderFunding.allocation); } } else { - if ( - receiverFD == ITradingStructs.FundingLocation.TradingVault - ) { - _accountSendVault( - sender, - receiver, - senderFunding.allocation - ); + if (receiverFD == ITradingStructs.FundingLocation.TradingVault) { + _accountSendVault(sender, receiver, senderFunding.allocation); } else { _accountSwap(sender, receiver, senderFunding.allocation); } From 4e99ed2dcf67a1017dd870a2e9c1a72eae47e150 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Nov 2024 16:31:11 +0200 Subject: [PATCH 4/5] feat(tradingvault): extract signature verification to TradingVaultAuthorizer --- src/interfaces/IAuthorizableV2.sol | 22 ++++++ src/interfaces/IAuthorizeV2.sol | 12 +++ src/interfaces/ITradingVault.sol | 8 +- src/vault/TradingVaultAuthorizer.sol | 59 +++++++++++++++ src/vault/TravingVault.sol | 109 ++++++++++++++++----------- 5 files changed, 159 insertions(+), 51 deletions(-) create mode 100644 src/interfaces/IAuthorizableV2.sol create mode 100644 src/interfaces/IAuthorizeV2.sol create mode 100644 src/vault/TradingVaultAuthorizer.sol diff --git a/src/interfaces/IAuthorizableV2.sol b/src/interfaces/IAuthorizableV2.sol new file mode 100644 index 0000000..a708773 --- /dev/null +++ b/src/interfaces/IAuthorizableV2.sol @@ -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; +} diff --git a/src/interfaces/IAuthorizeV2.sol b/src/interfaces/IAuthorizeV2.sol new file mode 100644 index 0000000..8ed7f36 --- /dev/null +++ b/src/interfaces/IAuthorizeV2.sol @@ -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); +} diff --git a/src/interfaces/ITradingVault.sol b/src/interfaces/ITradingVault.sol index 689c7b8..0d48dee 100644 --- a/src/interfaces/ITradingVault.sol +++ b/src/interfaces/ITradingVault.sol @@ -95,7 +95,6 @@ interface ITradingVault { error InsufficientBalance(address token, uint256 required, uint256 available); error NativeTransferFailed(); - error InvalidSignature(); error NonceMismatch(uint256 expected, uint256 actual); error InvalidFundingLocation(); error InvalidAssetOutcome(); @@ -107,10 +106,9 @@ interface ITradingVault { // NOTE: added a possibility to batch-deposit function deposit(ITradingStructs.Intent calldata intent) external payable; - function withdraw(ITradingStructs.Intent calldata intent, bytes calldata brokerSig) external; + function withdraw(ITradingStructs.Intent calldata intent, bytes calldata additionalAuthData) external; - function settle(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) external; + function settle(ITradingStructs.Outcome calldata outcome, bytes calldata additionalAuthData) external; - /// @param brokerSig Broker signature over the incremented nonce of a latest settlement (either completed or liquidated) - function liquidate(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) external; + function liquidate(ITradingStructs.Outcome calldata outcome, bytes calldata additionalAuthData) external; } diff --git a/src/vault/TradingVaultAuthorizer.sol b/src/vault/TradingVaultAuthorizer.sol new file mode 100644 index 0000000..aab4aea --- /dev/null +++ b/src/vault/TradingVaultAuthorizer.sol @@ -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()); + } + } +} diff --git a/src/vault/TravingVault.sol b/src/vault/TravingVault.sol index 10bdafe..3a2c10d 100644 --- a/src/vault/TravingVault.sol +++ b/src/vault/TravingVault.sol @@ -6,33 +6,30 @@ import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import {ITradingStructs, ITradingVault} from "../interfaces/ITradingVault.sol"; +import {IAuthorizeV2} from "../interfaces/IAuthorizeV2.sol"; +import {IAuthorizableV2} from "../interfaces/IAuthorizableV2.sol"; -contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { +contract TradingVault is ITradingVault, IAuthorizableV2, ReentrancyGuard, Ownable2Step { /// @dev Using SafeERC20 to support non fully ERC20-compliant tokens, /// that may not return a boolean value on success. using SafeERC20 for IERC20; - using MessageHashUtils for bytes32; - using ECDSA for bytes32; - using EnumerableSet for EnumerableSet.AddressSet; mapping(address user => mapping(address token => uint256 balance)) internal _balances; mapping(address user => uint256 session) internal _nonces; address public broker; + IAuthorizeV2 public authorizer; modifier notZeroAddress(address addr) { require(addr != address(0), InvalidAddress()); _; } - constructor(address owner, address broker_) Ownable(owner) { + constructor(address owner, address broker_, IAuthorizeV2 authorizer_) Ownable(owner) { broker = broker_; + authorizer = authorizer_; } // ---------- View functions ---------- @@ -49,12 +46,23 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { return balances; } - // ---------- Write functions ---------- + // ---------- Owner functions ---------- function setBroker(address broker_) external onlyOwner { broker = broker_; } + function setAuthorizer(IAuthorizeV2 newAuthorizer) external onlyOwner { + if (address(newAuthorizer) == address(0)) { + revert InvalidAddress(); + } + + authorizer = newAuthorizer; + emit AuthorizerChanged(newAuthorizer); + } + + // ---------- Write functions ---------- + function deposit(ITradingStructs.Intent calldata intent) external payable notZeroAddress(intent.trader) { address sender = msg.sender; address recipient = intent.trader; @@ -81,14 +89,20 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { } } - function withdraw(ITradingStructs.Intent calldata intent, bytes calldata brokerSig) - external - notZeroAddress(intent.trader) - { + function withdraw( + ITradingStructs.Intent calldata intent, + bytes calldata additionalAuthData + ) external notZeroAddress(intent.trader) { address account = intent.trader; uint256 nonce = _nonces[account]; require(nonce == intent.nonce, NonceMismatch(nonce, intent.nonce)); - _requireValidSigner(broker, abi.encode(intent), brokerSig); + + bytes memory authData = abi.encodePacked( + ITradingVault.withdraw.selector, + abi.encode(intent, additionalAuthData) + ); + require(authorizer.authorize(authData), IAuthorizeV2.Unauthorized(authData)); + // TODO: do we need seasons support here? // if ( // !_isWithdrawalGracePeriodActive( @@ -110,7 +124,7 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { if (asset == address(0)) { /// @dev using `call` instead of `transfer` to overcome 2300 gas ceiling that could make it revert with some AA wallets - (bool success,) = account.call{value: amount}(""); + (bool success, ) = account.call{value: amount}(""); require(success, NativeTransferFailed()); } else { IERC20(asset).safeTransfer(account, amount); @@ -120,10 +134,14 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { } } - function settle(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) external { + function settle(ITradingStructs.Outcome calldata outcome, bytes calldata additionalAuthData) external { uint256 nonce = _nonces[outcome.trader]; require(nonce == outcome.nonce, NonceMismatch(nonce, outcome.nonce)); - _requireValidSigner(broker, abi.encode(outcome, ITradingVault.settle.selector), brokerSig); + bytes memory authData = abi.encodePacked( + ITradingVault.settle.selector, + abi.encode(outcome, additionalAuthData) + ); + require(authorizer.authorize(authData), IAuthorizeV2.Unauthorized(authData)); _nonces[outcome.trader]++; @@ -133,17 +151,23 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { emit Settled(outcome.trader, nonce - 1); } - function liquidate(ITradingStructs.Outcome calldata outcome, bytes calldata brokerSig) - external - notZeroAddress(outcome.trader) - { + function liquidate( + ITradingStructs.Outcome calldata outcome, + bytes calldata additionalAuthData + ) external notZeroAddress(outcome.trader) { require( - outcome.traderFundingDestination == ITradingStructs.FundingLocation.TradingVault, InvalidFundingLocation() + outcome.traderFundingDestination == ITradingStructs.FundingLocation.TradingVault, + InvalidFundingLocation() ); require(outcome.brokerGives.length == 0, InvalidAssetOutcome()); uint256 nonce = _nonces[outcome.trader]; require(nonce == outcome.nonce, NonceMismatch(nonce, outcome.nonce)); - _requireValidSigner(broker, abi.encode(outcome, ITradingVault.liquidate.selector), brokerSig); + + bytes memory authData = abi.encodePacked( + ITradingVault.liquidate.selector, + abi.encode(outcome, additionalAuthData) + ); + require(authorizer.authorize(authData), IAuthorizeV2.Unauthorized(authData)); _nonces[outcome.trader]++; @@ -153,21 +177,12 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { } // ---------- Internal functions ---------- - function _requireValidSigner(address expectedSigner, bytes memory message, bytes calldata 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()); - } - } - function _checkAndVaultSwap(address sender, address receiver, ITradingStructs.Allocation memory alloc) - internal - virtual - { + function _checkAndVaultSwap( + address sender, + address receiver, + ITradingStructs.Allocation memory alloc + ) internal virtual { uint256 balance = _balances[sender][alloc.asset]; require(balance >= alloc.amount, InsufficientBalance(alloc.asset, alloc.amount, balance)); @@ -175,10 +190,11 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { _balances[receiver][alloc.asset] += alloc.amount; } - function _checkAndVaultSendAccount(address sender, address receiver, ITradingStructs.Allocation memory alloc) - internal - virtual - { + function _checkAndVaultSendAccount( + address sender, + address receiver, + ITradingStructs.Allocation memory alloc + ) internal virtual { uint256 balance = _balances[sender][alloc.asset]; require(balance >= alloc.amount, InsufficientBalance(alloc.asset, alloc.amount, balance)); @@ -186,10 +202,11 @@ contract TradingVault is ITradingVault, ReentrancyGuard, Ownable2Step { _balances[receiver][alloc.asset] += alloc.amount; } - function _accountSendVault(address sender, address receiver, ITradingStructs.Allocation memory alloc) - internal - virtual - { + function _accountSendVault( + address sender, + address receiver, + ITradingStructs.Allocation memory alloc + ) internal virtual { IERC20(alloc.asset).safeTransferFrom(sender, address(this), alloc.amount); _balances[receiver][alloc.asset] += alloc.amount; } From 62ba25640bbdd351645c6b81dfdc1e92984011a0 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Nov 2024 16:35:17 +0200 Subject: [PATCH 5/5] build(foundry): remove paris evm version --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 4c3d885..36d0368 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ out = 'out' libs = ['lib'] test = 'test' auto_detect_solc = true -evm_version = "paris" +evm_version = "cancun" optimizer = true optimizer_runs = 1000000 gas_price = 1000000000