Skip to content

Commit 51444fb

Browse files
Merge pull request #26 from neutron-org/dasset-synchronous-updater
dasset cron updator
2 parents 3a2537c + c478f12 commit 51444fb

File tree

8 files changed

+1806
-0
lines changed

8 files changed

+1806
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "dasset-updator"
3+
version = "0.1.0"
4+
description = "updates config params of dasset vault"
5+
edition = { workspace = true }
6+
7+
[lib]
8+
crate-type = ["cdylib", "rlib"]
9+
10+
[features]
11+
library = []
12+
13+
[dependencies]
14+
serde_json = "1.0"
15+
schemars = { workspace = true }
16+
serde = { version = "1.0.180", default-features = false, features = ["derive"] }
17+
cosmwasm-std = { workspace = true }
18+
cosmwasm-schema = { workspace = true }
19+
cw2 = { workspace = true }
20+
cw-storage-plus = { workspace = true }
21+
thiserror = { workspace = true }
22+
test-case = { workspace = true }
23+
neutron-std = { workspace = true }
24+
prost = { workspace = true }
25+
prost-types = { workspace = true }
26+
27+
28+
[dev-dependencies]
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
2+
3+
use crate::error::{ContractError, ContractResult};
4+
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, UpdateConfig};
5+
use crate::state::{Config, CONFIG};
6+
use crate::utils::{*, validate_instantiate_msg, validate_update_config};
7+
use crate::external_types::{AllApyResponse, CalculatedFeeTiers};
8+
use cosmwasm_std::{attr, entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, Addr, Decimal};
9+
use cw2::set_contract_version;
10+
11+
use serde_json::to_vec;
12+
13+
const CONTRACT_NAME: &str = concat!("crates.io:neutron-contracts__", env!("CARGO_PKG_NAME"));
14+
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
15+
16+
///////////////////
17+
/// INSTANTIATE ///
18+
///////////////////
19+
20+
21+
#[cfg_attr(not(feature = "library"), entry_point)]
22+
pub fn instantiate(
23+
deps: DepsMut,
24+
_env: Env,
25+
_info: MessageInfo,
26+
msg: InstantiateMsg,
27+
) -> ContractResult<Response> {
28+
validate_instantiate_msg(&msg)?;
29+
30+
// Set contract version for migration info
31+
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
32+
33+
let whitelist = msg
34+
.whitelist
35+
.iter()
36+
.map(|addr| deps.api.addr_validate(addr).map_err(ContractError::Std))
37+
.collect::<Result<Vec<Addr>, ContractError>>()?;
38+
39+
let core_contracts = msg.assets.iter()
40+
.map(|c| deps.api.addr_validate(&c.core_contract).map_err(ContractError::from))
41+
.collect::<ContractResult<Vec<Addr>>>()?;
42+
let vault_addresses = msg.assets.iter()
43+
.map(|c| deps.api.addr_validate(&c.vault_address).map_err(ContractError::from))
44+
.collect::<ContractResult<Vec<Addr>>>()?;
45+
46+
47+
let apy_contract = deps.api.addr_validate(&msg.apy_contract)?;
48+
49+
// Create and save config
50+
let config = Config {
51+
assets: msg.assets,
52+
apy_contract,
53+
whitelist,
54+
};
55+
56+
CONFIG.save(deps.storage, &config)?;
57+
58+
Ok(Response::new()
59+
.add_attribute("action", "instantiate")
60+
.add_attribute("asset_count", config.assets.len().to_string())
61+
.add_attributes([
62+
attr("core_contracts", format!("{:?}", core_contracts)),
63+
attr("vault_addresses", format!("{:?}", vault_addresses)),
64+
attr("apy_contract", config.apy_contract.to_string()),
65+
]))
66+
}
67+
68+
///////////////
69+
/// EXECUTE ///
70+
///////////////
71+
72+
#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)]
73+
pub fn execute(
74+
deps: DepsMut,
75+
env: Env,
76+
info: MessageInfo,
77+
msg: ExecuteMsg,
78+
) -> Result<Response, ContractError> {
79+
match msg {
80+
ExecuteMsg::UpdateConfig { new_config } => execute_update_config(deps, info, new_config),
81+
ExecuteMsg::RunVaultUpdate {} => execute_run_vault_update(deps, env, info)
82+
}
83+
}
84+
/////////////
85+
/// QUERY ///
86+
/////////////
87+
88+
#[entry_point]
89+
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult<Binary> {
90+
match msg {
91+
QueryMsg::GetConfig {} => {
92+
let config = CONFIG.load(deps.storage)?;
93+
let serialized_config = to_vec(&config).map_err(|_| ContractError::SerializationError)?;
94+
Ok(Binary::from(serialized_config))
95+
}
96+
QueryMsg::GetAllApy {} => {
97+
let config = CONFIG.load(deps.storage)?;
98+
let mut apys = Vec::<Decimal>::new();
99+
100+
for asset in config.assets {
101+
match query_apy_contract(&deps, &config.apy_contract, &asset.core_contract, asset.query_period_hours) {
102+
Ok(apy) => apys.push(apy),
103+
Err(_e) => {
104+
apys.push(Decimal::zero());
105+
continue;
106+
}
107+
}
108+
}
109+
110+
let response = AllApyResponse { apys };
111+
let serialized_response = to_vec(&response).map_err(|_| ContractError::SerializationError)?;
112+
Ok(Binary::from(serialized_response))
113+
}
114+
QueryMsg::GetFeeTiers {} => {
115+
let config = CONFIG.load(deps.storage)?;
116+
let mut calculated_tiers = Vec::<CalculatedFeeTiers>::new();
117+
118+
for asset in config.assets {
119+
let apy = match query_apy_contract(&deps, &config.apy_contract, &asset.core_contract, asset.query_period_hours) {
120+
Ok(apy) => apy,
121+
Err(_) => Decimal::zero(),
122+
};
123+
124+
if apy.is_zero() {
125+
// For zero APY, add entry with zeros
126+
calculated_tiers.push(CalculatedFeeTiers {
127+
denom: asset.denom,
128+
apy,
129+
base_fee: 0,
130+
oracle_skew: 0,
131+
fee_tiers: vec![],
132+
});
133+
} else {
134+
let base_fee = calculate_fee_tier(apy, asset.unbonding_period, asset.fee_dempening_amount)?;
135+
let fee_tiers = create_fee_tiers(base_fee, &asset.fee_spacings, &asset.percentages)?;
136+
let oracle_skew = (base_fee + 1) as i32;
137+
138+
// Convert to simple (fee, percentage) pairs
139+
let fee_tier_pairs: Vec<(u64, u64)> = fee_tiers
140+
.iter()
141+
.map(|tier| (tier.fee, tier.percentage))
142+
.collect();
143+
144+
calculated_tiers.push(CalculatedFeeTiers {
145+
denom: asset.denom,
146+
apy,
147+
base_fee,
148+
oracle_skew,
149+
fee_tiers: fee_tier_pairs,
150+
});
151+
}
152+
}
153+
154+
let serialized_response = to_vec(&calculated_tiers).map_err(|_| ContractError::SerializationError)?;
155+
Ok(Binary::from(serialized_response))
156+
}
157+
}
158+
}
159+
160+
///////////////
161+
/// MIGRATE ///
162+
///////////////
163+
164+
#[cfg_attr(not(feature = "library"), entry_point)]
165+
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
166+
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
167+
168+
Ok(Response::new().add_attribute("action", "migrate"))
169+
}
170+
171+
fn execute_update_config(
172+
deps: DepsMut,
173+
info: MessageInfo,
174+
new_config: UpdateConfig,
175+
) -> Result<Response, ContractError> {
176+
validate_update_config(&new_config)?;
177+
178+
// Check if sender is in whitelist
179+
let mut config = CONFIG.load(deps.storage)?;
180+
if !config.whitelist.contains(&info.sender) {
181+
return Err(ContractError::Unauthorized);
182+
}
183+
184+
// Update APY contract if provided
185+
if let Some(new_apy_contract) = new_config.new_apy_contract {
186+
187+
let validated_apy_contract = deps.api.addr_validate(&new_apy_contract)?;
188+
config.apy_contract = validated_apy_contract;
189+
}
190+
191+
// Update whitelist if provided
192+
if let Some(new_whitelist) = new_config.new_whitelist {
193+
let whitelist = new_whitelist
194+
.iter()
195+
.map(|addr| deps.api.addr_validate(addr).map_err(ContractError::Std))
196+
.collect::<Result<Vec<Addr>, ContractError>>()?;
197+
config.whitelist = whitelist;
198+
}
199+
200+
// Update assets if provided
201+
if let Some(new_assets) = new_config.new_assets {
202+
203+
for asset in &new_assets {
204+
deps.api.addr_validate(&asset.vault_address)?;
205+
deps.api.addr_validate(&asset.core_contract)?;
206+
}
207+
config.assets = new_assets;
208+
}
209+
210+
CONFIG.save(deps.storage, &config)?;
211+
212+
Ok(Response::new()
213+
.add_attribute("action", "update_config")
214+
.add_attribute("asset_count", config.assets.len().to_string())
215+
.add_attributes([
216+
attr("assets", format!("{:?}", config.assets)),
217+
attr("apy_contract", config.apy_contract.to_string()),
218+
]))
219+
}
220+
221+
fn execute_run_vault_update(
222+
deps: DepsMut,
223+
_env: Env,
224+
info: MessageInfo,
225+
) -> Result<Response, ContractError> {
226+
let config = CONFIG.load(deps.storage)?;
227+
228+
// Check if sender is in whitelist
229+
if !config.whitelist.contains(&info.sender) {
230+
return Err(ContractError::Unauthorized);
231+
}
232+
let mut messages = Vec::new();
233+
let mut attributes = Vec::new();
234+
235+
attributes.push(attr("action", "run_vault_update"));
236+
attributes.push(attr("vault_count", config.assets.len().to_string()));
237+
238+
// Process each asset vault
239+
for asset in &config.assets {
240+
// Query APY for this asset
241+
let apy = query_apy_contract(
242+
&deps.as_ref(),
243+
&config.apy_contract,
244+
&asset.core_contract,
245+
asset.query_period_hours,
246+
)?;
247+
248+
// Check if APY is zero
249+
let is_apy_zero: bool = apy.is_zero();
250+
251+
if is_apy_zero {
252+
// If APY is zero, only perform withdrawal (no update or deposit)
253+
let withdrawal_msg = create_dex_withdrawal_message(&asset.vault_address)?;
254+
messages.push(withdrawal_msg);
255+
256+
// zero APY case attrs
257+
attributes.push(attr(format!("vault_{}_apy", asset.denom), "0"));
258+
attributes.push(attr(format!("vault_{}_action", asset.denom), "withdrawal_only"));
259+
attributes.push(attr(format!("vault_{}_reason", asset.denom), "zero_apy"));
260+
} else {
261+
// if not zero apy, calculate base fee tier, create fee tiers, and update vault.
262+
// Calculate base fee tier using the APY and unbonding period
263+
let base_fee = calculate_fee_tier(apy, asset.unbonding_period, asset.fee_dempening_amount)?;
264+
265+
// Create fee tiers by adding configured values to the calculated base fee
266+
let fee_tiers = create_fee_tiers(base_fee, &asset.fee_spacings, &asset.percentages)?;
267+
268+
// Oracle skew is base fee + 1. can be counteracted with fee_spacing of 1 on the first tick index.
269+
let oracle_skew = (base_fee + 1) as i32;
270+
271+
// Full sequence for all vaults: dex_withdrawal, update_config, dex_deposit
272+
let withdrawal_msg = create_dex_withdrawal_message(&asset.vault_address)?;
273+
let update_msg = create_vault_update_message(
274+
&asset.vault_address,
275+
&fee_tiers,
276+
oracle_skew,
277+
&info.sender.to_string(),
278+
)?;
279+
let deposit_msg = create_dex_deposit_message(&asset.vault_address)?;
280+
281+
messages.push(withdrawal_msg);
282+
messages.push(update_msg);
283+
messages.push(deposit_msg);
284+
285+
// Add attributes for this vault update
286+
attributes.push(attr(format!("vault_{}_apy", asset.denom), apy.to_string()));
287+
attributes.push(attr(format!("vault_{}_base_fee", asset.denom), base_fee.to_string()));
288+
attributes.push(attr(format!("vault_{}_oracle_skew", asset.denom), oracle_skew.to_string()));
289+
attributes.push(attr(format!("vault_{}_action", asset.denom), "full_update"));
290+
}
291+
}
292+
293+
Ok(Response::new()
294+
.add_messages(messages)
295+
.add_attributes(attributes))
296+
}

0 commit comments

Comments
 (0)