Skip to content
Merged
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 trading perpetual contracts and managing liquidity on GMX V2, a decentralized derivatives platform. It provides comprehensive access to leveraged trading with up to high leverage ratios, conditional order placement, and GM token liquidity provisioning across Arbitrum and Avalanche networks. The plugin handles all technical complexities including token approvals, execution fees, and keeper-based order execution.

## 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.
Install the plugin via OKX plugin store, connect your wallet with `onchainos wallet login`, then use commands like `gmx-v2 open-position` or `gmx-v2 list-markets`. Always run with `--dry-run` first to preview transactions before execution.

## 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 leveraged positions |
| `get-orders` | Query pending conditional orders |
| `open-position` | Open long/short leveraged position |
| `close-position` | Close existing position (full/partial) |
| `place-order` | Place limit/stop-loss/take-profit orders |
| `cancel-order` | Cancel pending conditional order |
| `deposit-liquidity` | Add tokens to GM liquidity pools |
| `withdraw-liquidity` | Remove liquidity and burn GM tokens |
| `claim-funding-fees` | Claim accrued funding fee rewards |

## 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 want to trade leveraged perpetuals, manage derivatives positions, or provide liquidity on GMX V2. Common phrases include "open GMX position", "GMX leverage trade", "deposit GM pool", "set stop loss", or "claim funding fees".
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.
Trade perpetuals and spot on GMX V2 with leveraged positions, limit/stop orders, and GM pool liquidity management on Arbitrum and Avalanche.

## Highlights
- Open and close leveraged perpetual positions (long/short) on GMX V2
- Open/close leveraged perpetual positions (long/short) with market orders
- 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
- Automatic token approvals and execution fee handling
- Built-in dry-run mode for safe transaction previewing
- Integration with onchainos wallet for secure transaction signing
- Add/remove liquidity to GM pools and earn trading fees
- Support for Arbitrum (42161) and Avalanche (43114) networks
- Real-time oracle prices and market data from GMX infrastructure
- Query open positions, pending orders, and market statistics
- Auto-approve token allowances and handle execution fees
- Claim accrued funding fees from leveraged 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