Skip to content
Closed
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
1 change: 1 addition & 0 deletions skills/gmx-v2/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ tags:




## Pre-flight Dependencies (auto-injected by Plugin Store CI)

> Run once per session before first use. These checks ensure required tools are installed.
Expand Down
30 changes: 16 additions & 14 deletions skills/gmx-v2/SKILL_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@
# gmx-v2 -- Skill Summary

## Overview
This skill enables trading leveraged perpetual positions and managing liquidity on GMX V2 protocol across Arbitrum and Avalanche networks. It provides comprehensive functionality for opening/closing positions, placing conditional orders, depositing/withdrawing from GM pools, and querying market data with built-in safety features including dry-run previews and automatic fee management.
This skill enables AI agents to interact with GMX V2, a decentralized perpetuals exchange, allowing users to trade leveraged positions, place conditional orders, and provide liquidity to earn fees. The plugin supports both Arbitrum and Avalanche networks and includes comprehensive safety features like dry-run previews and mandatory user confirmation for all write operations.

## Usage
Install the plugin via OKX plugin store, ensure your onchainos wallet is connected, then use commands like `gmx-v2 --chain arbitrum open-position` to trade. Always run commands with `--dry-run` first to preview transactions before execution.
Connect your wallet with `onchainos wallet login`, then use commands like `gmx-v2 --chain arbitrum open-position` to trade or `gmx-v2 list-markets` to view available markets. All write operations require user confirmation after showing a dry-run preview.

## Commands
- `list-markets` - View active perpetual markets with liquidity and rates
- `get-prices` - Get current oracle prices for tokens
- `get-positions` - Query open positions for a wallet
- `get-orders` - Query pending conditional orders
- `open-position` - Open leveraged long/short positions
- `close-position` - Close existing positions (full or partial)
- `place-order` - Place limit/stop-loss/take-profit orders
- `cancel-order` - Cancel pending conditional orders
- `deposit-liquidity` - Add liquidity to GM pools
- `withdraw-liquidity` - Remove liquidity from GM pools
- `claim-funding-fees` - Claim accrued funding fee rewards
| Command | Description |
|---------|-------------|
| `list-markets` | View active perpetual markets with liquidity and rates |
| `get-prices` | Get current oracle prices for tokens |
| `get-positions` | Query open positions for a wallet |
| `get-orders` | Query pending conditional orders |
| `open-position` | Open leveraged long/short positions |
| `close-position` | Close existing positions (full or partial) |
| `place-order` | Place limit/stop-loss/take-profit orders |
| `cancel-order` | Cancel pending conditional orders |
| `deposit-liquidity` | Add tokens to GM pools for LP rewards |
| `withdraw-liquidity` | Remove liquidity from GM pools |
| `claim-funding-fees` | Claim accrued funding fee income |

## Triggers
Activate when users want to trade leveraged perpetuals, manage GMX positions, or interact with GM liquidity pools on Arbitrum/Avalanche. Common trigger phrases include "open position GMX", "GMX trade", "GMX leverage", "GMX liquidity", or "GMX stop loss".
Activate when users mention GMX trading, opening/closing leveraged positions, setting stop losses or take profits, providing liquidity to GM pools, or querying perpetuals market data on Arbitrum or Avalanche.
16 changes: 8 additions & 8 deletions skills/gmx-v2/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# gmx-v2
Trade perpetuals and spot on GMX V2 with leverage across Arbitrum and Avalanche networks.
A comprehensive plugin for trading perpetuals and managing liquidity on GMX V2 protocol across Arbitrum and Avalanche chains.

