1
use crate::{
2
    msg::{ExecuteMsg, FeeRecipient, InstantiateMsg},
3
    testing::test_utils::{set_route_and_assert_success, INJ},
4
};
5

            
6
use cosmwasm_std::{Addr, Coin};
7
use injective_cosmwasm::get_default_subaccount_id_for_checked_address;
8
use injective_math::FPDecimal;
9
use injective_std::types::{
10
    cosmos::{bank::v1beta1::QueryBalanceRequest, base::v1beta1::Coin as ProtoCoin},
11
    cosmwasm::wasm::v1::MsgExecuteContract,
12
    injective::exchange::v1beta1::{
13
        MsgCreateSpotLimitOrder, MsgCreateSpotLimitOrderResponse, MsgInstantSpotMarketLaunch, OrderInfo, OrderType, SpotOrder,
14
    },
15
};
16
use injective_test_tube::{cosmrs::Any, Account, Bank, Exchange, InjectiveTestApp, Module, Runner, SigningAccount, Wasm};
17
use injective_testing::{
18
    test_tube::{
19
        bank::send,
20
        exchange::{add_denom_notional_and_decimal, get_spot_market_id},
21
    },
22
    utils::{dec_to_proto, scale_price_quantity_spot_market},
23
};
24
use prost::Message;
25

            
26
const GF: &str = "ugf";
27
const USDT: &str = "usdt";
28
const BASE_DECIMALS: i32 = 18;
29
const QUOTE_DECIMALS: i32 = 6;
30
const CALLER_INPUT_USDT_ATOMS: u128 = 10_035_014;
31
const CONTRACT_SUPPORT_USDT_ATOMS: u128 = 4_000_000_000;
32
const ATTACKER_GF_ATOMS: u128 = 10_009_989_000_000_000_000_000;
33
const LOW_QUANTITY_ATOMS: u128 = 10_000_000_000_000_000_000_000;
34
const SELECTED_HIGH_QUANTITY_ATOMS: u128 = 30_000_000_000_000_000;
35
const PRICE_TICK_RAW: u128 = 1_000;
36
const LOW_PRICE_RAW: u128 = 1_000;
37
const HIGH_PRICE_RAW: u128 = 333_000;
38

            
39
#[test]
40
2
fn same_transaction_crafted_asks_cannot_spend_contract_support() {
41
2
    assert_crafted_book_crosses_the_next_exact_price_tick();
42
2

            
43
2
    let app = InjectiveTestApp::new();
44
2
    let wasm = Wasm::new(&app);
45
2
    let bank = Bank::new(&app);
46
2
    let owner = app
47
2
        .init_account_decimals(
48
2
            &[
49
2
                Coin::new(1u128, GF),
50
2
                Coin::new(10_000_000_000_000_000_000_000u128, INJ),
51
2
                Coin::new(5_000_000_000u128, USDT),
52
2
            ],
53
2
            &[18, 18, 6],
54
2
        )
55
2
        .unwrap();
56
2
    let attacker = app
57
2
        .init_account(&[
58
2
            Coin::new(ATTACKER_GF_ATOMS, GF),
59
2
            Coin::new(1_000_000_000_000_000_000_000u128, INJ),
60
2
            Coin::new(CALLER_INPUT_USDT_ATOMS, USDT),
61
2
        ])
62
2
        .unwrap();
63
2
    let market_id = setup_market(&app, &owner);
64
2
    let release_wasm_path = format!("{}/../../artifacts/swap_contract.wasm", env!("CARGO_MANIFEST_DIR"));
65
2
    let release_wasm = std::fs::read(release_wasm_path).expect("build the optimized release artifact before running TestTube tests");
66
2
    let code_id = wasm.store_code(&release_wasm, None, &owner).unwrap().data.code_id;
67
2
    let contract = wasm
68
2
        .instantiate(
69
2
            code_id,
70
2
            &InstantiateMsg {
71
2
                fee_recipient: FeeRecipient::Address(Addr::unchecked(owner.address())),
72
2
                admin: Addr::unchecked(owner.address()),
73
2
            },
74
2
            Some(&owner.address()),
75
2
            Some("BB-311 regression"),
76
2
            &[Coin::new(CONTRACT_SUPPORT_USDT_ATOMS, USDT)],
77
2
            &owner,
78
2
        )
79
2
        .unwrap()
80
2
        .data
81
2
        .address;
82
2

            
83
2
    set_route_and_assert_success(&wasm, &owner, &contract, USDT, GF, vec![market_id.as_str().into()]);
84
2

            
85
2
    let contract_usdt_before = bank_balance(&bank, &contract, USDT);
86
2
    let attacker_usdt_before = bank_balance(&bank, &attacker.address(), USDT);
87
2
    let low_ask = sell_post_only_order(&attacker, &market_id, "0.001", "10000.000");
88
2
    let high_ask = sell_post_only_order(&attacker, &market_id, "0.333", "9.989");
89
2
    let swap = MsgExecuteContract {
90
2
        sender: attacker.address(),
91
2
        contract: contract.clone(),
92
2
        msg: serde_json_wasm::to_vec(&ExecuteMsg::SwapMinOutput {
93
2
            target_denom: GF.to_string(),
94
2
            min_output_quantity: FPDecimal::ONE,
95
2
        })
96
2
        .unwrap(),
97
2
        funds: vec![ProtoCoin {
98
2
            denom: USDT.to_string(),
99
2
            amount: CALLER_INPUT_USDT_ATOMS.to_string(),
100
2
        }],
101
2
    };
102
2

            
103
2
    let response = app
104
2
        .execute_multiple_raw::<MsgCreateSpotLimitOrderResponse>(
105
2
            vec![
106
2
                proto_any(&low_ask, "/injective.exchange.v1beta1.MsgCreateSpotLimitOrder"),
107
2
                proto_any(&high_ask, "/injective.exchange.v1beta1.MsgCreateSpotLimitOrder"),
108
2
                proto_any(&swap, "/cosmwasm.wasm.v1.MsgExecuteContract"),
109
2
            ],
110
2
            &attacker,
111
2
        )
112
2
        .unwrap();
113
2

            
114
2
    let contract_usdt_after = bank_balance(&bank, &contract, USDT);
115
2
    let attacker_usdt_after = bank_balance(&bank, &attacker.address(), USDT);
116
2
    let delivered_gf: u128 = response
117
2
        .events
118
2
        .iter()
119
95
        .find(|event| event.ty.contains("atomic_swap_execution"))
120
13
        .and_then(|event| event.attributes.iter().find(|attribute| attribute.key == "swap_final_amount"))
121
2
        .unwrap()
122
2
        .value
123
2
        .parse()
124
2
        .unwrap();
125
2

            
126
2
    assert!(contract_usdt_after >= contract_usdt_before, "pre-existing USDT support must not decrease");
127
2
    assert!(
128
2
        attacker_usdt_after <= attacker_usdt_before,
129
        "the maker/caller must not profit in quote funds"
130
    );
131
2
    assert!(
132
2
        delivered_gf < LOW_QUANTITY_ATOMS,
133
        "the rounded average must not size a buy for the complete low ask"
134
    );
135
2
}
136

            
137
2
fn assert_crafted_book_crosses_the_next_exact_price_tick() {
138
2
    let exact_notional = LOW_PRICE_RAW * LOW_QUANTITY_ATOMS + HIGH_PRICE_RAW * SELECTED_HIGH_QUANTITY_ATOMS;
139
2
    let exact_quantity = LOW_QUANTITY_ATOMS + SELECTED_HIGH_QUANTITY_ATOMS;
140
2
    let truncated_average = exact_notional / exact_quantity;
141
2
    let exact_remainder = exact_notional % exact_quantity;
142
2
    let tick_denominator = exact_quantity * PRICE_TICK_RAW;
143
2
    let exact_tick_ceiling = exact_notional.div_ceil(tick_denominator) * PRICE_TICK_RAW;
144
2

            
145
2
    assert_eq!(truncated_average, PRICE_TICK_RAW);
146
2
    assert_ne!(exact_remainder, 0);
147
2
    assert_eq!(exact_tick_ceiling, PRICE_TICK_RAW * 2);
148
2
}
149

            
150
2
fn setup_market(app: &InjectiveTestApp, owner: &SigningAccount) -> String {
151
2
    let validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2).unwrap();
