Skip to main content

abstract_account/
execution.rs

1use abstract_sdk::std::account::state::WHITELISTED_MODULES;
2use abstract_std::{
3    account::state::{ACCOUNT_MODULES, CALLING_TO_AS_ADMIN},
4    objects::ownership,
5    ICA_CLIENT,
6};
7use cosmwasm_std::{
8    Addr, Binary, Coin, CosmosMsg, DepsMut, Empty, Env, MessageInfo, StdError, SubMsg, WasmMsg,
9    WasmQuery,
10};
11
12use crate::{
13    contract::{AccountResponse, AccountResult, ADMIN_ACTION_REPLY_ID, FORWARD_RESPONSE_REPLY_ID},
14    error::AccountError,
15    modules::load_module_addr,
16};
17
18/// Check that sender either whitelisted or governance
19pub(crate) fn assert_whitelisted_owner_or_self(
20    deps: &mut DepsMut,
21    env: &Env,
22    sender: &Addr,
23) -> AccountResult<()> {
24    let whitelisted_modules = WHITELISTED_MODULES.load(deps.storage)?;
25    if whitelisted_modules.0.contains(sender)
26        || ownership::assert_nested_owner(deps.storage, &deps.querier, sender).is_ok()
27        || sender == env.contract.address
28    {
29        Ok(())
30    } else {
31        Err(AccountError::SenderNotWhitelistedOrOwner {})
32    }
33}
34
35/// Executes `Vec<CosmosMsg>` on the account.
36/// Permission: Module
37pub fn execute_msgs(
38    mut deps: DepsMut,
39    env: Env,
40    msg_sender: &Addr,
41    msgs: Vec<CosmosMsg<Empty>>,
42) -> AccountResult {
43    assert_whitelisted_owner_or_self(&mut deps, &env, msg_sender)?;
44
45    Ok(AccountResponse::action("execute_module_action").add_messages(msgs))
46}
47
48/// Executes `CosmosMsg` on the account and forwards its response.
49/// Permission: Module
50pub fn execute_msgs_with_data(
51    mut deps: DepsMut,
52    env: Env,
53    msg_sender: &Addr,
54    msg: CosmosMsg<Empty>,
55) -> AccountResult {
56    assert_whitelisted_owner_or_self(&mut deps, &env, msg_sender)?;
57
58    let submsg = SubMsg::reply_on_success(msg, FORWARD_RESPONSE_REPLY_ID);
59
60    Ok(AccountResponse::action("execute_module_action_response").add_submessage(submsg))
61}
62
63/// Execute the [`exec_msg`] on the provided [`module_id`],
64/// This is a simple wrapper around [`ExecuteMsg::Execute`](abstract_std::account::ExecuteMsg::Execute).
65pub fn execute_on_module(
66    deps: DepsMut,
67    env: Env,
68    info: MessageInfo,
69    module_id: String,
70    exec_msg: Binary,
71    funds: Vec<Coin>,
72) -> AccountResult {
73    let module_addr = load_module_addr(deps.storage, &module_id)?;
74    execute_msgs(
75        deps,
76        env,
77        &info.sender,
78        vec![CosmosMsg::Wasm(WasmMsg::Execute {
79            contract_addr: module_addr.into(),
80            msg: exec_msg,
81            funds,
82        })],
83    )
84}
85
86pub fn admin_execute(
87    deps: DepsMut,
88    info: MessageInfo,
89    addr: Addr,
90    exec_msg: Binary,
91) -> AccountResult {
92    ownership::assert_nested_owner(deps.storage, &deps.querier, &info.sender)?;
93
94    if CALLING_TO_AS_ADMIN.exists(deps.storage) {
95        return Err(AccountError::CantChainAdminCalls {});
96    }
97    CALLING_TO_AS_ADMIN.save(deps.storage, &addr)?;
98
99    let msg = SubMsg::reply_on_success(
100        WasmMsg::Execute {
101            contract_addr: addr.to_string(),
102            msg: exec_msg,
103            funds: info.funds,
104        },
105        ADMIN_ACTION_REPLY_ID,
106    );
107
108    Ok(AccountResponse::action("admin_execute").add_submessage(msg))
109}
110
111pub fn admin_execute_on_module(
112    deps: DepsMut,
113    info: MessageInfo,
114    module_id: String,
115    exec_msg: Binary,
116) -> AccountResult {
117    let module_addr = load_module_addr(deps.storage, &module_id)?;
118    admin_execute(deps, info, module_addr, exec_msg)
119}
120
121pub fn add_auth_method(
122    _deps: DepsMut,
123    _env: Env,
124    _info: MessageInfo,
125    #[allow(unused_mut)] mut _auth: crate::msg::Authenticator,
126) -> AccountResult {
127    #[cfg(feature = "xion")]
128    {
129        ownership::assert_nested_owner(_deps.storage, &_deps.querier, &_info.sender)?;
130        abstract_xion::execute::add_auth_method(_deps, &_env, &mut _auth).map_err(Into::into)
131    }
132    #[cfg(not(feature = "xion"))]
133    {
134        Err(AccountError::NoAuthFeature {})
135    }
136}
137
138pub fn remove_auth_method(_deps: DepsMut, _env: Env, _info: MessageInfo, _id: u8) -> AccountResult {
139    #[cfg(feature = "xion")]
140    {
141        ownership::assert_nested_owner(_deps.storage, &_deps.querier, &_info.sender)?;
142        abstract_xion::execute::remove_auth_method(_deps, _env, _id).map_err(Into::into)
143    }
144    #[cfg(not(feature = "xion"))]
145    {
146        Err(AccountError::NoAuthFeature {})
147    }
148}
149
150/// Execute an action on an ICA.
151/// Permission: Module
152///
153/// This function queries the `abstract:ica-client` contract from the account.
154/// It then fires a smart-query on that address of type [`QueryMsg::IcaAction`](abstract_ica::msg::QueryMsg).
155///
156/// The resulting `Vec<CosmosMsg>` are then executed on the account contract.
157pub fn ica_action(
158    mut deps: DepsMut,
159    env: Env,
160    msg_info: MessageInfo,
161    action_query: Binary,
162) -> AccountResult {
163    assert_whitelisted_owner_or_self(&mut deps, &env, &msg_info.sender)?;
164
165    let ica_client_address = ACCOUNT_MODULES
166        .may_load(deps.storage, ICA_CLIENT)?
167        .ok_or_else(|| {
168            StdError::generic_err(format!(
169                "ica_client not found on account. Add it under the {ICA_CLIENT} name."
170            ))
171        })?;
172
173    let res: abstract_ica::msg::IcaActionResult = deps.querier.query(
174        &WasmQuery::Smart {
175            contract_addr: ica_client_address.into(),
176            msg: action_query,
177        }
178        .into(),
179    )?;
180
181    Ok(AccountResponse::action("ica_action").add_messages(res.msgs))
182}
183
184#[cfg(test)]
185mod test {
186    use crate::contract::execute;
187    use crate::error::AccountError;
188    use crate::test_common::mock_init;
189    use abstract_sdk::namespaces::OWNERSHIP_STORAGE_KEY;
190    use abstract_std::account::{state::*, *};
191    use abstract_std::objects::gov_type::GovernanceDetails;
192    use abstract_std::objects::ownership::Ownership;
193    use abstract_std::{account, IBC_CLIENT};
194    use abstract_testing::prelude::*;
195    use cosmwasm_std::{coins, CosmosMsg, SubMsg};
196    use cosmwasm_std::{testing::*, Addr};
197    use cw_storage_plus::Item;
198
199    #[coverage_helper::test]
200    fn abstract_account_can_execute_on_itself() -> anyhow::Result<()> {
201        let mut deps = mock_dependencies();
202        deps.querier = abstract_mock_querier(deps.api);
203        mock_init(&mut deps)?;
204
205        let env = mock_env_validated(deps.api);
206        // We set the contract as owner.
207        // We can't make it through execute msgs, because of XION signatures are too messy to reproduce in tests
208        let ownership = Ownership {
209            owner: GovernanceDetails::AbstractAccount {
210                address: env.contract.address.clone(),
211            }
212            .verify(deps.as_ref())?,
213            pending_owner: None,
214            pending_expiry: None,
215        };
216        const OWNERSHIP: Item<Ownership<Addr>> = Item::new(OWNERSHIP_STORAGE_KEY);
217        OWNERSHIP.save(deps.as_mut().storage, &ownership)?;
218
219        // Module calls nested admin calls on account, making it admin
220        let info = message_info(&env.contract.address, &[]);
221
222        let msg = ExecuteMsg::Execute { msgs: vec![] };
223
224        execute(deps.as_mut(), env.clone(), info, msg).unwrap();
225
226        Ok(())
227    }
228
229    mod execute_action {
230
231        use cosmwasm_std::{testing::MOCK_CONTRACT_ADDR, wasm_execute, CosmosMsg};
232
233        use super::*;
234
235        #[cfg(feature = "xion")]
236        #[coverage_helper::test]
237        fn admin_actions_not_chained() -> anyhow::Result<()> {
238            use crate::contract::AccountResult;
239            use abstract_sdk::namespaces::OWNERSHIP_STORAGE_KEY;
240            use abstract_std::objects::{gov_type::GovernanceDetails, ownership::Ownership};
241            use cosmwasm_std::{Addr, Binary, DepsMut, Empty, Env, Response, WasmMsg};
242            use cw_storage_plus::Item;
243
244            fn execute_from_res(deps: DepsMut, env: Env, res: Response) -> AccountResult<Response> {
245                // Execute all messages
246                let info = message_info(&env.contract.address, &[]);
247                if let CosmosMsg::Wasm(WasmMsg::Execute {
248                    contract_addr: _,
249                    msg,
250                    funds: _,
251                }) = res.messages[0].msg.clone()
252                {
253                    execute(deps, env.clone(), info, from_json(&msg)?).map_err(Into::into)
254                } else {
255                    panic!("Wrong message received");
256                }
257            }
258            let mut deps = mock_dependencies();
259            deps.querier = abstract_mock_querier(deps.api);
260            mock_init(&mut deps)?;
261            let env = mock_env_validated(deps.api);
262
263            // We set the contract as owner.
264            // We can't make it through execute msgs, because signatures are too messy to reproduce in tests
265            let ownership = Ownership {
266                owner: GovernanceDetails::AbstractAccount {
267                    address: env.contract.address.clone(),
268                }
269                .verify(deps.as_ref())?,
270                pending_owner: None,
271                pending_expiry: None,
272            };
273
274            const OWNERSHIP: Item<Ownership<Addr>> = Item::new(OWNERSHIP_STORAGE_KEY);
275            OWNERSHIP.save(deps.as_mut().storage, &ownership)?;
276
277            let msg = ExecuteMsg::AdminExecute {
278                addr: env.contract.address.to_string(),
279                msg: to_json_binary(&ExecuteMsg::<Empty>::AdminExecute {
280                    addr: env.contract.address.to_string(),
281                    msg: Binary::new(vec![]),
282                })?,
283            };
284
285            let info = message_info(&env.contract.address, &[]);
286            let env = mock_env_validated(deps.api);
287
288            // We simulate it's an admin call
289            AUTH_ADMIN.save(deps.as_mut().storage, &true)?;
290            let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap();
291            // We simulate it's still an admin call
292            AUTH_ADMIN.save(deps.as_mut().storage, &true)?;
293            let res = execute_from_res(deps.as_mut(), env, res);
294            assert_eq!(res, Err(AccountError::CantChainAdminCalls {}));
295            Ok(())
296        }
297
298        #[coverage_helper::test]
299        fn only_whitelisted_can_execute() -> anyhow::Result<()> {
300            let mut deps = mock_dependencies();
301            deps.querier = abstract_mock_querier(deps.api);
302            mock_init(&mut deps)?;
303
304            let msg = ExecuteMsg::Execute { msgs: vec![] };
305
306            let info = message_info(&deps.api.addr_make("not_whitelisted"), &[]);
307            let env = mock_env_validated(deps.api);
308
309            let res = execute(deps.as_mut(), env, info, msg);
310            assert_eq!(res, Err(AccountError::SenderNotWhitelistedOrOwner {}));
311            Ok(())
312        }
313
314        #[coverage_helper::test]
315        fn forwards_action() -> anyhow::Result<()> {
316            let mut deps = mock_dependencies();
317            deps.querier = abstract_mock_querier(deps.api);
318            let env = mock_env_validated(deps.api);
319            mock_init(&mut deps)?;
320
321            // stub a module
322            let module_addr = deps.api.addr_make(TEST_MODULE_ID);
323            WHITELISTED_MODULES.save(
324                &mut deps.storage,
325                &WhitelistedModules(vec![module_addr.clone()]),
326            )?;
327
328            let action: CosmosMsg = wasm_execute(
329                MOCK_CONTRACT_ADDR.to_string(),
330                // example garbage
331                &<ExecuteMsg>::UpdateOwnership(
332                    abstract_std::objects::gov_type::GovAction::RenounceOwnership,
333                ),
334                vec![],
335            )?
336            .into();
337
338            let msg = ExecuteMsg::Execute {
339                msgs: vec![action.clone()],
340            };
341
342            // execute it AS the module
343            let res = execute(deps.as_mut(), env, message_info(&module_addr, &[]), msg);
344            assert!(res.is_ok());
345
346            let msgs = res?.messages;
347            assert_eq!(msgs.len(), 1);
348
349            let msg = &msgs[0];
350            assert_eq!(msg.msg, action);
351
352            Ok(())
353        }
354    }
355
356    mod execute_ibc {
357        use crate::modules::update_module_addresses;
358
359        use super::*;
360
361        #[coverage_helper::test]
362        fn add_module() -> anyhow::Result<()> {
363            let mut deps = mock_dependencies();
364            deps.querier = abstract_mock_querier(deps.api);
365            let env = mock_env_validated(deps.api);
366            mock_init(&mut deps)?;
367            let abstr = AbstractMockAddrs::new(deps.api);
368            // whitelist creator
369            account::state::WHITELISTED_MODULES.save(
370                &mut deps.storage,
371                &WhitelistedModules(vec![abstr.account.addr().clone()]),
372            )?;
373
374            let msg = ExecuteMsg::ExecuteOnModule {
375                module_id: IBC_CLIENT.to_owned(),
376                exec_msg: to_json_binary(&abstract_std::ibc_client::ExecuteMsg::Register {
377                    host_chain: "juno".parse()?,
378                    namespace: None,
379                    install_modules: vec![],
380                })?,
381                funds: vec![],
382            };
383
384            let not_whitelisted_info = message_info(&deps.api.addr_make("not_whitelisted"), &[]);
385            execute(
386                deps.as_mut(),
387                env.clone(),
388                not_whitelisted_info,
389                msg.clone(),
390            )
391            .unwrap_err();
392
393            let account_info = message_info(abstr.account.addr(), &[]);
394            // ibc not enabled
395            execute(
396                deps.as_mut(),
397                env.clone(),
398                account_info.clone(),
399                msg.clone(),
400            )
401            .unwrap_err();
402            // mock enabling ibc
403            let ibc_client_addr = deps.api.addr_make("ibc_client_addr");
404            deps.querier = MockQuerierBuilder::default()
405                .with_contract_map_entry(
406                    abstr.account.addr(),
407                    account::state::ACCOUNT_MODULES,
408                    (IBC_CLIENT, ibc_client_addr.clone()),
409                )
410                .build();
411            // mock enabling ibc
412            update_module_addresses(
413                deps.as_mut(),
414                vec![(IBC_CLIENT.into(), ibc_client_addr.clone())],
415                vec![],
416            )?;
417
418            let res = execute(deps.as_mut(), env, account_info, msg)?;
419            assert_eq!(res.messages.len(), 1);
420            assert_eq!(
421                res.messages[0],
422                SubMsg::new(CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute {
423                    contract_addr: ibc_client_addr.to_string(),
424                    msg: to_json_binary(&abstract_std::ibc_client::ExecuteMsg::Register {
425                        host_chain: "juno".parse()?,
426                        namespace: None,
427                        install_modules: vec![],
428                    })?,
429                    funds: vec![],
430                },))
431            );
432            Ok(())
433        }
434
435        #[coverage_helper::test]
436        fn send_funds() -> anyhow::Result<()> {
437            let mut deps = mock_dependencies();
438            deps.querier = abstract_mock_querier(deps.api);
439            let env = mock_env_validated(deps.api);
440            mock_init(&mut deps)?;
441            let abstr = AbstractMockAddrs::new(deps.api);
442            // whitelist creator
443            account::state::WHITELISTED_MODULES.save(
444                &mut deps.storage,
445                &WhitelistedModules(vec![abstr.account.addr().clone()]),
446            )?;
447
448            let funds = coins(10, "denom");
449            let msg = ExecuteMsg::ExecuteOnModule {
450                module_id: IBC_CLIENT.to_owned(),
451                exec_msg: to_json_binary(&abstract_std::ibc_client::ExecuteMsg::SendFunds {
452                    host_chain: "juno".parse()?,
453                    memo: None,
454                    receiver: None,
455                })?,
456                funds: funds.clone(),
457            };
458
459            let not_whitelisted_info = message_info(&deps.api.addr_make("not_whitelisted"), &[]);
460            execute(
461                deps.as_mut(),
462                env.clone(),
463                not_whitelisted_info,
464                msg.clone(),
465            )
466            .unwrap_err();
467
468            let account_info = message_info(abstr.account.addr(), &[]);
469            // ibc not enabled
470            execute(
471                deps.as_mut(),
472                env.clone(),
473                account_info.clone(),
474                msg.clone(),
475            )
476            .unwrap_err();
477            // mock enabling ibc
478            let ibc_client_addr = deps.api.addr_make("ibc_client_addr");
479            deps.querier = MockQuerierBuilder::default()
480                .with_contract_map_entry(
481                    &abstr.account.addr().clone(),
482                    account::state::ACCOUNT_MODULES,
483                    (IBC_CLIENT, ibc_client_addr.clone()),
484                )
485                .build();
486            update_module_addresses(
487                deps.as_mut(),
488                vec![(IBC_CLIENT.into(), ibc_client_addr.clone())],
489                vec![],
490            )?;
491
492            let res = execute(deps.as_mut(), env, account_info, msg)?;
493            assert_eq!(res.messages.len(), 1);
494            assert_eq!(
495                res.messages[0],
496                SubMsg::new(CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute {
497                    contract_addr: ibc_client_addr.into(),
498                    msg: to_json_binary(&abstract_std::ibc_client::ExecuteMsg::SendFunds {
499                        host_chain: "juno".parse()?,
500                        memo: None,
501                        receiver: None,
502                    })?,
503                    funds,
504                },))
505            );
506            Ok(())
507        }
508    }
509
510    mod ica_action {
511        use abstract_ica::msg::IcaActionResult;
512        use abstract_std::ICA_CLIENT;
513        use cosmwasm_std::{Binary, Empty};
514
515        use crate::modules::update_module_addresses;
516
517        use super::*;
518
519        #[coverage_helper::test]
520        fn ica_action() -> anyhow::Result<()> {
521            let mut deps = mock_dependencies();
522            deps.querier = abstract_mock_querier(deps.api);
523            let env = mock_env_validated(deps.api);
524            let abstr = AbstractMockAddrs::new(deps.api);
525            let ica_client_addr = deps.api.addr_make("ica_client_addr");
526            mock_init(&mut deps)?;
527            // whitelist creator
528            account::state::WHITELISTED_MODULES.save(
529                &mut deps.storage,
530                &WhitelistedModules(vec![abstr.account.addr().clone()]),
531            )?;
532
533            let action = Binary::from(b"some_action");
534            let msg = ExecuteMsg::IcaAction {
535                action_query_msg: action.clone(),
536            };
537
538            let not_whitelisted_info = message_info(&deps.api.addr_make("not_whitelisted"), &[]);
539            execute(
540                deps.as_mut(),
541                env.clone(),
542                not_whitelisted_info,
543                msg.clone(),
544            )
545            .unwrap_err();
546
547            let account_info = message_info(abstr.account.addr(), &[]);
548            // ibc not enabled
549            execute(
550                deps.as_mut(),
551                env.clone(),
552                account_info.clone(),
553                msg.clone(),
554            )
555            .unwrap_err();
556            // mock enabling ibc
557            update_module_addresses(
558                deps.as_mut(),
559                vec![(ICA_CLIENT.into(), ica_client_addr.clone())],
560                vec![],
561            )?;
562
563            deps.querier = MockQuerierBuilder::default()
564                .with_smart_handler(&ica_client_addr, move |bin| {
565                    if bin.eq(&action) {
566                        Ok(to_json_binary(&IcaActionResult {
567                            msgs: vec![CosmosMsg::Custom(Empty {})],
568                        })
569                        .unwrap())
570                    } else {
571                        Err("Unexpected action query".to_owned())
572                    }
573                })
574                .build();
575
576            let res = execute(deps.as_mut(), env, account_info, msg)?;
577            assert_eq!(res.messages.len(), 1);
578            assert_eq!(res.messages[0], SubMsg::new(CosmosMsg::Custom(Empty {})));
579            Ok(())
580        }
581    }
582}