## Highlights
- Open and close leveraged perpetual positions (long/short) on GMX V2
- Place conditional orders (limit, stop-loss, take-profit) with keeper execution
- Add and remove liquidity to GM pools for yield generation
- Support for both Arbitrum and Avalanche chains
- Query real-time market data, prices, positions, and pending orders
- Open/close leveraged long and short positions with customizable slippage
- Place conditional orders (limit, stop-loss, take-profit) with trigger prices
- Add/remove liquidity from GM pools to earn fees
- Query real-time market data, positions, and pending orders
- Support for both Arbitrum (lower fees) and Avalanche chains
- Built-in safety with dry-run previews and user confirmation flows
- Automatic token approvals and execution fee handling
- Built-in dry-run mode for safe transaction previewing
- Integration with onchainos wallet for secure transaction signing
- Claim accumulated funding fees from trading positions

211 changes: 98 additions & 113 deletions skills/gmx-v2/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,138 +237,123 @@ pub fn encode_create_order(
}

/// Encode `createDeposit(CreateDepositParams)` calldata
/// Selector: 0xadc567e6 (createDeposit((address,address,address,address,address,address[],address[],uint256,uint256,uint256,uint256,uint256)))
/// We use manual ABI encoding for the struct.
///
/// Selector: 0xc82aa41b
/// keccak256("createDeposit(((address,address,address,address,address,address,address[],address[]),uint256,bool,uint256,uint256,bytes32[]))")
/// Verified from deployed ExchangeRouter bytecode (PUSH4 scan on Arbitrum mainnet).
///
/// Flat struct layout (T = outer tuple):
/// T HEAD (6 words = 192 bytes):
/// W0: offset_to_addresses = 192
/// W1: minMarketTokens
/// W2: shouldUnwrapNativeToken = false
/// W3: executionFee
/// W4: callbackGasLimit = 0
/// W5: offset_to_dataList = 192 + 320 = 512
/// addresses tuple (10 words = 320 bytes):
/// receiver, callbackContract=0, uiFeeReceiver=0, market,
/// initialLongToken, initialShortToken,
/// offset_longSwapPath=256, offset_shortSwapPath=288,
/// longSwapPath length=0, shortSwapPath length=0
/// dataList (1 word): length = 0
#[allow(clippy::too_many_arguments)]
pub fn encode_create_deposit(
receiver: &str,
callback_contract: &str,
ui_fee_receiver: &str,
_callback_contract: &str,
_ui_fee_receiver: &str,
market: &str,
initial_long_token: &str,
initial_short_token: &str,
min_market_tokens: u128,
execution_fee: u128,
src_chain_id: u64,
_src_chain_id: u64,
) -> String {
// createDeposit((Addresses, Numbers, Flags))
// Addresses: (receiver, callbackContract, uiFeeReceiver, market, initialLongToken, initialShortToken, longTokenSwapPath[], shortTokenSwapPath[])
// Numbers: (minMarketTokens, executionFee, callbackGasLimit, srcChainId)
// Flags: (shouldUnwrapNativeToken)
//
// Selector: let's use the verified one from design
// The function signature is complex, so we'll build it piece by piece.

// Addresses tuple (static head + 2 dynamic arrays):
// 6 static address slots + offset to longSwapPath + offset to shortSwapPath + 2 empty arrays
let addr_head_slots = 8usize; // 6 addresses + 2 offsets
let long_swap_offset = addr_head_slots * 32; // offset to longSwapPath within addr tuple
let short_swap_offset = long_swap_offset + 32; // 32 bytes for length=0 array

let mut addr_encoded = String::new();
addr_encoded.push_str(&encode_address(receiver));
addr_encoded.push_str(&encode_address(callback_contract));
addr_encoded.push_str(&encode_address(ui_fee_receiver));
addr_encoded.push_str(&encode_address(market));
addr_encoded.push_str(&encode_address(initial_long_token));
addr_encoded.push_str(&encode_address(initial_short_token));
addr_encoded.push_str(&encode_u256(long_swap_offset as u128));
addr_encoded.push_str(&encode_u256(short_swap_offset as u128));
addr_encoded.push_str(&encode_u256(0)); // longSwapPath length=0
addr_encoded.push_str(&encode_u256(0)); // shortSwapPath length=0

// Numbers tuple (4 static slots):
let mut num_encoded = String::new();
num_encoded.push_str(&encode_u256(min_market_tokens));
num_encoded.push_str(&encode_u256(execution_fee));
num_encoded.push_str(&encode_u256(0)); // callbackGasLimit
num_encoded.push_str(&encode_u256(src_chain_id as u128));

// Flags tuple (1 bool):
let mut flags_encoded = String::new();
flags_encoded.push_str(&encode_bool(false)); // shouldUnwrapNativeToken

// Build struct encoding
let addr_bytes = addr_encoded.len() / 2;
let num_bytes = num_encoded.len() / 2;
let offset_addr = 3 * 32usize;
let offset_num = offset_addr + addr_bytes;
let offset_flags = offset_num + num_bytes;

let mut struct_encoding = String::new();
struct_encoding.push_str(&encode_u256(offset_addr as u128));
struct_encoding.push_str(&encode_u256(offset_num as u128));
struct_encoding.push_str(&encode_u256(offset_flags as u128));
struct_encoding.push_str(&addr_encoded);
struct_encoding.push_str(&num_encoded);
struct_encoding.push_str(&flags_encoded);

// Selector for createDeposit
// createDeposit((address,address,address,address,address,address,address[],address[],uint256,uint256,uint256,uint256,bool))
// We use: 0xadc567e6
format!("adc567e6{}{}", encode_u256(0x20), struct_encoding)
// --- addresses tuple (10 words = 320 bytes) ---
let mut addresses = String::new();
addresses.push_str(&encode_address(receiver)); // receiver
addresses.push_str(&zero_address()); // callbackContract = 0
addresses.push_str(&zero_address()); // uiFeeReceiver = 0
addresses.push_str(&encode_address(market)); // market
addresses.push_str(&encode_address(initial_long_token)); // initialLongToken
addresses.push_str(&encode_address(initial_short_token)); // initialShortToken
addresses.push_str(&encode_u256(256)); // offset to longSwapPath = A_HEAD_SIZE
addresses.push_str(&encode_u256(288)); // offset to shortSwapPath = 256 + 32
addresses.push_str(&encode_u256(0)); // longSwapPath length = 0
addresses.push_str(&encode_u256(0)); // shortSwapPath length = 0

// --- T HEAD (6 words = 192 bytes) ---
const T_HEAD_SIZE: usize = 192;
const A_SIZE: usize = 320;
const DATALIST_OFFSET: usize = T_HEAD_SIZE + A_SIZE; // = 512

let mut t = String::new();
t.push_str(&encode_u256(T_HEAD_SIZE as u128)); // W0: offset to addresses
t.push_str(&encode_u256(min_market_tokens)); // W1: minMarketTokens
t.push_str(&encode_bool(false)); // W2: shouldUnwrapNativeToken
t.push_str(&encode_u256(execution_fee)); // W3: executionFee
t.push_str(&encode_u256(0)); // W4: callbackGasLimit = 0
t.push_str(&encode_u256(DATALIST_OFFSET as u128)); // W5: offset to dataList
t.push_str(&addresses); // addresses (320 bytes)
t.push_str(&encode_u256(0)); // dataList length = 0

format!("c82aa41b{}{}", encode_u256(0x20), t)
}

