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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ deploy_mainnet.sh

.env

scripts/
scripts/

.claude/
112 changes: 112 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# CLAUDE.md

## Project Overview

CosmWasm DEX aggregator smart contract for the Injective blockchain. Routes swaps through multiple AMM pools, orderbook contracts, and CLMM (Concentrated Liquidity) pools in parallel, multi-hop paths with automatic CW20/native token conversion. Cargo workspace with two members: `dex_aggregator` (main contract) and `mock_swap` (test helper). Deployed on Injective mainnet (Code ID 1892, address `inj1a4qvqym6ajewepa7v8y2rtxuz9f92kyq2zsg26`).

## Build, Test, and Deploy Commands

```bash
# Development build
cargo build

# Production WASM build (uses cosmwasm/workspace-optimizer:0.17.0 Docker image)
# Outputs to ./artifacts/dex_aggregator.wasm and ./artifacts/mock_swap.wasm
./build_release.sh

# Run tests (MUST run ./build_release.sh first — see note below)
cargo test

# Run a single test
cargo test <test_name> -- --nocapture

# Lint
cargo clippy --all-targets

# Generate JSON schemas to contracts/dex_aggregator/schema/
cd contracts/dex_aggregator && cargo run --example schema
```

**CRITICAL: Run `./build_release.sh` before `cargo test`.** Integration tests use `include_bytes!` to embed WASM artifacts at compile time. Tests will fail to compile or test stale code if artifacts aren't rebuilt after source changes.

### Deployment (uses `injectived` CLI)

```bash
./scripts/upload_code_mainnet.sh # Upload new code to mainnet
./scripts/deploy_mainnet.sh # Instantiate on mainnet (edit CODE_ID first)
./scripts/deploy_testnet.sh # Deploy on testnet (chain ID: injective-888)
```

## Architecture

### Key Files (contracts/dex_aggregator/src/)

| File | Purpose |
|------|---------|
| `contract.rs` | Entry points: `instantiate`, `execute`, `query`, `reply`. Routes `ExecuteMsg` variants to handlers. |
| `msg.rs` | All message types. Submodules: `amm`, `orderbook`, `clmm`, `cw20_adapter`, `reflection`. Defines `Stage > Split > Operation` route structure. |
| `execute.rs` | Core swap logic (`execute_aggregate_swaps_internal`, `create_swap_cosmos_msg`). Admin functions: `set_fee`, `remove_fee`, `update_fee_collector`, `update_admin`, `emergency_withdraw`, `register_tax_token`, `deregister_tax_token`. |
| `reply.rs` | Submessage reply state machine. Manages `Awaiting` states. Core function `proceed_to_next_step` drives stage-by-stage execution. Fee deduction via `apply_fee` at path completion. |
| `state.rs` | Storage: `CONFIG`, `FEE_MAP`, `ACTIVE_ROUTES`, `SUBMSG_REPLY_STATES`, `REPLY_ID_COUNTER`, `TAX_TOKEN_REGISTRY`. Defines `ExecutionState`, `SubmsgReplyState`, `Awaiting` enum. |
| `query.rs` | `simulate_route`, `query_config`, `query_fee_for_pool`, `query_all_fees`. Contains unit tests. |
| `error.rs` | `ContractError` enum with `thiserror`. |

### Execution Flow

1. User calls `ExecuteRoute` (native funds) or sends CW20 via `Receive` hook
2. `execute_aggregate_swaps_internal` validates input, creates `ExecutionState`, calls `proceed_to_next_step`
3. Each stage: calculates per-split amounts, dispatches CW20/native conversions if needed (`Awaiting::Conversions`)
4. Executes parallel swap submessages, each tracked by unique reply IDs in `SUBMSG_REPLY_STATES`
5. `handle_swap_reply` processes each reply; for multi-hop paths, chains to next operation
6. Mid-path conversions handled via `Awaiting::PathConversion`
7. After final stage: normalizes output assets (`Awaiting::FinalConversions`), checks `minimum_receive`, sends to user

### Supporting Contracts

- `mock_swap` (`contracts/mock_swap/src/lib.rs`) — Mock DEX with configurable rates, supports AMM/Orderbook/CLMM protocol types, used in integration tests
- `cw20_adapter` and `cw20_base` — Pre-compiled WASMs in project root, not built from this workspace

## Code Conventions

### Naming
- `snake_case` for functions, variables, module names
- `PascalCase` for types, enums, structs, enum variants
- `UPPER_SNAKE_CASE` for constants