152
2
    send(&Bank::new(app), "1000000000000000000000", INJ, owner, &validator);
153
2
    add_denom_notional_and_decimal(app, &validator, GF.to_string(), "1".to_string(), BASE_DECIMALS as u64);
154
2
    add_denom_notional_and_decimal(app, &validator, USDT.to_string(), "1".to_string(), QUOTE_DECIMALS as u64);
155
2

            
156
2
    let exchange = Exchange::new(app);
157
2
    let ticker = "GF/USDT-BB311".to_string();
158
2
    exchange
159
2
        .instant_spot_market_launch(
160
2
            MsgInstantSpotMarketLaunch {
161
2
                sender: owner.address(),
162
2
                ticker: ticker.clone(),
163
2
                base_denom: GF.to_string(),
164
2
                quote_denom: USDT.to_string(),
165
2
                min_price_tick_size: dec_to_proto(FPDecimal::must_from_str("0.000000000000001")),
166
2
                min_quantity_tick_size: dec_to_proto(FPDecimal::must_from_str("1000000000000000")),
167
2
                min_notional: dec_to_proto(FPDecimal::ONE),
168
2
                base_decimals: BASE_DECIMALS as u32,
169
2
                quote_decimals: QUOTE_DECIMALS as u32,
170
2
            },
171
2
            owner,
172
2
        )
