storage_outpost/
contract.rs

1//! This module handles the execution logic of the contract.
2
3use cosmos_sdk_proto::tendermint::p2p::packet;
4use cosmwasm_std::entry_point;
5use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Event, Empty, CosmosMsg, IbcQuery};
6use crate::ibc::types::stargate::channel::new_ica_channel_open_init_cosmos_msg;
7use crate::types::keys::{self, CONTRACT_NAME, CONTRACT_VERSION};
8use crate::types::msg::{OutpostFactoryExecuteMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
9use crate::types::state::{
10    self, CallbackCounter, ChannelState, ContractState, CALLBACK_COUNTER, CHANNEL_STATE, STATE, CHANNEL_OPEN_INIT_OPTIONS, ALLOW_CHANNEL_OPEN_INIT
11};
12use crate::types::ContractError;
13use crate::types::filetree::{MsgPostKey, MsgPostFile};
14use crate::helpers::filetree_helpers::{hash_and_hex, merkle_helper};
15
16
17/// Instantiates the contract.
18/// Linker confused when building outpost owner so we 
19/// enable this optional feature to disable these entry points during compilation
20#[cfg(not(feature = "no_exports"))] 
21#[entry_point]
22pub fn instantiate(
23    deps: DepsMut,
24    env: Env,
25    info: MessageInfo,
26    msg: InstantiateMsg, //call back object is nested here 
27) -> Result<Response, ContractError> {
28    use cosmwasm_std::WasmMsg;
29
30    cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
31
32    // SECURITY NOTE: If Alice instantiated an outpost that's owned by Bob, this really has no consequence
33    // Alice wouldn't be able to use that outpost and Bob could just instantiate a fresh outpost for himself
34
35    // NOTE: When the factory calls this function, its address is info.sender, so it will need to pass in the User's address 
36    // as owner and admin to 'msg: InstantiateMsg'. 
37    // If a user calls this function directly without using the factory, they can leave 'msg: InstantiateMsg' empty and 
38    // their address--automatically set in info.sender--will be used as owner and admin
39    let owner = msg.owner.unwrap_or_else(|| info.sender.to_string());
40    cw_ownable::initialize_owner(deps.storage, deps.api, Some(&owner))?;
41
42    // NOTE: This doesn't actually have much effect because admin is set when the factory makes a cross contract instantiate
43    // call, but we'll leave this anyway.
44    let admin = if let Some(admin) = msg.admin {
45        deps.api.addr_validate(&admin)?
46    } else {
47        info.sender.clone()
48    };
49
50    let mut event = Event::new("OUTPOST:instantiate");
51    event = event.add_attribute("info.sender", info.sender.clone());
52    event = event.add_attribute("outpost_address", env.contract.address.to_string());
53
54    // NOTE: saving admin to ContractState doesn't have much effect, but we'll leave it as is because the outpost
55    // factory lists itself as admin in storage_outpost.InstantiateMsg 
56
57    // The below is not the same thing as saving the admin properly to ContractInfo struct defined in wasmd types
58    // wasmd's instantiate msg has an admin field which serves as the absolute admin for migration purposes
59
60    // Save the admin. Ica address is determined during handshake.
61    STATE.save(deps.storage, &ContractState::new(admin))?;
62
63    // NOTE: The callback counter is used for troubleshooting the callback mechanism--i.e., did it fail?
64    // Not needed so far but leaving it in for future use
65    // Initialize the callback counter.
66    CALLBACK_COUNTER.save(deps.storage, &CallbackCounter::default())?;
67
68    if let Some(ref options) = msg.channel_open_init_options {
69        CHANNEL_OPEN_INIT_OPTIONS.save(deps.storage, options)?;
70    }
71
72    ALLOW_CHANNEL_OPEN_INIT.save(deps.storage, &true)?;
73
74    // If channel open init options are provided, open the channel.
75    if let Some(channel_open_init_options) = msg.channel_open_init_options {
76        let ica_channel_open_init_msg = new_ica_channel_open_init_cosmos_msg(
77            env.contract.address.to_string(),
78            channel_open_init_options.connection_id,
79            channel_open_init_options.counterparty_port_id,
80            channel_open_init_options.counterparty_connection_id,
81            channel_open_init_options.tx_encoding,
82            channel_open_init_options.channel_ordering,
83        );
84
85    // Only call the factory contract back and execute 'MapuserOutpost' if instructed to do so--i.e., callback object exists
86    let callback_factory_msg = if let Some(callback) = &msg.callback {
87
88        Some(CosmosMsg::Wasm(WasmMsg::Execute { 
89            contract_addr: callback.contract.clone(), 
90            msg: to_json_binary(&OutpostFactoryExecuteMsg::MapUserOutpost { 
91                outpost_owner: callback.outpost_owner.clone(), 
92            }).ok().expect("Failed to serialize callback_msg"), 
93            funds: vec![], 
94        }))
95    } else {
96        None
97    };
98
99    let mut messages: Vec<CosmosMsg> = Vec::new();
100    messages.push(ica_channel_open_init_msg);
101    if let Some(msg) = callback_factory_msg {
102        messages.push(msg)
103    }
104    
105    Ok(Response::new().add_messages(messages).add_event(event).add_attribute("outpost_address", env.contract.address.to_string()))  
106    } else {
107        Ok(Response::default())
108    }
109}
110
111/// Handles the execution of the contract.
112#[cfg(not(feature = "no_exports"))]
113#[entry_point]
114pub fn execute(
115    deps: DepsMut,
116    env: Env,
117    info: MessageInfo,
118    msg: ExecuteMsg,
119) -> Result<Response, ContractError> {
120    match msg {
121        ExecuteMsg::CreateChannel {
122            channel_open_init_options,
123        } => execute::create_channel(deps, env, info, channel_open_init_options),
124        ExecuteMsg::SendCosmosMsgs {
125            messages,
126            packet_memo,
127            timeout_seconds,
128        } => {
129            execute::send_cosmos_msgs(deps, env, info, messages, packet_memo, timeout_seconds)
130        },
131    }
132}
133
134/// Handles the query of the contract.
135#[cfg(not(feature = "no_exports"))]
136#[entry_point]
137pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
138    match msg {
139        QueryMsg::GetContractState {} => to_json_binary(&query::state(deps)?),
140        QueryMsg::GetChannel {} => to_json_binary(&query::channel(deps)?),
141        QueryMsg::GetCallbackCounter {} => to_json_binary(&query::callback_counter(deps)?),
142        QueryMsg::Ownership {} => to_json_binary(&query::get_owner(deps)?),
143    }
144}
145
146/// Migrate contract if version is lower than current version
147#[cfg(not(feature = "no_exports"))]
148#[entry_point]
149pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
150    migrate::validate_semver(deps.as_ref())?;
151
152    cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
153    // If state structure changed in any contract version in the way migration is needed, it
154    // should occur here
155
156    Ok(Response::default())
157}
158
159mod execute {
160    use cosmwasm_std::{coin, coins, BankMsg, CosmosMsg, IbcMsg, IbcTimeout, IbcTimeoutBlock, StdResult};
161    use prost::Message;
162
163    use crate::{
164        ibc::types::{metadata::TxEncoding, packet::IcaPacketData, stargate::channel},
165        types::msg::options::ChannelOpenInitOptions,
166    };
167
168    use cosmos_sdk_proto::cosmos::{bank::v1beta1::MsgSend, base::v1beta1::Coin};
169    use cosmos_sdk_proto::Any;
170
171    use super::*;
172
173    /// Submits a stargate `MsgChannelOpenInit` to the chain.
174    /// Can only be called by the contract owner or a whitelisted address.
175    /// Only the contract owner can include the channel open init options.
176    
177    pub fn create_channel(
178        deps: DepsMut,
179        env: Env,
180        info: MessageInfo,
181        options: Option<ChannelOpenInitOptions>,
182    ) -> Result<Response, ContractError> {
183        cw_ownable::assert_owner(deps.storage, &info.sender)?;
184
185        let options = if let Some(new_options) = options {
186            state::CHANNEL_OPEN_INIT_OPTIONS.save(deps.storage, &new_options)?;
187            new_options
188        } else {
189            state::CHANNEL_OPEN_INIT_OPTIONS
190                .may_load(deps.storage)?
191                .ok_or(ContractError::NoChannelInitOptions)?
192        };
193
194        state::ALLOW_CHANNEL_OPEN_INIT.save(deps.storage, &true)?;
195
196        let ica_channel_open_init_msg = new_ica_channel_open_init_cosmos_msg(
197            env.contract.address.to_string(),
198            options.connection_id,
199            options.counterparty_port_id,
200            options.counterparty_connection_id,
201            options.tx_encoding, // This is kind of redundant because only proto3 is supported now 
202            options.channel_ordering,
203        );
204
205        Ok(Response::new().add_message(ica_channel_open_init_msg))
206    }
207
208    /// Sends an array of [`CosmosMsg`] to the ICA host.
209    #[allow(clippy::needless_pass_by_value)]
210    pub fn send_cosmos_msgs(
211        deps: DepsMut,
212        env: Env,
213        info: MessageInfo,
214        messages: Vec<CosmosMsg>,
215        packet_memo: Option<String>,
216        timeout_seconds: Option<u64>,
217        // Optional Size_of_data - v0.1.1 release?
218    ) -> Result<Response, ContractError> {
219
220        // NOTE: Ownership of the root Files{} object for filetree is also checked in canine-chain
221        // NOTE: You could give ownership of the outpost to a non-factory contract, e.g., an nft minter
222        // and the nft minter could call this function
223        cw_ownable::assert_owner(deps.storage, &info.sender)?;
224
225        let contract_state = STATE.load(deps.storage)?;
226        let ica_info = contract_state.get_ica_info()?;
227
228        let ica_packet = IcaPacketData::from_cosmos_msgs(
229            messages,
230            &ica_info.encoding,
231            packet_memo,
232            &ica_info.ica_address,
233        )?;
234        let send_packet_msg = ica_packet.to_ibc_msg(&env, ica_info.channel_id, timeout_seconds)?;
235
236        Ok(Response::default().add_message(send_packet_msg))
237
238    }
239}
240
241
242
243mod query {
244    use std::error::Error;
245
246    use cosmwasm_std::StdError;
247
248    use super::*;
249
250    /// Returns the saved contract state.
251    pub fn state(deps: Deps) -> StdResult<ContractState> {
252        STATE.load(deps.storage)
253    }
254
255    /// Returns the saved channel state if it exists.
256    pub fn channel(deps: Deps) -> StdResult<ChannelState> {
257        CHANNEL_STATE.load(deps.storage)
258    }
259
260    /// Returns the saved callback counter.
261    pub fn callback_counter(deps: Deps) -> StdResult<CallbackCounter> {
262        CALLBACK_COUNTER.load(deps.storage)
263    }
264
265    /// Return the outpost owner
266    pub fn get_owner(deps: Deps) -> StdResult<String> {
267        let ownership = cw_ownable::get_ownership(deps.storage)?;
268
269        if let Some(owner) = ownership.owner {
270            Ok(owner.to_string())
271        } else {
272            Err(StdError::generic_err("No owner found"))
273        }
274    }
275}
276
277mod migrate {
278    use super::{keys, state, ContractError, Deps};
279
280    /// Validate that the contract version is semver compliant
281    /// and greater than the previous version.
282    pub fn validate_semver(deps: Deps) -> Result<(), ContractError> {
283        let prev_cw2_version = cw2::get_contract_version(deps.storage)?;
284        if prev_cw2_version.contract != keys::CONTRACT_NAME {
285            return Err(ContractError::InvalidMigrationVersion {
286                expected: keys::CONTRACT_NAME.to_string(),
287                actual: prev_cw2_version.contract,
288            });
289        }
290
291        let version: semver::Version = keys::CONTRACT_VERSION.parse()?;
292        let prev_version: semver::Version = prev_cw2_version.version.parse()?;
293        if prev_version >= version {
294            return Err(ContractError::InvalidMigrationVersion {
295                expected: format!("> {prev_version}"),
296                actual: keys::CONTRACT_VERSION.to_string(),
297            });
298        }
299        Ok(())
300    }
301
302    /// Validate that the channel encoding is protobuf if set.
303    pub fn validate_channel_encoding(deps: Deps) -> Result<(), ContractError> {
304        // Reject the migration if the channel encoding is not protobuf
305        if let Some(ica_info) = state::STATE.load(deps.storage)?.ica_info {
306            if !matches!(
307                ica_info.encoding,
308                crate::ibc::types::metadata::TxEncoding::Protobuf
309            ) {
310                return Err(ContractError::UnsupportedPacketEncoding(
311                    ica_info.encoding.to_string(),
312                ));
313            }
314        }
315
316        Ok(())
317    }
318}