astroport_emissions_controller/
ibc.rs

1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4    ensure, from_json, wasm_execute, Deps, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse,
5    IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcPacketAckMsg,
6    IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Never, StdError, StdResult,
7    Uint128,
8};
9
10use astroport_governance::assembly;
11use astroport_governance::emissions_controller::consts::{IBC_APP_VERSION, IBC_ORDERING};
12use astroport_governance::emissions_controller::hub::OutpostInfo;
13use astroport_governance::emissions_controller::msg::{
14    ack_fail, ack_ok, IbcAckResult, VxAstroIbcMsg,
15};
16
17use crate::error::ContractError;
18use crate::execute::{handle_update_user, handle_vote};
19use crate::state::{get_all_outposts, CONFIG};
20use crate::utils::jail_outpost;
21
22#[cfg_attr(not(feature = "library"), entry_point)]
23pub fn ibc_channel_open(
24    _deps: DepsMut,
25    _env: Env,
26    msg: IbcChannelOpenMsg,
27) -> StdResult<Option<Ibc3ChannelOpenResponse>> {
28    let channel = msg.channel();
29
30    ensure!(
31        channel.order == IBC_ORDERING,
32        StdError::generic_err("Ordering is invalid. The channel must be unordered",)
33    );
34    ensure!(
35        channel.version == IBC_APP_VERSION,
36        StdError::generic_err(format!("Must set version to `{IBC_APP_VERSION}`",))
37    );
38    if let Some(counter_version) = msg.counterparty_version() {
39        if counter_version != IBC_APP_VERSION {
40            return Err(StdError::generic_err(format!(
41                "Counterparty version must be `{IBC_APP_VERSION}`"
42            )));
43        }
44    }
45
46    Ok(Some(Ibc3ChannelOpenResponse {
47        version: IBC_APP_VERSION.to_string(),
48    }))
49}
50
51#[cfg_attr(not(feature = "library"), entry_point)]
52pub fn ibc_channel_connect(
53    _deps: DepsMut,
54    _env: Env,
55    msg: IbcChannelConnectMsg,
56) -> StdResult<IbcBasicResponse> {
57    if let Some(counter_version) = msg.counterparty_version() {
58        if counter_version != IBC_APP_VERSION {
59            return Err(StdError::generic_err(format!(
60                "Counterparty version must be `{IBC_APP_VERSION}`"
61            )));
62        }
63    }
64
65    let channel = msg.channel();
66
67    Ok(IbcBasicResponse::new()
68        .add_attribute("action", "ibc_connect")
69        .add_attribute("channel_id", &channel.endpoint.channel_id))
70}
71
72#[cfg_attr(not(feature = "library"), entry_point)]
73pub fn ibc_packet_receive(
74    deps: DepsMut,
75    env: Env,
76    msg: IbcPacketReceiveMsg,
77) -> Result<IbcReceiveResponse, Never> {
78    do_packet_receive(deps, env, msg).or_else(|err| {
79        Ok(IbcReceiveResponse::new()
80            .add_attribute("action", "ibc_packet_receive")
81            .set_ack(ack_fail(err)))
82    })
83}
84
85/// Confirm that total voting power reported from remote outpost doesn't exceed the total xASTRO
86/// bridged over (held in escrow).
87fn is_outpost_valid(
88    deps: Deps,
89    outpost: &OutpostInfo,
90    ibc_msg: &VxAstroIbcMsg,
91) -> Result<bool, ContractError> {
92    let escrow_address = outpost
93        .params
94        .as_ref()
95        .expect("Outpost params must be set") // It must be guaranteed that params are set
96        .escrow_address
97        .clone();
98
99    let xastro_denom = CONFIG.load(deps.storage)?.xastro_denom;
100
101    let escrow_balance = deps
102        .querier
103        .query_balance(escrow_address, xastro_denom)?
104        .amount;
105
106    match ibc_msg {
107        VxAstroIbcMsg::EmissionsVote {
108            total_voting_power, ..
109        }
110        | VxAstroIbcMsg::UpdateUserVotes {
111            total_voting_power, ..
112        }
113        | VxAstroIbcMsg::GovernanceVote {
114            total_voting_power, ..
115        } => Ok(*total_voting_power <= escrow_balance),
116        VxAstroIbcMsg::RegisterProposal { .. } => {
117            unreachable!("Hub can't receive RegisterProposal message")
118        }
119    }
120}
121
122pub fn do_packet_receive(
123    deps: DepsMut,
124    env: Env,
125    msg: IbcPacketReceiveMsg,
126) -> Result<IbcReceiveResponse, ContractError> {
127    // Ensure this outpost was ever registered
128    let (prefix, outpost) = get_all_outposts(deps.storage)?
129        .into_iter()
130        .find_map(|(prefix, outpost)| {
131            outpost.params.as_ref().and_then(|params| {
132                if msg.packet.dest.channel_id == params.voting_channel {
133                    Some((prefix.clone(), outpost.clone()))
134                } else {
135                    None
136                }
137            })
138        })
139        .ok_or_else(|| {
140            StdError::generic_err(format!(
141                "Unknown outpost with {} voting channel",
142                msg.packet.dest.channel_id
143            ))
144        })?;
145
146    let ibc_msg: VxAstroIbcMsg = from_json(&msg.packet.data)?;
147
148    if outpost.jailed {
149        match ibc_msg {
150            VxAstroIbcMsg::UpdateUserVotes {
151                voter,
152                is_unlock: true,
153                ..
154            } => handle_update_user(deps.storage, env, voter.as_str(), Uint128::zero()).map(
155                |orig_response| {
156                    IbcReceiveResponse::new()
157                        .add_attributes(orig_response.attributes)
158                        .set_ack(ack_ok())
159                },
160            ),
161            _ => Err(ContractError::JailedOutpost { prefix }),
162        }
163    } else {
164        // Check for possible malicious xASTRO minting behavior on the outpost.
165        // Jail this outpost in case of total vxASTRO exceeds the total xASTRO bridged over.
166        if !is_outpost_valid(deps.as_ref(), &outpost, &ibc_msg)? {
167            jail_outpost(deps.storage, &prefix, env)?;
168
169            return Ok(IbcReceiveResponse::default()
170                .set_ack(ack_ok())
171                .add_attributes([("action", "jail_outpost"), ("prefix", &prefix)]));
172        }
173
174        match ibc_msg {
175            VxAstroIbcMsg::EmissionsVote {
176                voter,
177                voting_power,
178                votes,
179                ..
180            } => handle_vote(deps, env, &voter, voting_power, votes).map(|orig_response| {
181                IbcReceiveResponse::new()
182                    .add_attributes(orig_response.attributes)
183                    .set_ack(ack_ok())
184            }),
185            VxAstroIbcMsg::UpdateUserVotes {
186                voter,
187                voting_power,
188                ..
189            } => handle_update_user(deps.storage, env, voter.as_str(), voting_power).map(
190                |orig_response| {
191                    IbcReceiveResponse::new()
192                        .add_attributes(orig_response.attributes)
193                        .set_ack(ack_ok())
194                },
195            ),
196            VxAstroIbcMsg::GovernanceVote {
197                voter,
198                voting_power,
199                proposal_id,
200                vote,
201                ..
202            } => {
203                let config = CONFIG.load(deps.storage)?;
204                let cast_vote_msg = wasm_execute(
205                    config.assembly,
206                    &assembly::ExecuteMsg::CastVoteOutpost {
207                        voter,
208                        voting_power,
209                        proposal_id,
210                        vote,
211                    },
212                    vec![],
213                )?;
214
215                Ok(IbcReceiveResponse::new()
216                    .add_message(cast_vote_msg)
217                    .set_ack(ack_ok()))
218            }
219            VxAstroIbcMsg::RegisterProposal { .. } => {
220                unreachable!("Hub can't receive RegisterProposal message")
221            }
222        }
223    }
224}
225
226#[cfg(not(tarpaulin_include))]
227#[cfg_attr(not(feature = "library"), entry_point)]
228pub fn ibc_packet_ack(
229    _deps: DepsMut,
230    _env: Env,
231    msg: IbcPacketAckMsg,
232) -> StdResult<IbcBasicResponse> {
233    match from_json(msg.acknowledgement.data)? {
234        IbcAckResult::Ok(_) => {
235            Ok(IbcBasicResponse::default().add_attribute("action", "ibc_packet_ack"))
236        }
237        IbcAckResult::Error(err) => Ok(IbcBasicResponse::default().add_attribute("error", err)),
238    }
239}
240
241#[cfg(not(tarpaulin_include))]
242#[cfg_attr(not(feature = "library"), entry_point)]
243pub fn ibc_packet_timeout(
244    _deps: DepsMut,
245    _env: Env,
246    _msg: IbcPacketTimeoutMsg,
247) -> StdResult<IbcBasicResponse> {
248    Ok(IbcBasicResponse::default().add_attribute("action", "ibc_packet_timeout"))
249}
250
251#[cfg(not(tarpaulin_include))]
252#[cfg_attr(not(feature = "library"), entry_point)]
253pub fn ibc_channel_close(
254    _deps: DepsMut,
255    _env: Env,
256    _channel: IbcChannelCloseMsg,
257) -> StdResult<IbcBasicResponse> {
258    unimplemented!()
259}
260
261#[cfg(test)]
262mod unit_tests {
263    use std::collections::HashMap;
264    use std::marker::PhantomData;
265
266    use cosmwasm_std::testing::{mock_dependencies, mock_env, MockQuerier, MockStorage};
267    use cosmwasm_std::{
268        attr, coins, to_json_binary, Addr, Decimal, IbcChannel, IbcEndpoint, IbcOrder, IbcPacket,
269        IbcTimeout, OwnedDeps, Timestamp,
270    };
271    use cw_multi_test::MockApiBech32;
272    use neutron_sdk::bindings::query::NeutronQuery;
273
274    use astroport_governance::assembly::ProposalVoteOption;
275    use astroport_governance::emissions_controller::hub::{
276        Config, OutpostInfo, OutpostParams, VotedPoolInfo,
277    };
278    use astroport_governance::emissions_controller::msg::IbcAckResult;
279    use astroport_governance::utils::determine_ics20_escrow_address;
280
281    use crate::state::{OUTPOSTS, POOLS_WHITELIST, VOTED_POOLS};
282
283    use super::*;
284
285    pub fn mock_custom_dependencies(
286    ) -> OwnedDeps<MockStorage, MockApiBech32, MockQuerier, NeutronQuery> {
287        OwnedDeps {
288            storage: MockStorage::default(),
289            api: MockApiBech32::new("neutron"),
290            querier: MockQuerier::default(),
291            custom_query_type: PhantomData,
292        }
293    }
294
295    #[test]
296    fn test_channel_open() {
297        let mut deps = mock_dependencies();
298
299        let mut ibc_channel = IbcChannel::new(
300            IbcEndpoint {
301                port_id: "doesnt matter".to_string(),
302                channel_id: "doesnt matter".to_string(),
303            },
304            IbcEndpoint {
305                port_id: "doesnt matter".to_string(),
306                channel_id: "doesnt matter".to_string(),
307            },
308            IbcOrder::Unordered,
309            IBC_APP_VERSION,
310            "doesnt matter",
311        );
312        let res = ibc_channel_open(
313            deps.as_mut(),
314            mock_env(),
315            IbcChannelOpenMsg::new_init(ibc_channel.clone()),
316        )
317        .unwrap()
318        .unwrap();
319
320        assert_eq!(res.version, IBC_APP_VERSION);
321
322        ibc_channel.order = IbcOrder::Ordered;
323
324        let res = ibc_channel_open(
325            deps.as_mut(),
326            mock_env(),
327            IbcChannelOpenMsg::new_init(ibc_channel.clone()),
328        )
329        .unwrap_err();
330        assert_eq!(
331            res,
332            StdError::generic_err("Ordering is invalid. The channel must be unordered")
333        );
334
335        ibc_channel.order = IbcOrder::Unordered;
336        ibc_channel.version = "wrong_version".to_string();
337
338        let res = ibc_channel_open(
339            deps.as_mut(),
340            mock_env(),
341            IbcChannelOpenMsg::new_init(ibc_channel.clone()),
342        )
343        .unwrap_err();
344        assert_eq!(
345            res,
346            StdError::generic_err(format!("Must set version to `{IBC_APP_VERSION}`"))
347        );
348
349        ibc_channel.version = IBC_APP_VERSION.to_string();
350
351        let res = ibc_channel_open(
352            deps.as_mut(),
353            mock_env(),
354            IbcChannelOpenMsg::new_try(ibc_channel.clone(), "wrong_version"),
355        )
356        .unwrap_err();
357        assert_eq!(
358            res,
359            StdError::generic_err(format!("Counterparty version must be `{IBC_APP_VERSION}`"))
360        );
361
362        ibc_channel_open(
363            deps.as_mut(),
364            mock_env(),
365            IbcChannelOpenMsg::new_try(ibc_channel.clone(), IBC_APP_VERSION),
366        )
367        .unwrap()
368        .unwrap();
369    }
370
371    #[test]
372    fn test_channel_connect() {
373        let mut deps = mock_dependencies();
374
375        let ibc_channel = IbcChannel::new(
376            IbcEndpoint {
377                port_id: "doesnt matter".to_string(),
378                channel_id: "doesnt matter".to_string(),
379            },
380            IbcEndpoint {
381                port_id: "doesnt matter".to_string(),
382                channel_id: "doesnt matter".to_string(),
383            },
384            IbcOrder::Unordered,
385            IBC_APP_VERSION,
386            "doesnt matter",
387        );
388
389        ibc_channel_connect(
390            deps.as_mut(),
391            mock_env(),
392            IbcChannelConnectMsg::new_ack(ibc_channel.clone(), IBC_APP_VERSION),
393        )
394        .unwrap();
395
396        let err = ibc_channel_connect(
397            deps.as_mut(),
398            mock_env(),
399            IbcChannelConnectMsg::new_ack(ibc_channel.clone(), "wrong version"),
400        )
401        .unwrap_err();
402        assert_eq!(
403            err,
404            StdError::generic_err(format!("Counterparty version must be `{IBC_APP_VERSION}`"))
405        );
406    }
407
408    #[test]
409    fn test_packet_receive() {
410        let mut deps = mock_custom_dependencies();
411
412        const XASTRO_DENOM: &str = "xastro";
413
414        CONFIG
415            .save(
416                deps.as_mut().storage,
417                &Config {
418                    owner: Addr::unchecked("".to_string()),
419                    assembly: Addr::unchecked("".to_string()),
420                    vxastro: Addr::unchecked("".to_string()),
421                    factory: Addr::unchecked("".to_string()),
422                    astro_denom: "".to_string(),
423                    xastro_denom: XASTRO_DENOM.to_string(),
424                    staking: Addr::unchecked("".to_string()),
425                    incentives_addr: Addr::unchecked("".to_string()),
426                    pools_per_outpost: 0,
427                    whitelisting_fee: Default::default(),
428                    fee_receiver: Addr::unchecked("".to_string()),
429                    whitelist_threshold: Default::default(),
430                    emissions_multiple: Default::default(),
431                    max_astro: Default::default(),
432                },
433            )
434            .unwrap();
435
436        let voting_msg = VxAstroIbcMsg::EmissionsVote {
437            voter: "osmo1voter".to_string(),
438            voting_power: 1000u128.into(),
439            total_voting_power: Default::default(),
440            votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]),
441        };
442        let packet = IbcPacket::new(
443            to_json_binary(&voting_msg).unwrap(),
444            IbcEndpoint {
445                port_id: "".to_string(),
446                channel_id: "".to_string(),
447            },
448            IbcEndpoint {
449                port_id: "".to_string(),
450                channel_id: "channel-2".to_string(),
451            },
452            1,
453            IbcTimeout::with_timestamp(Timestamp::from_seconds(100)),
454        );
455        let ibc_msg = IbcPacketReceiveMsg::new(packet, Addr::unchecked("doesnt matter"));
456
457        let resp =
458            ibc_packet_receive(deps.as_mut().into_empty(), mock_env(), ibc_msg.clone()).unwrap();
459        let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap();
460        assert_eq!(
461            ack_err,
462            IbcAckResult::Error(
463                "Generic error: Unknown outpost with channel-2 voting channel".to_string()
464            )
465        );
466
467        let escrow_address =
468            determine_ics20_escrow_address(deps.as_mut().api, "transfer", "channel-2").unwrap();
469
470        // Mock added outpost and whitelist
471        OUTPOSTS
472            .save(
473                deps.as_mut().storage,
474                "osmo",
475                &OutpostInfo {
476                    params: Some(OutpostParams {
477                        emissions_controller: "".to_string(),
478                        voting_channel: "channel-2".to_string(),
479                        ics20_channel: "".to_string(),
480                        escrow_address: escrow_address.clone(),
481                    }),
482                    astro_denom: "".to_string(),
483                    astro_pool_config: None,
484                    jailed: false,
485                },
486            )
487            .unwrap();
488        POOLS_WHITELIST
489            .save(deps.as_mut().storage, &vec!["osmo1pool1".to_string()])
490            .unwrap();
491
492        let mut env = mock_env();
493        env.block.time = Timestamp::from_seconds(1724922008);
494
495        VOTED_POOLS
496            .save(
497                deps.as_mut().storage,
498                "osmo1pool1",
499                &VotedPoolInfo {
500                    init_ts: env.block.time.seconds(),
501                    voting_power: 0u128.into(),
502                },
503                env.block.time.seconds(),
504            )
505            .unwrap();
506
507        let resp =
508            ibc_packet_receive(deps.as_mut().into_empty(), env.clone(), ibc_msg.clone()).unwrap();
509        let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap();
510        assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into()));
511
512        // The same user can only vote at the next epoch
513        let resp = ibc_packet_receive(deps.as_mut().into_empty(), env.clone(), ibc_msg).unwrap();
514        let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap();
515        assert_eq!(
516            ack_err,
517            IbcAckResult::Error("Next time you can change your vote is at 1725235200".to_string())
518        );
519
520        // Voting from random channel is not possible
521        let packet = IbcPacket::new(
522            to_json_binary(&voting_msg).unwrap(),
523            IbcEndpoint {
524                port_id: "".to_string(),
525                channel_id: "".to_string(),
526            },
527            IbcEndpoint {
528                port_id: "".to_string(),
529                channel_id: "channel-3".to_string(),
530            },
531            1,
532            IbcTimeout::with_timestamp(Timestamp::from_seconds(100)),
533        );
534        let ibc_msg = IbcPacketReceiveMsg::new(packet, Addr::unchecked("doesnt matter"));
535        let resp = ibc_packet_receive(deps.as_mut().into_empty(), env.clone(), ibc_msg).unwrap();
536        let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap();
537        assert_eq!(
538            ack_err,
539            IbcAckResult::Error(
540                "Generic error: Unknown outpost with channel-3 voting channel".to_string()
541            )
542        );
543
544        // However, his voting power can be updated any time
545        let update_msg = VxAstroIbcMsg::UpdateUserVotes {
546            voter: "osmo1voter".to_string(),
547            voting_power: 2000u128.into(),
548            total_voting_power: Default::default(),
549            is_unlock: false,
550        };
551        let packet = IbcPacket::new(
552            to_json_binary(&update_msg).unwrap(),
553            IbcEndpoint {
554                port_id: "".to_string(),
555                channel_id: "".to_string(),
556            },
557            IbcEndpoint {
558                port_id: "".to_string(),
559                channel_id: "channel-2".to_string(),
560            },
561            1,
562            IbcTimeout::with_timestamp(Timestamp::from_seconds(100)),
563        );
564        let ibc_msg = IbcPacketReceiveMsg::new(packet, Addr::unchecked("doesnt matter"));
565        let resp = ibc_packet_receive(deps.as_mut().into_empty(), env.clone(), ibc_msg).unwrap();
566        let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap();
567        assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into()));
568
569        // Test outpost voting power validation.
570
571        // Set escrow balance to 100_000 xASTRO
572        deps.querier
573            .update_balance(&escrow_address, coins(100_000, XASTRO_DENOM));
574
575        // Emulate outpost total voting power at 99_999 xASTRO
576        let voting_msg = VxAstroIbcMsg::EmissionsVote {
577            voter: "osmo1voter2".to_string(),
578            voting_power: 1000u128.into(),
579            total_voting_power: 99_999u128.into(),
580            votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]),
581        };
582        let packet = IbcPacket::new(
583            to_json_binary(&voting_msg).unwrap(),
584            IbcEndpoint {
585                port_id: "".to_string(),
586                channel_id: "".to_string(),
587            },
588            IbcEndpoint {
589                port_id: "".to_string(),
590                channel_id: "channel-2".to_string(),
591            },
592            1,
593            IbcTimeout::with_timestamp(Timestamp::from_seconds(100)),
594        );
595        let ibc_msg = IbcPacketReceiveMsg::new(packet, Addr::unchecked("doesnt matter"));
596        let resp = ibc_packet_receive(deps.as_mut().into_empty(), env.clone(), ibc_msg).unwrap();
597        let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap();
598        assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into()));
599
600        // Emulate outpost total voting power at 150_000 xASTRO
601        let voting_msg = VxAstroIbcMsg::EmissionsVote {
602            voter: "osmo1voter3".to_string(),
603            voting_power: 1000u128.into(),
604            total_voting_power: 150_000u128.into(),
605            votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]),
606        };
607        let packet = IbcPacket::new(
608            to_json_binary(&voting_msg).unwrap(),
609            IbcEndpoint {
610                port_id: "".to_string(),
611                channel_id: "".to_string(),
612            },
613            IbcEndpoint {
614                port_id: "".to_string(),
615                channel_id: "channel-2".to_string(),
616            },
617            1,
618            IbcTimeout::with_timestamp(Timestamp::from_seconds(100)),
619        );
620        let ibc_msg = IbcPacketReceiveMsg::new(packet, Addr::unchecked("doesnt matter"));
621        let resp = ibc_packet_receive(deps.as_mut().into_empty(), env.clone(), ibc_msg).unwrap();
622
623        assert!(resp.messages.is_empty());
624        assert!(resp.events.is_empty());
625        assert_eq!(
626            resp.acknowledgement,
627            to_json_binary(&IbcAckResult::Ok(b"ok".into())).unwrap()
628        );
629        assert_eq!(
630            resp.attributes,
631            vec![attr("action", "jail_outpost"), attr("prefix", "osmo"),]
632        );
633    }
634
635    #[test]
636    fn test_jailed_outpost() {
637        let mut deps = mock_custom_dependencies();
638
639        // Mock jailed outpost
640        OUTPOSTS
641            .save(
642                deps.as_mut().storage,
643                "osmo",
644                &OutpostInfo {
645                    params: Some(OutpostParams {
646                        emissions_controller: "".to_string(),
647                        voting_channel: "channel-2".to_string(),
648                        ics20_channel: "".to_string(),
649                        escrow_address: Addr::unchecked("".to_string()),
650                    }),
651                    astro_denom: "".to_string(),
652                    astro_pool_config: None,
653                    jailed: true,
654                },
655            )
656            .unwrap();
657
658        for (msg, is_error) in [
659            (
660                VxAstroIbcMsg::EmissionsVote {
661                    voter: "osmo1voter".to_string(),
662                    voting_power: 1000u128.into(),
663                    total_voting_power: Default::default(),
664                    votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]),
665                },
666                true,
667            ),
668            (
669                VxAstroIbcMsg::GovernanceVote {
670                    voter: "osmo1voter".to_string(),
671                    voting_power: 1000u128.into(),
672                    total_voting_power: Default::default(),
673                    proposal_id: 1,
674                    vote: ProposalVoteOption::For,
675                },
676                true,
677            ),
678            (
679                VxAstroIbcMsg::UpdateUserVotes {
680                    voter: "osmo1voter".to_string(),
681                    voting_power: 2000u128.into(),
682                    total_voting_power: Default::default(),
683                    is_unlock: false,
684                },
685                true,
686            ),
687            (
688                VxAstroIbcMsg::UpdateUserVotes {
689                    voter: "osmo1voter".to_string(),
690                    voting_power: 0u128.into(),
691                    total_voting_power: Default::default(),
692                    is_unlock: true,
693                },
694                false,
695            ),
696        ] {
697            let packet = IbcPacket::new(
698                to_json_binary(&msg).unwrap(),
699                IbcEndpoint {
700                    port_id: "".to_string(),
701                    channel_id: "".to_string(),
702                },
703                IbcEndpoint {
704                    port_id: "".to_string(),
705                    channel_id: "channel-2".to_string(),
706                },
707                1,
708                IbcTimeout::with_timestamp(Timestamp::from_seconds(100)),
709            );
710            let ibc_msg = IbcPacketReceiveMsg::new(packet, Addr::unchecked("doesnt matter"));
711
712            let resp = ibc_packet_receive(deps.as_mut().into_empty(), mock_env(), ibc_msg).unwrap();
713            let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap();
714
715            if is_error {
716                assert_eq!(
717                    ack_err,
718                    IbcAckResult::Error(
719                        ContractError::JailedOutpost {
720                            prefix: "osmo".to_string()
721                        }
722                        .to_string()
723                    )
724                );
725            } else {
726                assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into()));
727            }
728        }
729    }
730}