### Patterns
- Entry points use Injective custom types: `DepsMut<InjectiveQueryWrapper>`, `Response<InjectiveMsgWrapper>`
- Messages use `#[cw_serde]` macro; query enum uses `#[derive(QueryResponses)]` with `#[returns(...)]`
- Error handling: `ContractError` enum via `thiserror`, propagated with `?`
- State: `cw-storage-plus` types — `Item<T>` for singletons, `Map<K, V>` for key-value stores
- Execute handlers return `Result<Response<InjectiveMsgWrapper>, ContractError>`
- Query handlers return `StdResult<Binary>`
- Admin checks: `info.sender != config.admin` → `ContractError::Unauthorized {}`
- Response attributes for tracking: `.add_attribute("action", "...")`

### Asset Handling
- `amm::AssetInfo` enum: `Token { contract_addr }` (CW20) or `NativeToken { denom }` (bank)
- Tax tokens in `TAX_TOKEN_REGISTRY` use `reflection::ExecuteMsg::TaxExemptTransfer` / `TaxExemptSend`
- CW20 tokens sent to pools via `Cw20ExecuteMsg::Send`; native tokens as `funds` in `WasmMsg::Execute`

### Submessage Reply Pattern
- Each swap gets a unique `submsg_id` from `REPLY_ID_COUNTER` (monotonically incrementing)
- `SubmsgReplyState` maps `submsg_id` → `master_reply_id`, `split_index`, `op_index`
- `ExecutionState` stored in `ACTIVE_ROUTES` keyed by `master_reply_id`
- All submessages use `SubMsg::reply_on_success`
- Reply amounts parsed from wasm event attributes: `return_amount` (AMM), `swap_final_amount` (orderbook), `amount_out` (CLMM), `post_tax_amount` (tax tokens)

## Testing

- **Integration tests** (`tests/integration.rs`): Uses `injective-test-tube` for local chain simulation. `setup()` deploys all contracts, returns `TestEnv` with admin/user accounts and contract addresses.
- **Unit tests** (`contracts/dex_aggregator/src/query.rs`): Simulation and fee query tests using `mock_dependencies()`.
- Mock swap contracts configured with `SwapConfig { rate, protocol_type, input_decimals, output_decimals, ... }`.
- WASM artifacts loaded via `include_bytes!` — stale artifacts mean stale tests.

## Important Notes

- `reply.rs` is the most complex module — state machine changes require careful review of all `Awaiting` state transitions
- Orderbook swaps only support native token inputs/outputs; amounts rounded to `min_quantity_tick_size`
- CLMM swaps support both native and CW20 tokens; no rounding needed. Pre-execution `Quote` query computes `minimum_amount_out` with 0.5% slippage
- `FPDecimal` (from `injective-math`) for orderbook quantities; `Uint128`/`Decimal` (from `cosmwasm-std`) for everything else (including CLMM)
- Fees deducted at path completion (end of a split's operation chain), not per-operation
- CI (`.github/workflows/test.yml`) runs `cargo build --verbose && cargo test --verbose` on push/PR to main
4 changes: 2 additions & 2 deletions artifacts/checksums.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
a0881d16ef7b87479688715c7e9796ee67aa117c3692090813ede98bfe8109c7 dex_aggregator.wasm
fca25ee84ed0903921574c9efef0144880a81f7533e952f6ce8cb3ea4f8e57b4 mock_swap.wasm
f012a5cc59e924f89a4716ed44eb8d94b57b914c045e0d8be0607844f4a61703 dex_aggregator.wasm
a3cb85b097d4cb7640cb441a5dcb8721167961aa5f5eee94eeebfb2818b5e2df mock_swap.wasm
Binary file modified artifacts/dex_aggregator.wasm
Binary file not shown.
Binary file modified artifacts/mock_swap.wasm
Binary file not shown.
32 changes: 32 additions & 0 deletions contracts/dex_aggregator/schema/execute_msg.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,26 @@
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"ClmmSwapOp": {
"type": "object",
"required": [
"ask_asset_info",
"offer_asset_info",
"pool_address"
],
"properties": {
"ask_asset_info": {
"$ref": "#/definitions/AssetInfo"
},
"offer_asset_info": {
"$ref": "#/definitions/AssetInfo"
},
"pool_address": {
"type": "string"
}
},
"additionalProperties": false
},
"Cw20ReceiveMsg": {
"description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg",
"type": "object",
Expand Down Expand Up @@ -323,6 +343,18 @@
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"clmm_swap"
],
"properties": {
"clmm_swap": {
"$ref": "#/definitions/ClmmSwapOp"
}
},
"additionalProperties": false
}
]
},
Expand Down
32 changes: 32 additions & 0 deletions contracts/dex_aggregator/schema/query_msg.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@
}
]
},
"ClmmSwapOp": {
"type": "object",
"required": [
"ask_asset_info",
"offer_asset_info",
"pool_address"
],
"properties": {
"ask_asset_info": {
"$ref": "#/definitions/AssetInfo"
},
"offer_asset_info": {
"$ref": "#/definitions/AssetInfo"
},
"pool_address": {
"type": "string"
}
},
"additionalProperties": false
},
"Coin": {
"type": "object",
"required": [
Expand Down Expand Up @@ -202,6 +222,18 @@
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"clmm_swap"
],
"properties": {
"clmm_swap": {
"$ref": "#/definitions/ClmmSwapOp"
}
},
"additionalProperties": false
}
]
},
Expand Down
72 changes: 71 additions & 1 deletion contracts/dex_aggregator/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use injective_math::FPDecimal;
use std::str::FromStr;