/// Encode `createWithdrawal(CreateWithdrawalParams)` calldata
/// Selector: 0x9b8eb9e7
#[allow(clippy::too_many_arguments)]
///
/// Selector: 0xe78dc235
/// keccak256("createWithdrawal(((address,address,address,address,address[],address[]),uint256,uint256,bool,uint256,uint256,bytes32[]))")
/// Verified from deployed ExchangeRouter bytecode (PUSH4 scan on Arbitrum mainnet).
///
/// Flat struct layout (T = outer tuple):
/// T HEAD (7 words = 224 bytes):
/// W0: offset_to_addresses = 224
/// W1: minLongTokenAmount
/// W2: minShortTokenAmount
/// W3: shouldUnwrapNativeToken = false
/// W4: executionFee
/// W5: callbackGasLimit = 0
/// W6: offset_to_dataList = 224 + 256 = 480
/// addresses tuple (8 words = 256 bytes):
/// receiver, callbackContract=0, uiFeeReceiver=0, market,
/// offset_longSwapPath=192, offset_shortSwapPath=224,
/// longSwapPath length=0, shortSwapPath length=0
/// dataList (1 word): length = 0
pub fn encode_create_withdrawal(
receiver: &str,
callback_contract: &str,
ui_fee_receiver: &str,
market: &str,
min_long_token_amount: u128,
min_short_token_amount: u128,
execution_fee: u128,
src_chain_id: u64,
) -> String {
// CreateWithdrawalParams: (receiver, callbackContract, uiFeeReceiver, market, longTokenSwapPath[], shortTokenSwapPath[])
// Numbers: (minLongTokenAmount, minShortTokenAmount, executionFee, callbackGasLimit, srcChainId)
// Flags: (shouldUnwrapNativeToken)

// Addresses tuple
let addr_head_slots = 6usize; // 4 addresses + 2 offsets for swap paths
let long_swap_offset = addr_head_slots * 32;
let short_swap_offset = long_swap_offset + 32;

let mut addr_encoded = String::new();
addr_encoded.push_str(&encode_address(receiver));
addr_encoded.push_str(&encode_address(callback_contract));
addr_encoded.push_str(&encode_address(ui_fee_receiver));
addr_encoded.push_str(&encode_address(market));
addr_encoded.push_str(&encode_u256(long_swap_offset as u128));
addr_encoded.push_str(&encode_u256(short_swap_offset as u128));
addr_encoded.push_str(&encode_u256(0)); // longSwapPath length=0
addr_encoded.push_str(&encode_u256(0)); // shortSwapPath length=0

// Numbers tuple
let mut num_encoded = String::new();
num_encoded.push_str(&encode_u256(min_long_token_amount));
num_encoded.push_str(&encode_u256(min_short_token_amount));
num_encoded.push_str(&encode_u256(execution_fee));
num_encoded.push_str(&encode_u256(0)); // callbackGasLimit
num_encoded.push_str(&encode_u256(src_chain_id as u128));

// Flags
let mut flags_encoded = String::new();
flags_encoded.push_str(&encode_bool(false)); // shouldUnwrapNativeToken

let addr_bytes = addr_encoded.len() / 2;
let num_bytes = num_encoded.len() / 2;
let offset_addr = 3 * 32usize;
let offset_num = offset_addr + addr_bytes;
let offset_flags = offset_num + num_bytes;

let mut struct_encoding = String::new();
struct_encoding.push_str(&encode_u256(offset_addr as u128));
struct_encoding.push_str(&encode_u256(offset_num as u128));
struct_encoding.push_str(&encode_u256(offset_flags as u128));
struct_encoding.push_str(&addr_encoded);
struct_encoding.push_str(&num_encoded);
struct_encoding.push_str(&flags_encoded);

// Selector for createWithdrawal: 0x9b8eb9e7
format!("9b8eb9e7{}{}", encode_u256(0x20), struct_encoding)
// --- addresses tuple (8 words = 256 bytes) ---
let mut addresses = String::new();
addresses.push_str(&encode_address(receiver)); // receiver
addresses.push_str(&zero_address()); // callbackContract = 0
addresses.push_str(&zero_address()); // uiFeeReceiver = 0
addresses.push_str(&encode_address(market)); // market
addresses.push_str(&encode_u256(192)); // offset to longSwapPath = A_HEAD_SIZE
addresses.push_str(&encode_u256(224)); // offset to shortSwapPath = 192 + 32
addresses.push_str(&encode_u256(0)); // longSwapPath length = 0
addresses.push_str(&encode_u256(0)); // shortSwapPath length = 0

// --- T HEAD (7 words = 224 bytes) ---
const T_HEAD_SIZE: usize = 224;
const A_SIZE: usize = 256;
const DATALIST_OFFSET: usize = T_HEAD_SIZE + A_SIZE; // = 480

let mut t = String::new();
t.push_str(&encode_u256(T_HEAD_SIZE as u128)); // W0: offset to addresses
t.push_str(&encode_u256(min_long_token_amount)); // W1
t.push_str(&encode_u256(min_short_token_amount)); // W2
t.push_str(&encode_bool(false)); // W3: shouldUnwrapNativeToken
t.push_str(&encode_u256(execution_fee)); // W4
t.push_str(&encode_u256(0)); // W5: callbackGasLimit = 0
t.push_str(&encode_u256(DATALIST_OFFSET as u128)); // W6: offset to dataList
t.push_str(&addresses); // addresses (256 bytes)
t.push_str(&encode_u256(0)); // dataList length = 0

format!("e78dc235{}{}", encode_u256(0x20), t)
}

