1use cosmwasm_std::{
2 attr, entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response,
3 StdError, StdResult, Uint128,
4};
5use cw20::{
6 AllAccountsResponse, BalanceResponse, Cw20Coin, Cw20ReceiveMsg, EmbeddedLogo, Logo, LogoInfo,
7 MarketingInfoResponse,
8};
9use cw20_base::allowances::{
10 deduct_allowance, execute_decrease_allowance, execute_increase_allowance, query_allowance,
11};
12
13use crate::state::{capture_total_supply_history, check_minter, get_total_supply_at, BALANCES};
14use astroport::asset::addr_opt_validate;
15use astroport::xastro_token::{InstantiateMsg, MigrateMsg, QueryMsg};
16use cw2::{get_contract_version, set_contract_version};
17use cw20_base::contract::{
18 execute_update_marketing, execute_upload_logo, query_download_logo, query_marketing_info,
19 query_minter, query_token_info,
20};
21use cw20_base::enumerable::query_owner_allowances;
22use cw20_base::msg::ExecuteMsg;
23use cw20_base::state::{MinterData, TokenInfo, LOGO, MARKETING_INFO, TOKEN_INFO};
24use cw20_base::ContractError;
25use cw_storage_plus::Bound;
26
27const CONTRACT_NAME: &str = "astroport-xastro-token";
29const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
31
32const MAX_LIMIT: u32 = 30;
34const DEFAULT_LIMIT: u32 = 10;
35
36const LOGO_SIZE_CAP: usize = 5 * 1024;
37
38fn verify_xml_preamble(data: &[u8]) -> Result<(), ContractError> {
40 let preamble = data
44 .split_inclusive(|c| *c == b'>')
45 .next()
46 .ok_or(ContractError::InvalidXmlPreamble {})?;
47
48 const PREFIX: &[u8] = b"<?xml ";
49 const POSTFIX: &[u8] = b"?>";
50
51 if !(preamble.starts_with(PREFIX) && preamble.ends_with(POSTFIX)) {
52 Err(ContractError::InvalidXmlPreamble {})
53 } else {
54 Ok(())
55 }
56
57 }
60
61fn verify_xml_logo(logo: &[u8]) -> Result<(), ContractError> {
63 verify_xml_preamble(logo)?;
64
65 if logo.len() > LOGO_SIZE_CAP {
66 Err(ContractError::LogoTooBig {})
67 } else {
68 Ok(())
69 }
70}
71
72fn verify_png_logo(logo: &[u8]) -> Result<(), ContractError> {
74 const HEADER: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
81 if logo.len() > LOGO_SIZE_CAP {
82 Err(ContractError::LogoTooBig {})
83 } else if !logo.starts_with(&HEADER) {
84 Err(ContractError::InvalidPngHeader {})
85 } else {
86 Ok(())
87 }
88}
89
90fn verify_logo(logo: &Logo) -> Result<(), ContractError> {
92 match logo {
93 Logo::Embedded(EmbeddedLogo::Svg(logo)) => verify_xml_logo(logo),
94 Logo::Embedded(EmbeddedLogo::Png(logo)) => verify_png_logo(logo),
95 Logo::Url(_) => Ok(()), }
97}
98
99#[cfg_attr(not(feature = "library"), entry_point)]
101pub fn instantiate(
102 mut deps: DepsMut,
103 env: Env,
104 _info: MessageInfo,
105 msg: InstantiateMsg,
106) -> Result<Response, ContractError> {
107 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
108
109 msg.validate()?;
111
112 let total_supply = create_accounts(&mut deps, &env, &msg.initial_balances)?;
114
115 if !total_supply.is_zero() {
116 capture_total_supply_history(deps.storage, &env, total_supply)?;
117 }
118
119 if let Some(limit) = msg.get_cap() {
121 if total_supply > limit {
122 return Err(StdError::generic_err("Initial supply greater than cap").into());
123 }
124 }
125
126 let mint = match msg.mint {
127 Some(m) => Some(MinterData {
128 minter: deps.api.addr_validate(&m.minter)?,
129 cap: m.cap,
130 }),
131 None => None,
132 };
133
134 let data = TokenInfo {
136 name: msg.name,
137 symbol: msg.symbol,
138 decimals: msg.decimals,
139 total_supply,
140 mint,
141 };
142 TOKEN_INFO.save(deps.storage, &data)?;
143
144 if let Some(marketing) = msg.marketing {
145 let logo = if let Some(logo) = marketing.logo {
146 verify_logo(&logo)?;
147 LOGO.save(deps.storage, &logo)?;
148
149 match logo {
150 Logo::Url(url) => Some(LogoInfo::Url(url)),
151 Logo::Embedded(_) => Some(LogoInfo::Embedded),
152 }
153 } else {
154 None
155 };
156
157 let data = MarketingInfoResponse {
158 project: marketing.project,
159 description: marketing.description,
160 marketing: marketing
161 .marketing
162 .map(|addr| deps.api.addr_validate(&addr))
163 .transpose()?,
164 logo,
165 };
166 MARKETING_INFO.save(deps.storage, &data)?;
167 }
168
169 Ok(Response::default())
170}
171
172pub fn create_accounts(deps: &mut DepsMut, env: &Env, accounts: &[Cw20Coin]) -> StdResult<Uint128> {
176 let mut total_supply = Uint128::zero();
177
178 for row in accounts {
179 let address = deps.api.addr_validate(&row.address)?;
180 BALANCES.save(deps.storage, &address, &row.amount, env.block.height)?;
181 total_supply += row.amount;
182 }
183
184 Ok(total_supply)
185}
186
187#[cfg_attr(not(feature = "library"), entry_point)]
212pub fn execute(
213 deps: DepsMut,
214 env: Env,
215 info: MessageInfo,
216 msg: ExecuteMsg,
217) -> Result<Response, ContractError> {
218 match msg {
219 ExecuteMsg::Transfer { recipient, amount } => {
220 execute_transfer(deps, env, info, recipient, amount)
221 }
222 ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount),
223 ExecuteMsg::Send {
224 contract,
225 amount,
226 msg,
227 } => execute_send(deps, env, info, contract, amount, msg),
228 ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount),
229 ExecuteMsg::IncreaseAllowance {
230 spender,
231 amount,
232 expires,
233 } => execute_increase_allowance(deps, env, info, spender, amount, expires),
234 ExecuteMsg::DecreaseAllowance {
235 spender,
236 amount,
237 expires,
238 } => execute_decrease_allowance(deps, env, info, spender, amount, expires),
239 ExecuteMsg::TransferFrom {
240 owner,
241 recipient,
242 amount,
243 } => execute_transfer_from(deps, env, info, owner, recipient, amount),
244 ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount),
245 ExecuteMsg::SendFrom {
246 owner,
247 contract,
248 amount,
249 msg,
250 } => execute_send_from(deps, env, info, owner, contract, amount, msg),
251 ExecuteMsg::UpdateMarketing {
252 project,
253 description,
254 marketing,
255 } => execute_update_marketing(deps, env, info, project, description, marketing),
256 ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo),
257 _ => Err(StdError::generic_err("Unsupported execute message").into()),
258 }
259}
260
261pub fn execute_transfer(
263 deps: DepsMut,
264 env: Env,
265 info: MessageInfo,
266 recipient: String,
267 amount: Uint128,
268) -> Result<Response, ContractError> {
269 if amount == Uint128::zero() {
270 return Err(ContractError::InvalidZeroAmount {});
271 }
272
273 let rcpt_addr = deps.api.addr_validate(&recipient)?;
274
275 BALANCES.update(
276 deps.storage,
277 &info.sender,
278 env.block.height,
279 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_sub(amount)?) },
280 )?;
281 BALANCES.update(
282 deps.storage,
283 &rcpt_addr,
284 env.block.height,
285 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
286 )?;
287
288 Ok(Response::new().add_attributes(vec![
289 attr("action", "transfer"),
290 attr("from", info.sender),
291 attr("to", rcpt_addr),
292 attr("amount", amount),
293 ]))
294}
295
296pub fn execute_burn(
300 deps: DepsMut,
301 env: Env,
302 info: MessageInfo,
303 amount: Uint128,
304) -> Result<Response, ContractError> {
305 if amount == Uint128::zero() {
306 return Err(ContractError::InvalidZeroAmount {});
307 }
308
309 let config = TOKEN_INFO.load(deps.storage)?;
310 check_minter(&info.sender, &config)?;
311
312 BALANCES.update(
314 deps.storage,
315 &info.sender,
316 env.block.height,
317 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_sub(amount)?) },
318 )?;
319
320 let token_info = TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> {
322 info.total_supply = info.total_supply.checked_sub(amount)?;
323 Ok(info)
324 })?;
325
326 capture_total_supply_history(deps.storage, &env, token_info.total_supply)?;
327
328 let res = Response::new().add_attributes(vec![
329 attr("action", "burn"),
330 attr("from", info.sender),
331 attr("amount", amount),
332 ]);
333 Ok(res)
334}
335
336pub fn execute_mint(
338 deps: DepsMut,
339 env: Env,
340 info: MessageInfo,
341 recipient: String,
342 amount: Uint128,
343) -> Result<Response, ContractError> {
344 if amount == Uint128::zero() {
345 return Err(ContractError::InvalidZeroAmount {});
346 }
347
348 let mut config = TOKEN_INFO.load(deps.storage)?;
349 check_minter(&info.sender, &config)?;
350
351 config.total_supply = config
353 .total_supply
354 .checked_add(amount)
355 .map_err(StdError::from)?;
356 if let Some(limit) = config.get_cap() {
357 if config.total_supply > limit {
358 return Err(ContractError::CannotExceedCap {});
359 }
360 }
361
362 TOKEN_INFO.save(deps.storage, &config)?;
363
364 capture_total_supply_history(deps.storage, &env, config.total_supply)?;
365
366 let rcpt_addr = deps.api.addr_validate(&recipient)?;
368 BALANCES.update(
369 deps.storage,
370 &rcpt_addr,
371 env.block.height,
372 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
373 )?;
374
375 Ok(Response::new().add_attributes(vec![
376 attr("action", "mint"),
377 attr("to", rcpt_addr),
378 attr("amount", amount),
379 ]))
380}
381
382pub fn execute_send(
390 deps: DepsMut,
391 env: Env,
392 info: MessageInfo,
393 contract: String,
394 amount: Uint128,
395 msg: Binary,
396) -> Result<Response, ContractError> {
397 if amount == Uint128::zero() {
398 return Err(ContractError::InvalidZeroAmount {});
399 }
400
401 let rcpt_addr = deps.api.addr_validate(&contract)?;
402
403 BALANCES.update(
405 deps.storage,
406 &info.sender,
407 env.block.height,
408 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_sub(amount)?) },
409 )?;
410 BALANCES.update(
411 deps.storage,
412 &rcpt_addr,
413 env.block.height,
414 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
415 )?;
416
417 let res = Response::new()
418 .add_attributes(vec![
419 attr("action", "send"),
420 attr("from", &info.sender),
421 attr("to", &rcpt_addr),
422 attr("amount", amount),
423 ])
424 .add_message(
425 Cw20ReceiveMsg {
426 sender: info.sender.into(),
427 amount,
428 msg,
429 }
430 .into_cosmos_msg(contract)?,
431 );
432 Ok(res)
433}
434
435pub fn execute_transfer_from(
443 deps: DepsMut,
444 env: Env,
445 info: MessageInfo,
446 owner: String,
447 recipient: String,
448 amount: Uint128,
449) -> Result<Response, ContractError> {
450 let rcpt_addr = deps.api.addr_validate(&recipient)?;
451 let owner_addr = deps.api.addr_validate(&owner)?;
452
453 deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?;
455
456 BALANCES.update(
457 deps.storage,
458 &owner_addr,
459 env.block.height,
460 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_sub(amount)?) },
461 )?;
462 BALANCES.update(
463 deps.storage,
464 &rcpt_addr,
465 env.block.height,
466 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_add(amount)?) },
467 )?;
468
469 let res = Response::new().add_attributes(vec![
470 attr("action", "transfer_from"),
471 attr("from", owner),
472 attr("to", recipient),
473 attr("by", info.sender),
474 attr("amount", amount),
475 ]);
476 Ok(res)
477}
478
479pub fn execute_burn_from(
485 deps: DepsMut,
486 env: Env,
487 info: MessageInfo,
488 owner: String,
489 amount: Uint128,
490) -> Result<Response, ContractError> {
491 let owner_addr = deps.api.addr_validate(&owner)?;
492
493 let config = TOKEN_INFO.load(deps.storage)?;
494 check_minter(&info.sender, &config)?;
495
496 deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?;
498
499 BALANCES.update(
501 deps.storage,
502 &owner_addr,
503 env.block.height,
504 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_sub(amount)?) },
505 )?;
506
507 let token_info = TOKEN_INFO.update(deps.storage, |mut meta| -> StdResult<_> {
509 meta.total_supply = meta.total_supply.checked_sub(amount)?;
510 Ok(meta)
511 })?;
512
513 capture_total_supply_history(deps.storage, &env, token_info.total_supply)?;
514
515 let res = Response::new().add_attributes(vec![
516 attr("action", "burn_from"),
517 attr("from", owner),
518 attr("by", info.sender),
519 attr("amount", amount),
520 ]);
521 Ok(res)
522}
523
524pub fn execute_send_from(
534 deps: DepsMut,
535 env: Env,
536 info: MessageInfo,
537 owner: String,
538 contract: String,
539 amount: Uint128,
540 msg: Binary,
541) -> Result<Response, ContractError> {
542 let rcpt_addr = deps.api.addr_validate(&contract)?;
543 let owner_addr = deps.api.addr_validate(&owner)?;
544
545 deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?;
547
548 BALANCES.update(
550 deps.storage,
551 &owner_addr,
552 env.block.height,
553 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_sub(amount)?) },
554 )?;
555 BALANCES.update(
556 deps.storage,
557 &rcpt_addr,
558 env.block.height,
559 |balance| -> StdResult<_> { Ok(balance.unwrap_or_default().checked_add(amount)?) },
560 )?;
561
562 let res = Response::new()
563 .add_attributes(vec![
564 attr("action", "send_from"),
565 attr("from", &owner),
566 attr("to", &contract),
567 attr("by", &info.sender),
568 attr("amount", amount),
569 ])
570 .add_message(
571 Cw20ReceiveMsg {
572 sender: info.sender.into(),
573 amount,
574 msg,
575 }
576 .into_cosmos_msg(contract)?,
577 );
578 Ok(res)
579}
580
581#[cfg_attr(not(feature = "library"), entry_point)]
613pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
614 match msg {
615 QueryMsg::Balance { address } => to_json_binary(&query_balance(deps, address)?),
616 QueryMsg::BalanceAt { address, block } => {
617 to_json_binary(&query_balance_at(deps, address, block)?)
618 }
619 QueryMsg::TotalSupplyAt { block } => {
620 to_json_binary(&get_total_supply_at(deps.storage, block)?)
621 }
622 QueryMsg::TokenInfo {} => to_json_binary(&query_token_info(deps)?),
623 QueryMsg::Minter {} => to_json_binary(&query_minter(deps)?),
624 QueryMsg::Allowance { owner, spender } => {
625 to_json_binary(&query_allowance(deps, owner, spender)?)
626 }
627 QueryMsg::AllAllowances {
628 owner,
629 start_after,
630 limit,
631 } => to_json_binary(&query_owner_allowances(deps, owner, start_after, limit)?),
632 QueryMsg::AllAccounts { start_after, limit } => {
633 to_json_binary(&query_all_accounts(deps, start_after, limit)?)
634 }
635 QueryMsg::MarketingInfo {} => to_json_binary(&query_marketing_info(deps)?),
636 QueryMsg::DownloadLogo {} => to_json_binary(&query_download_logo(deps)?),
637 }
638}
639
640pub fn query_balance(deps: Deps, address: String) -> StdResult<BalanceResponse> {
642 let address = deps.api.addr_validate(&address)?;
643 let balance = BALANCES
644 .may_load(deps.storage, &address)?
645 .unwrap_or_default();
646 Ok(BalanceResponse { balance })
647}
648
649pub fn query_balance_at(deps: Deps, address: String, block: u64) -> StdResult<BalanceResponse> {
651 let address = deps.api.addr_validate(&address)?;
652 let balance = BALANCES
653 .may_load_at_height(deps.storage, &address, block)?
654 .unwrap_or_default();
655 Ok(BalanceResponse { balance })
656}
657
658pub fn query_all_accounts(
664 deps: Deps,
665 start_after: Option<String>,
666 limit: Option<u32>,
667) -> StdResult<AllAccountsResponse> {
668 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
669 let start = addr_opt_validate(deps.api, &start_after)?;
670 let start = start.as_ref().map(Bound::exclusive);
671
672 let accounts = BALANCES
673 .keys(deps.storage, start, None, Order::Ascending)
674 .take(limit)
675 .map(|addr| addr.map(Into::into))
676 .collect::<StdResult<_>>()?;
677
678 Ok(AllAccountsResponse { accounts })
679}
680
681#[cfg_attr(not(feature = "library"), entry_point)]
682pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
683 let contract_version = get_contract_version(deps.storage)?;
684
685 match contract_version.contract.as_ref() {
686 "astroport-xastro-token" => match contract_version.version.as_ref() {
687 "1.0.0" | "1.0.1" | "1.0.2" => {}
688 _ => {
689 return Err(StdError::generic_err(
690 "Cannot migrate. Unsupported contract version",
691 ))
692 }
693 },
694 _ => {
695 return Err(StdError::generic_err(
696 "Cannot migrate. Unsupported contract name",
697 ))
698 }
699 }
700
701 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
702
703 Ok(Response::default()
704 .add_attribute("previous_contract_name", &contract_version.contract)
705 .add_attribute("previous_contract_version", &contract_version.version)
706 .add_attribute("new_contract_name", CONTRACT_NAME)
707 .add_attribute("new_contract_version", CONTRACT_VERSION))
708}
709
710#[cfg(test)]
711mod tests {
712 use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
713 use cosmwasm_std::{Addr, StdError};
714
715 use super::*;
716 use astroport::xastro_token::InstantiateMarketingInfo;
717
718 mod marketing {
719 use cw20::DownloadLogoResponse;
720 use cw20_base::contract::{query_download_logo, query_marketing_info};
721
722 use super::*;
723
724 #[test]
725 fn basic() {
726 let mut deps = mock_dependencies();
727 let instantiate_msg = InstantiateMsg {
728 name: "Cash Token".to_string(),
729 symbol: "CASH".to_string(),
730 decimals: 9,
731 initial_balances: vec![],
732 mint: None,
733 marketing: Some(InstantiateMarketingInfo {
734 project: Some("Project".to_owned()),
735 description: Some("Description".to_owned()),
736 marketing: Some("marketing".to_owned()),
737 logo: Some(Logo::Url("url".to_owned())),
738 }),
739 };
740
741 let info = mock_info("creator", &[]);
742 let env = mock_env();
743 let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap();
744 assert_eq!(0, res.messages.len());
745
746 assert_eq!(
747 query_marketing_info(deps.as_ref()).unwrap(),
748 MarketingInfoResponse {
749 project: Some("Project".to_owned()),
750 description: Some("Description".to_owned()),
751 marketing: Some(Addr::unchecked("marketing")),
752 logo: Some(LogoInfo::Url("url".to_owned())),
753 }
754 );
755
756 let err = query_download_logo(deps.as_ref()).unwrap_err();
757 assert!(
758 matches!(err, StdError::NotFound { .. }),
759 "Expected StdError::NotFound, received {}",
760 err
761 );
762 }
763
764 #[test]
765 fn svg() {
766 let mut deps = mock_dependencies();
767 let img = "<?xml version=\"1.0\"?><svg></svg>".as_bytes();
768 let instantiate_msg = InstantiateMsg {
769 name: "Cash Token".to_string(),
770 symbol: "CASH".to_string(),
771 decimals: 9,
772 initial_balances: vec![],
773 mint: None,
774 marketing: Some(InstantiateMarketingInfo {
775 project: Some("Project".to_owned()),
776 description: Some("Description".to_owned()),
777 marketing: Some("marketing".to_owned()),
778 logo: Some(Logo::Embedded(EmbeddedLogo::Svg(img.into()))),
779 }),
780 };
781
782 let info = mock_info("creator", &[]);
783 let env = mock_env();
784 let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap();
785 assert_eq!(0, res.messages.len());
786
787 assert_eq!(
788 query_marketing_info(deps.as_ref()).unwrap(),
789 MarketingInfoResponse {
790 project: Some("Project".to_owned()),
791 description: Some("Description".to_owned()),
792 marketing: Some(Addr::unchecked("marketing")),
793 logo: Some(LogoInfo::Embedded),
794 }
795 );
796
797 let res: DownloadLogoResponse = query_download_logo(deps.as_ref()).unwrap();
798 assert_eq! {
799 res,
800 DownloadLogoResponse{
801 data: img.into(),
802 mime_type: "image/svg+xml".to_owned(),
803 }
804 }
805 }
806
807 #[test]
808 fn png() {
809 let mut deps = mock_dependencies();
810 const PNG_HEADER: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
811 let instantiate_msg = InstantiateMsg {
812 name: "Cash Token".to_string(),
813 symbol: "CASH".to_string(),
814 decimals: 9,
815 initial_balances: vec![],
816 mint: None,
817 marketing: Some(InstantiateMarketingInfo {
818 project: Some("Project".to_owned()),
819 description: Some("Description".to_owned()),
820 marketing: Some("marketing".to_owned()),
821 logo: Some(Logo::Embedded(EmbeddedLogo::Png(PNG_HEADER.into()))),
822 }),
823 };
824
825 let info = mock_info("creator", &[]);
826 let env = mock_env();
827 let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap();
828 assert_eq!(0, res.messages.len());
829
830 assert_eq!(
831 query_marketing_info(deps.as_ref()).unwrap(),
832 MarketingInfoResponse {
833 project: Some("Project".to_owned()),
834 description: Some("Description".to_owned()),
835 marketing: Some(Addr::unchecked("marketing")),
836 logo: Some(LogoInfo::Embedded),
837 }
838 );
839
840 let res: DownloadLogoResponse = query_download_logo(deps.as_ref()).unwrap();
841 assert_eq! {
842 res,
843 DownloadLogoResponse{
844 data: PNG_HEADER.into(),
845 mime_type: "image/png".to_owned(),
846 }
847 }
848 }
849 }
850}