use crate::error::ContractError;
use crate::msg::{self, amm, orderbook, Operation, Stage};
use crate::msg::{self, amm, clmm, orderbook, Operation, Stage};
use crate::reply::proceed_to_next_step;
use crate::state::{
Awaiting, ExecutionState, RoutePlan, CONFIG, FEE_MAP, REPLY_ID_COUNTER, TAX_TOKEN_REGISTRY,
Expand Down Expand Up @@ -213,6 +213,76 @@ pub fn create_swap_cosmos_msg(
funds,
})
}
Operation::ClmmSwap(clmm_op) => {
// Query the pool for expected output
let quote_query = clmm::ClmmPoolQueryMsg::Quote {
token_in: offer_asset_info.clone(),
amount_in: amount,
};
let quote_response: clmm::QuoteResponse = deps
.querier
.query_wasm_smart(&clmm_op.pool_address, &quote_query)?;

if quote_response.amount_out.is_zero() {
return Ok(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: env.contract.address.to_string(),
msg: to_json_binary(&{})?,
funds: vec![],
}));
}

// Apply 0.5% slippage
let minimum_amount_out = quote_response.amount_out.multiply_ratio(995u128, 1000u128);

let clmm_swap_msg = clmm::ClmmPoolExecuteMsg::SwapExactInput {
minimum_amount_out,
recipient: Some(recipient),
deadline: None,
};

match offer_asset_info {
amm::AssetInfo::NativeToken { denom } => CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: clmm_op.pool_address.clone(),
msg: to_json_binary(&clmm_swap_msg)?,
funds: vec![Coin {
denom: denom.clone(),
amount,
}],
}),
amm::AssetInfo::Token { contract_addr } => {
let token_addr = deps.api.addr_validate(contract_addr)?;
let hook_msg = clmm::Cw20HookMsg::SwapExactInput {
minimum_amount_out,
recipient: Some(env.contract.address.to_string()),
deadline: None,
};
if TAX_TOKEN_REGISTRY.has(deps.storage, &token_addr) {
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract_addr.clone(),
msg: to_json_binary(
&crate::msg::reflection::ExecuteMsg::TaxExemptSend {
contract: clmm_op.pool_address.clone(),
amount,
msg: to_json_binary(&hook_msg)?,
},
)?,
funds: vec![],
})
} else {
let cw20_send_msg = Cw20ExecuteMsg::Send {
contract: clmm_op.pool_address.clone(),
amount,
msg: to_json_binary(&hook_msg)?,
};
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract_addr.clone(),
msg: to_json_binary(&cw20_send_msg)?,
funds: vec![],
})
}
}
}
}
};

Ok(cosmos_msg)
Expand Down
45 changes: 45 additions & 0 deletions contracts/dex_aggregator/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,43 @@ pub mod reflection {
}
}

pub mod clmm {
use super::*;

#[cw_serde]
pub enum ClmmPoolExecuteMsg {
SwapExactInput {
minimum_amount_out: Uint128,
recipient: Option<String>,
deadline: Option<u64>,
},
}

#[cw_serde]
pub enum Cw20HookMsg {
SwapExactInput {
minimum_amount_out: Uint128,
recipient: Option<String>,
deadline: Option<u64>,
},
}

#[cw_serde]
pub enum ClmmPoolQueryMsg {
Quote {
token_in: amm::AssetInfo,
amount_in: Uint128,
},
}

#[cw_serde]
pub struct QuoteResponse {
pub amount_out: Uint128,
pub amount_in_consumed: Uint128,
pub fee_amount: Uint128,
}
}

#[cw_serde]
pub struct AmmSwapOp {
pub pool_address: String,
Expand All @@ -151,10 +188,18 @@ pub struct OrderbookSwapOp {
pub min_quantity_tick_size: Uint128,
}

#[cw_serde]
pub struct ClmmSwapOp {
pub pool_address: String,
pub offer_asset_info: amm::AssetInfo,
pub ask_asset_info: amm::AssetInfo,
}

#[cw_serde]
pub enum Operation {
AmmSwap(AmmSwapOp),
OrderbookSwap(OrderbookSwapOp),
ClmmSwap(ClmmSwapOp),
}

#[cw_serde]
Expand Down
Loading