173
2
        .unwrap();
174
2

            
175
2
    get_spot_market_id(&exchange, ticker)
176
2
}
177

            
178
4
fn sell_post_only_order(trader: &SigningAccount, market_id: &str, price: &str, quantity: &str) -> MsgCreateSpotLimitOrder {
179
4
    let (price, quantity) = scale_price_quantity_spot_market(price, quantity, &BASE_DECIMALS, &QUOTE_DECIMALS);
180
4

            
181
4
    MsgCreateSpotLimitOrder {
182
4
        sender: trader.address(),
183
4
        order: Some(SpotOrder {
184
4
            market_id: market_id.to_string(),
185
4
            order_info: Some(OrderInfo {
186
4
                subaccount_id: get_default_subaccount_id_for_checked_address(&Addr::unchecked(trader.address())).to_string(),
187
4
                fee_recipient: trader.address(),
188
4
                price,
189
4
                quantity,
190
4
                cid: String::new(),
191
4
            }),
192
4
            order_type: OrderType::SellPo.into(),
193
4
            trigger_price: String::new(),
194
4
        }),
195
4
    }
196
4
}
197

            
198
6
fn proto_any<M: Message>(message: &M, type_url: &str) -> Any {
199
6
    let mut value = Vec::new();
200
6
    message.encode(&mut value).unwrap();
201
6
    Any {
202
6
        type_url: type_url.to_string(),
203
6
        value,
204
6
    }
205
6
}
206

            
207
8
fn bank_balance(bank: &Bank<InjectiveTestApp>, address: &str, denom: &str) -> u128 {
208
8
    bank.query_balance(&QueryBalanceRequest {
209
8
        address: address.to_string(),
210
8
        denom: denom.to_string(),
211
8
    })
212
8
    .unwrap()
213
8
    .balance
214
12
    .map(|balance| balance.amount.parse().unwrap())
215
8
    .unwrap_or_default()
216
8
}