// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
contract USDTCrossMarginVault
is AccessControl,
ReentrancyGuard,
Pausable
{
using SafeERC20 for IERC20;
// =============================================================
// ROLES
// =============================================================
bytes32 public constant RISK_ENGINE_ROLE =
keccak256("RISK_ENGINE_ROLE");
bytes32 public constant SETTLEMENT_ROLE =
keccak256("SETTLEMENT_ROLE");
bytes32 public constant LIQUIDATION_ROLE =
keccak256("LIQUIDATION_ROLE");
bytes32 public constant TREASURY_ROLE =
keccak256("TREASURY_ROLE");
// =============================================================
// CONSTANTS
// =============================================================
uint256 public constant PRECISION = 1e18;
// =============================================================
// STATE
// =============================================================
IERC20 public immutable USDT;
uint256 public totalDeposited;
uint256 public totalLockedMargin;
uint256 public insuranceFund;
// =============================================================
// ACCOUNT
// =============================================================
struct Account {
uint256 deposited;
// Margin reserved for open positions.
uint256 lockedMargin;
// Net realized PnL.
int256 realizedPnL;
// Trading fees paid.
uint256 feesPaid;
// Funding paid/received.
int256 fundingPnL;
// Number of active positions.
uint256 activePositions;
}
mapping(address => Account) public accounts;
// =============================================================
// POSITION
// =============================================================
enum Side {
LONG,
SHORT
}
struct Position {
bytes32 positionId;
address trader;
Side side;
uint256 size;
uint256 entryPrice;
uint256 initialMargin;
uint256 maintenanceMargin;
bool active;
}
mapping(bytes32 => Position) public positions;
mapping(address => bytes32[]) public traderPositions;
// =============================================================
// EVENTS
// =============================================================
event Deposited(
address indexed trader,
uint256 amount
);
event Withdrawn(
address indexed trader,
uint256 amount
);
event MarginLocked(
address indexed trader,
uint256 amount
);
event MarginUnlocked(
address indexed trader,
uint256 amount
);
event PositionOpened(
bytes32 indexed positionId,
address indexed trader,
Side side,
uint256 size,
uint256 entryPrice,
uint256 margin
);
event PositionClosed(
bytes32 indexed positionId,
address indexed trader,
int256 pnl
);
event PnLSettled(
address indexed trader,
int256 pnl
);
event FeeCharged(
address indexed trader,
uint256 fee
);
event FundingSettled(
address indexed trader,
int256 funding
);
event Liquidated(
bytes32 indexed positionId,
address indexed trader,
uint256 remainingCollateral
);
event InsuranceFundDeposited(
uint256 amount
);
event InsuranceFundUsed(
uint256 amount
);
// =============================================================
// CONSTRUCTOR
// =============================================================
constructor(
address usdt,
address admin
) {
require(
usdt != address(0),
"Invalid USDT"
);
require(
admin != address(0),
"Invalid admin"
);
USDT = IERC20(usdt);
_grantRole(
DEFAULT_ADMIN_ROLE,
admin
);
}
// =============================================================
// USER DEPOSIT
// =============================================================
function deposit(
uint256 amount
)
external
nonReentrant
whenNotPaused
{
require(
amount > 0,
"Amount is zero"
);
USDT.safeTransferFrom(
msg.sender,
address(this),
amount
);
accounts[msg.sender].deposited += amount;
totalDeposited += amount;
emit Deposited(
msg.sender,
amount
);
}
// =============================================================
// USER WITHDRAW
// =============================================================
function withdraw(
uint256 amount
)
external
nonReentrant
whenNotPaused
{
Account storage account =
accounts[msg.sender];
uint256 available =
availableMargin(msg.sender);
require(
amount <= available,
"Insufficient free margin"
);
account.deposited -= amount;
totalDeposited -= amount;
USDT.safeTransfer(
msg.sender,
amount
);
emit Withdrawn(
msg.sender,
amount
);
}
// =============================================================
// LOCK MARGIN
// =============================================================
function lockMargin(
address trader,
uint256 amount
)
external
onlyRole(RISK_ENGINE_ROLE)
whenNotPaused
{
_lockMargin(trader, amount);
}
function _lockMargin(
address trader,
uint256 amount
)
internal
{
Account storage account =
accounts[trader];
require(
amount <= availableMargin(trader),
"Insufficient margin"
);
account.lockedMargin += amount;
totalLockedMargin += amount;
emit MarginLocked(
trader,
amount
);
}
// =============================================================
// UNLOCK MARGIN
// =============================================================
function unlockMargin(
address trader,
uint256 amount
)
external
onlyRole(RISK_ENGINE_ROLE)
{
Account storage account =
accounts[trader];
require(
amount <= account.lockedMargin,
"Exceeds locked margin"
);
account.lockedMargin -= amount;
totalLockedMargin -= amount;
emit MarginUnlocked(
trader,
amount
);
}
// =============================================================
// OPEN POSITION
// =============================================================
function registerPosition(
bytes32 positionId,
address trader,
Side side,
uint256 size,
uint256 entryPrice,
uint256 initialMargin,
uint256 maintenanceMargin
)
external
onlyRole(RISK_ENGINE_ROLE)
whenNotPaused
{
require(
positionId != bytes32(0),
"Invalid position"
);
require(
!positions[positionId].active,
"Position exists"
);
require(
size > 0,
"Invalid size"
);
require(
entryPrice > 0,
"Invalid price"
);
require(
initialMargin > 0,
"Invalid margin"
);
_lockMargin(
trader,
initialMargin
);
positions[positionId] = Position({
positionId: positionId,
trader: trader,
side: side,
size: size,
entryPrice: entryPrice,
initialMargin: initialMargin,
maintenanceMargin: maintenanceMargin,
active: true
});
traderPositions[trader].push(
positionId
);
accounts[trader].activePositions++;
emit PositionOpened(
positionId,
trader,
side,
size,
entryPrice,
initialMargin
);
}
// =============================================================
// SETTLE REALIZED PNL
// =============================================================
function settlePnL(
address trader,
int256 pnl
)
external
onlyRole(SETTLEMENT_ROLE)
whenNotPaused
{
Account storage account =
accounts[trader];
if (pnl > 0) {
uint256 profit =
uint256(pnl);
require(
USDT.balanceOf(address(this))
>= totalDeposited + profit,
"Insufficient vault liquidity"
);
account.deposited += profit;
totalDeposited += profit;
} else if (pnl < 0) {
uint256 loss =
uint256(-pnl);
require(
loss <= account.deposited,
"Loss exceeds collateral"
);
account.deposited -= loss;
totalDeposited -= loss;
}
account.realizedPnL += pnl;
emit PnLSettled(
trader,
pnl
);
}
// =============================================================
// FEE
// =============================================================
function chargeFee(
address trader,
uint256 fee
)
external
onlyRole(SETTLEMENT_ROLE)
{
Account storage account =
accounts[trader];
require(
fee <= availableEquity(trader),
"Insufficient equity"
);
account.deposited -= fee;
account.feesPaid += fee;
totalDeposited -= fee;
emit FeeCharged(
trader,
fee
);
}
// =============================================================
// FUNDING
// =============================================================
function settleFunding(
address trader,
int256 funding
)
external
onlyRole(SETTLEMENT_ROLE)
{
Account storage account =
accounts[trader];
if (funding > 0) {
uint256 received =
uint256(funding);
account.deposited += received;
totalDeposited += received;
} else if (funding < 0) {
uint256 payment =
uint256(-funding);
require(
payment <= account.deposited,
"Insufficient collateral"
);
account.deposited -= payment;
totalDeposited -= payment;
}
account.fundingPnL += funding;
emit FundingSettled(
trader,
funding
);
}
// =============================================================
// CLOSE POSITION
// =============================================================
function closePosition(
bytes32 positionId,
int256 pnl
)
external
onlyRole(SETTLEMENT_ROLE)
whenNotPaused
{
Position storage position =
positions[positionId];
require(
position.active,
"Position inactive"
);
address trader =
position.trader;
uint256 margin =
position.initialMargin;
// First settle PnL.
_settlePnLInternal(
trader,
pnl
);
// Release margin.
Account storage account =
accounts[trader];
require(
margin <= account.lockedMargin,
"Margin error"
);
account.lockedMargin -= margin;
totalLockedMargin -= margin;
account.activePositions--;
position.active = false;
emit PositionClosed(
positionId,
trader,
pnl
);
}
// =============================================================
// LIQUIDATION
// =============================================================
function liquidate(
bytes32 positionId,
int256 pnl
)
external
onlyRole(LIQUIDATION_ROLE)
whenNotPaused
{
Position storage position =
positions[positionId];
require(
position.active,
"Position inactive"
);
address trader =
position.trader;
Account storage account =
accounts[trader];
// Settle loss/profit.
if (pnl < 0) {
uint256 loss =
uint256(-pnl);
if (loss >= account.deposited) {
uint256 remaining =
account.deposited;
totalDeposited -= remaining;
account.deposited = 0;
// Bad debt can be handled
// by the insurance fund.
uint256 badDebt =
loss - remaining;
if (badDebt > 0) {
_useInsuranceFund(
badDebt
);
}
} else {
account.deposited -= loss;
totalDeposited -= loss;
}
} else {
uint256 profit =
uint256(pnl);
account.deposited += profit;
totalDeposited += profit;
}
uint256 margin =
position.initialMargin;
if (margin <= account.lockedMargin) {
account.lockedMargin -= margin;
totalLockedMargin -= margin;
}
account.activePositions--;
position.active = false;
emit Liquidated(
positionId,
trader,
account.deposited
);
}
// =============================================================
// INTERNAL PNL
// =============================================================
function _settlePnLInternal(
address trader,
int256 pnl
)
internal
{
Account storage account =
accounts[trader];
if (pnl > 0) {
uint256 profit =
uint256(pnl);
account.deposited += profit;
totalDeposited += profit;
} else if (pnl < 0) {
uint256 loss =
uint256(-pnl);
require(
loss <= account.deposited,
"Insufficient collateral"
);
account.deposited -= loss;
totalDeposited -= loss;
}
account.realizedPnL += pnl;
emit PnLSettled(
trader,
pnl
);
}
// =============================================================
// AVAILABLE MARGIN
// =============================================================
function availableMargin(
address trader
)
public
view
returns (uint256)
{
Account memory account =
accounts[trader];
if (
account.deposited
<= account.lockedMargin
) {
return 0;
}
return
account.deposited
- account.lockedMargin;
}
// =============================================================
// AVAILABLE EQUITY
// =============================================================
function availableEquity(
address trader
)
public
view
returns (uint256)
{
return accounts[trader].deposited;
}
// =============================================================
// INSURANCE FUND
// =============================================================
function depositInsuranceFund(
uint256 amount
)
external
onlyRole(TREASURY_ROLE)
nonReentrant
{
USDT.safeTransferFrom(
msg.sender,
address(this),
amount
);
insuranceFund += amount;
emit InsuranceFundDeposited(
amount
);
}
function _useInsuranceFund(
uint256 amount
)
internal
{
require(
amount <= insuranceFund,
"Insurance fund insufficient"
);
insuranceFund -= amount;
emit InsuranceFundUsed(
amount
);
}
// =============================================================
// ACCOUNT VIEW
// =============================================================
function getAccount(
address trader
)
external
view
returns (
uint256 deposited,
uint256 lockedMargin,
uint256 available,
int256 realizedPnL,
uint256 fees,
int256 funding,
uint256 activePositions
)
{
Account memory account =
accounts[trader];
deposited =
account.deposited;
lockedMargin =
account.lockedMargin;
available =
availableMargin(trader);
realizedPnL =
account.realizedPnL;
fees =
account.feesPaid;
funding =
account.fundingPnL;
activePositions =
account.activePositions;
}
// =============================================================
// PAUSE
// =============================================================
function pause()
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
_pause();
}
function unpause()
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
_unpause();
}
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)