Skip to content

Commit 55ab730

Browse files
authored
Add dry run deposit query to inflow vault (#408)
* Add dry run deposit query to inflow vault contract Add a DryRunDeposit query that calculates how many vault shares would be minted for a given deposit amount without executing the deposit. This allows users and frontends to preview share amounts before committing. Closes #407 * Update changelog entry to reference PR #408 * Document that DryRunDeposit does not enforce the deposit cap
1 parent f6e098f commit 55ab730

File tree

9 files changed

+218
-0
lines changed

9 files changed

+218
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add "dry run deposit" query to the inflow vault contract that calculates how many shares would be minted for a given deposit amount without executing the deposit. ([\#408](https://github.com/informalsystems/hydro/pull/408))

contracts/inflow/vault/schema/raw/query.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,28 @@
269269
}
270270
},
271271
"additionalProperties": false
272+
},
273+
{
274+
"description": "Simulates a deposit and returns the number of vault shares that would be minted for the given amount of the deposit token, without executing it.",
275+
"type": "object",
276+
"required": [
277+
"dry_run_deposit"
278+
],
279+
"properties": {
280+
"dry_run_deposit": {
281+
"type": "object",
282+
"required": [
283+
"amount"
284+
],
285+
"properties": {
286+
"amount": {
287+
"$ref": "#/definitions/Uint128"
288+
}
289+
},
290+
"additionalProperties": false
291+
}
292+
},
293+
"additionalProperties": false
272294
}
273295
],
274296
"definitions": {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Uint128",
4+
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
5+
"type": "string"
6+
}

contracts/inflow/vault/schema/vault.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,28 @@
927927
}
928928
},
929929
"additionalProperties": false
930+
},
931+
{
932+
"description": "Simulates a deposit and returns the number of vault shares that would be minted for the given amount of the deposit token, without executing it.",
933+
"type": "object",
934+
"required": [
935+
"dry_run_deposit"
936+
],
937+
"properties": {
938+
"dry_run_deposit": {
939+
"type": "object",
940+
"required": [
941+
"amount"
942+
],
943+
"properties": {
944+
"amount": {
945+
"$ref": "#/definitions/Uint128"
946+
}
947+
},
948+
"additionalProperties": false
949+
}
950+
},
951+
"additionalProperties": false
930952
}
931953
],
932954
"definitions": {
@@ -1153,6 +1175,12 @@
11531175
}
11541176
}
11551177
},
1178+
"dry_run_deposit": {
1179+
"$schema": "http://json-schema.org/draft-07/schema#",
1180+
"title": "Uint128",
1181+
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
1182+
"type": "string"
1183+
},
11561184
"funded_withdrawal_requests": {
11571185
"$schema": "http://json-schema.org/draft-07/schema#",
11581186
"title": "FundedWithdrawalRequestsResponse",

contracts/inflow/vault/src/contract.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,6 +1770,9 @@ pub fn query(deps: Deps<NeutronQuery>, env: Env, msg: QueryMsg) -> StdResult<Bin
17701770
}
17711771
QueryMsg::ListAdapters {} => to_json_binary(&query_list_adapters(&deps)?),
17721772
QueryMsg::AdapterInfo { name } => to_json_binary(&query_adapter_info(&deps, name)?),
1773+
QueryMsg::DryRunDeposit { amount } => {
1774+
to_json_binary(&query_dry_run_deposit(&deps, amount)?)
1775+
}
17731776
}
17741777
}
17751778

@@ -1817,6 +1820,32 @@ fn query_user_shares_equivalent_value(
18171820
query_shares_equivalent_value(deps, &config, shares_balance)
18181821
}
18191822