/// Encode the outer `multicall(bytes[])` calldata
Expand Down
3 changes: 2 additions & 1 deletion skills/gmx-v2/src/commands/cancel_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ pub async fn run(chain: &str, dry_run: bool, confirm: bool, args: CancelOrderArg
eprintln!("Exchange router: {}", cfg.exchange_router);
eprintln!("Ask user to confirm before proceeding.");

let result = crate::onchainos::wallet_contract_call(
let result = crate::onchainos::wallet_contract_call_with_gas(
cfg.chain_id,
cfg.exchange_router,
&calldata,
Some(&wallet),
None,
dry_run,
confirm,
Some(300_000),
).await?;

let tx_hash = crate::onchainos::extract_tx_hash(&result);
Expand Down
3 changes: 2 additions & 1 deletion skills/gmx-v2/src/commands/claim_funding_fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ pub async fn run(chain: &str, dry_run: bool, confirm: bool, args: ClaimFundingFe
eprintln!("Note: No execution fee needed for claims.");
eprintln!("Ask user to confirm before proceeding.");

let result = crate::onchainos::wallet_contract_call(
let result = crate::onchainos::wallet_contract_call_with_gas(
cfg.chain_id,
cfg.exchange_router,
&calldata,
Some(&wallet),
None, // no ETH value needed for claim
dry_run,
confirm,
Some(300_000),
).await?;

let tx_hash = crate::onchainos::extract_tx_hash(&result);
Expand Down
3 changes: 2 additions & 1 deletion skills/gmx-v2/src/commands/deposit_liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,15 @@ pub async fn run(chain: &str, dry_run: bool, confirm: bool, args: DepositLiquidi
eprintln!("⚠ GMX V2 keeper model: GM tokens minted 1-30s after tx lands.");
eprintln!("Ask user to confirm before proceeding.");

let result = crate::onchainos::wallet_contract_call(
let result = crate::onchainos::wallet_contract_call_with_gas(
cfg.chain_id,
cfg.exchange_router,
&calldata,
Some(&wallet),
Some(execution_fee),
dry_run,
confirm,
Some(800_000),
).await?;

let tx_hash = crate::onchainos::extract_tx_hash(&result);
Expand Down
3 changes: 2 additions & 1 deletion skills/gmx-v2/src/commands/place_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,15 @@ pub async fn run(chain: &str, dry_run: bool, confirm: bool, args: PlaceOrderArgs
}
}

let result = crate::onchainos::wallet_contract_call(
let result = crate::onchainos::wallet_contract_call_with_gas(
cfg.chain_id,
cfg.exchange_router,
&calldata,
Some(&wallet),
Some(execution_fee),
dry_run,
confirm,
Some(500_000),
).await?;

let tx_hash = crate::onchainos::extract_tx_hash(&result);
Expand Down
Loading