1823+
/// Simulates a deposit and returns the number of vault shares that would be
1824+
/// minted for the given amount of the deposit token, without executing it.
1825+
/// Note: this does not enforce the deposit cap. It will return a share amount
1826+
/// even if the deposit would be rejected due to the cap being reached.
1827+
fn query_dry_run_deposit(deps: &Deps<NeutronQuery>, amount: Uint128) -> StdResult<Uint128> {
1828+
let config = load_config(deps.storage)?;
1829+
let pool_info = get_control_center_pool_info(deps, &config.control_center_contract)?;
1830+
1831+
let deposit_amount_base_tokens = convert_deposit_token_into_base_token(deps, &config, amount)?;
1832+
1833+
// In a real deposit, total_pool_value already includes the deposited tokens (they are sent
1834+
// before execute runs). Here no tokens have been sent, so we add the deposit amount to
1835+
// total_pool_value to match what calculate_number_of_shares_to_mint expects.
1836+
let total_pool_value_with_deposit = pool_info
1837+
.total_pool_value
1838+
.checked_add(deposit_amount_base_tokens)
1839+
.map_err(|e| StdError::generic_err(format!("overflow error: {e}")))?;
1840+
1841+
calculate_number_of_shares_to_mint(
1842+
deposit_amount_base_tokens,
1843+
total_pool_value_with_deposit,
1844+
pool_info.total_shares_issued,
1845+
)
1846+
.map_err(|e| StdError::generic_err(e.to_string()))
1847+
}
1848+
18201849
pub fn query_available_for_deployment(deps: &Deps<NeutronQuery>, env: &Env) -> StdResult<Uint128> {
18211850
let config = load_config(deps.storage)?;
18221851
let withdrawal_queue_info = load_withdrawal_queue_info(deps.storage)?;

contracts/inflow/vault/src/testing.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,110 @@ fn reporting_balance_queries_test() {
21252125
assert_eq!(eq_value, Uint128::new(501_000));
21262126
}
21272127

2128+
#[test]
2129+
fn dry_run_deposit_query_test() {
2130+
let (mut deps, mut env) = (mock_dependencies(), mock_env());
2131+
2132+
let vault_contract_addr = deps.api.addr_make(INFLOW);
2133+
let control_center_contract_addr = deps.api.addr_make(CONTROL_CENTER);
2134+
let token_info_provider_contract_addr = deps.api.addr_make(TOKEN_INFO_PROVIDER);
2135+
let whitelist_addr = deps.api.addr_make(WHITELIST_ADDR);
2136+
2137+
env.contract.address = vault_contract_addr.clone();
2138+
2139+
let instantiate_msg = get_default_instantiate_msg(
2140+
DEPOSIT_DENOM,
2141+
whitelist_addr.clone(),
2142+
control_center_contract_addr.clone(),
2143+
token_info_provider_contract_addr.clone(),
2144+
);
2145+
2146+
let info = get_message_info(&deps.api, "creator", &[]);
2147+
instantiate(deps.as_mut(), env.clone(), info, instantiate_msg).unwrap();
2148+
2149+
let vault_shares_denom_str: String =
2150+
format!("factory/{vault_contract_addr}/hydro_inflow_udatom");
2151+
2152+
set_vault_shares_denom(&mut deps, vault_shares_denom_str.clone());
2153+
2154+
// Case 1: Empty pool (no existing shares) → 1:1 ratio in base tokens
2155+
// With dATOM ratio of 1.2, depositing 500_000 deposit tokens = 600_000 base tokens = 600_000 shares
2156+
let wasm_querier = MockWasmQuerier::new(HashMap::from_iter([
2157+
setup_control_center_mock(
2158+
control_center_contract_addr.clone(),
2159+
DEFAULT_DEPOSIT_CAP,
2160+
Uint128::zero(),
2161+
Uint128::zero(),
2162+
),
2163+
setup_token_info_provider_mock(
2164+
token_info_provider_contract_addr.clone(),
2165+
DEPOSIT_DENOM.to_string(),
2166+
DATOM_DEFAULT_RATIO,
2167+
),
2168+
]));
2169+
2170+
let querier_for_deps = wasm_querier.clone();
2171+
deps.querier
2172+
.update_wasm(move |q| querier_for_deps.handler(q));
2173+
2174+
let deposit_amount = Uint128::new(500_000);
2175+
let query_msg = QueryMsg::DryRunDeposit {
2176+
amount: deposit_amount,
2177+
};
2178+
let query_res = query(deps.as_ref(), env.clone(), query_msg);
2179+
assert!(query_res.is_ok());
2180+
2181+
let shares: Uint128 = from_json(query_res.unwrap()).unwrap();
2182+
// 500_000 * 1.2 ratio = 600_000 base tokens = 600_000 shares (1:1 on empty pool)
2183+
assert_eq!(shares, Uint128::new(600_000));
2184+
2185+
// Case 2: Pool already has 600_000 base token value and 600_000 shares.
2186+
// Depositing another 500_000 deposit tokens (= 600_000 base) should mint 600_000 shares
2187+
// since the pool ratio is 1:1.
2188+
update_contract_mock(
2189+
&mut deps,
2190+
&wasm_querier,
2191+
setup_default_control_center_mock(Uint128::new(600_000), Uint128::new(600_000)),
2192+
);
2193+
2194+
let query_msg = QueryMsg::DryRunDeposit {
2195+
amount: deposit_amount,
2196+
};
2197+
let query_res = query(deps.as_ref(), env.clone(), query_msg);
2198+
assert!(query_res.is_ok());
2199+
2200+
let shares: Uint128 = from_json(query_res.unwrap()).unwrap();
2201+
assert_eq!(shares, Uint128::new(600_000));
2202+
2203+
// Case 3: Pool value has grown (e.g. yield). Pool has 1_200_000 base value but still 600_000 shares.
2204+
// Depositing 500_000 deposit tokens (= 600_000 base) should mint 300_000 shares
2205+
// (600_000 * 600_000 / 1_200_000 = 300_000)
2206+
update_contract_mock(
2207+
&mut deps,
2208+
&wasm_querier,
2209+
setup_default_control_center_mock(Uint128::new(1_200_000), Uint128::new(600_000)),
2210+
);
2211+
2212+
let query_msg = QueryMsg::DryRunDeposit {
2213+
amount: deposit_amount,
2214+
};
2215+
let query_res = query(deps.as_ref(), env.clone(), query_msg);
2216+
assert!(query_res.is_ok());
2217+
2218+
let shares: Uint128 = from_json(query_res.unwrap()).unwrap();
2219+
assert_eq!(shares, Uint128::new(300_000));
2220+
2221+
// Case 4: Zero amount deposit should return zero shares
2222+
let query_msg = QueryMsg::DryRunDeposit {
2223+
amount: Uint128::zero(),
2224+
};
2225+
let query_res = query(deps.as_ref(), env, query_msg);
2226+
assert!(query_res.is_ok());
2227+
2228+
let shares: Uint128 = from_json(query_res.unwrap()).unwrap();
2229+
assert_eq!(shares, Uint128::zero());
2230+
}
2231+
21282232
#[test]
21292233
fn withdrawal_with_config_update_test() {
21302234
let (mut deps, mut env) = (mock_dependencies(), mock_env());

packages/interface/src/inflow_vault.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,13 @@ pub enum QueryMsg {
258258
/// Returns information about a specific adapter
259259
#[returns(AdapterInfoResponse)]
260260
AdapterInfo { name: String },
261+
262+
/// Simulates a deposit and returns the number of vault shares that would be
263+
/// minted for the given amount of the deposit token, without executing it.
264+
/// Note: this query does not enforce the deposit cap. It will return a share
265+
/// amount even if the deposit would be rejected due to the cap being reached.
266+
#[returns(Uint128)]
267+
DryRunDeposit { amount: Uint128 },
261268
}
262269

263270
#[cw_serde]

ts_types/InflowVaultBase.client.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ export interface InflowVaultBaseReadOnlyInterface {
5757
}: {
5858
name: string;
5959
}) => Promise<AdapterInfoResponse>;
60+
dryRunDeposit: ({
61+
amount
62+
}: {
63+
amount: Uint128;
64+
}) => Promise<Uint128>;
6065
}
6166
export class InflowVaultBaseQueryClient implements InflowVaultBaseReadOnlyInterface {
6267
client: CosmWasmClient;
@@ -78,6 +83,7 @@ export class InflowVaultBaseQueryClient implements InflowVaultBaseReadOnlyInterf
7883
this.controlCenterPoolInfo = this.controlCenterPoolInfo.bind(this);
7984
this.listAdapters = this.listAdapters.bind(this);
8085
this.adapterInfo = this.adapterInfo.bind(this);
86+
this.dryRunDeposit = this.dryRunDeposit.bind(this);
8187
}
8288
config = async (): Promise<ConfigResponse> => {
8389
return this.client.queryContractSmart(this.contractAddress, {
@@ -200,6 +206,17 @@ export class InflowVaultBaseQueryClient implements InflowVaultBaseReadOnlyInterf
200206
}
201207
});
202208
};
209+
dryRunDeposit = async ({
210+
amount
211+
}: {
212+
amount: Uint128;
213+
}): Promise<Uint128> => {
214+
return this.client.queryContractSmart(this.contractAddress, {
215+
dry_run_deposit: {
216+
amount
217+
}
218+
});
219+
};
203220
}
204221
export interface InflowVaultBaseInterface extends InflowVaultBaseReadOnlyInterface {
205222
contractAddress: string;

ts_types/InflowVaultBase.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ export type QueryMsg = {
163163
adapter_info: {
164164
name: string;
165165
};
166+
} | {
167+
dry_run_deposit: {
168+
amount: Uint128;
169+
};
166170
};
167171
export type Order = "ascending" | "descending";
168172
export type Addr = string;

0 commit comments

Comments
 (0)