terra/contracts: columbus-5 migration for cw20-wrapped

Change-Id: I32a703cdeb60dcf288907df6318ae504171fc5f0
This commit is contained in:
Reisen 2021-09-06 08:27:27 +00:00 committed by David Paryente
parent ce6d92bb2b
commit 3cbbc6f2b8
5 changed files with 164 additions and 149 deletions

View File

@ -18,6 +18,7 @@ cosmwasm-std = { version = "0.16.0" }
cosmwasm-storage = { version = "0.16.0" } cosmwasm-storage = { version = "0.16.0" }
schemars = "0.8.1" schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] } serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw2 = { version = "0.8.0" }
cw20 = { version = "0.8.0" } cw20 = { version = "0.8.0" }
cw20-legacy = { version = "0.2.0", features = ["library"]} cw20-legacy = { version = "0.2.0", features = ["library"]}
cw-storage-plus = { version = "0.8.0" } cw-storage-plus = { version = "0.8.0" }

View File

@ -1,48 +1,47 @@
use cosmwasm_std::{ use cosmwasm_std::{
entry_point,
to_binary, to_binary,
Api,
Binary, Binary,
CosmosMsg, CosmosMsg,
Deps,
DepsMut,
Env, Env,
Extern, MessageInfo,
HandleResponse, Response,
HumanAddr,
InitResponse,
Querier,
StdError, StdError,
StdResult, StdResult,
Storage,
Uint128, Uint128,
WasmMsg, WasmMsg,
}; };
use cw20_base::{ use cw2::set_contract_version;
use cw20_legacy::{
allowances::{ allowances::{
handle_burn_from, execute_burn_from,
handle_decrease_allowance, execute_decrease_allowance,
handle_increase_allowance, execute_increase_allowance,
handle_send_from, execute_send_from,
handle_transfer_from, execute_transfer_from,
query_allowance, query_allowance,
}, },
contract::{ contract::{
handle_mint, execute_mint,
handle_send, execute_send,
handle_transfer, execute_transfer,
query_balance, query_balance,
}, },
state::{ state::{
token_info,
token_info_read,
MinterData, MinterData,
TokenInfo, TokenInfo,
TOKEN_INFO,
}, },
ContractError,
}; };
use crate::{ use crate::{
msg::{ msg::{
HandleMsg, ExecuteMsg,
InitMsg, InstantiateMsg,
QueryMsg, QueryMsg,
WrappedAssetInfoResponse, WrappedAssetInfoResponse,
}, },
@ -55,116 +54,136 @@ use crate::{
use cw20::TokenInfoResponse; use cw20::TokenInfoResponse;
use std::string::String; use std::string::String;
pub fn init<S: Storage, A: Api, Q: Querier>( type HumanAddr = String;
deps: &mut Extern<S, A, Q>,
// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw20-base";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env, env: Env,
msg: InitMsg, info: MessageInfo,
) -> StdResult<InitResponse> { msg: InstantiateMsg,
) -> StdResult<Response> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// store token info using cw20-base format // store token info using cw20-base format
let data = TokenInfo { let data = TokenInfo {
name: msg.name, name: msg.name,
symbol: msg.symbol, symbol: msg.symbol,
decimals: msg.decimals, decimals: msg.decimals,
total_supply: Uint128(0), total_supply: Uint128::new(0),
// set creator as minter // set creator as minter
mint: Some(MinterData { mint: Some(MinterData {
minter: deps.api.canonical_address(&env.message.sender)?, minter: deps.api.addr_canonicalize(&info.sender.as_str())?,
cap: None, cap: None,
}), }),
}; };
token_info(&mut deps.storage).save(&data)?; TOKEN_INFO.save(deps.storage, &data)?;
// save wrapped asset info // save wrapped asset info
let data = WrappedAssetInfo { let data = WrappedAssetInfo {
asset_chain: msg.asset_chain, asset_chain: msg.asset_chain,
asset_address: msg.asset_address, asset_address: msg.asset_address,
bridge: deps.api.canonical_address(&env.message.sender)?, bridge: deps.api.addr_canonicalize(&info.sender.as_str())?,
}; };
wrapped_asset_info(&mut deps.storage).save(&data)?; wrapped_asset_info(deps.storage).save(&data)?;
if let Some(mint_info) = msg.mint { if let Some(mint_info) = msg.mint {
handle_mint(deps, env, mint_info.recipient, mint_info.amount)?; execute_mint(deps, env, info, mint_info.recipient, mint_info.amount)
.map_err(|e| StdError::generic_err(format!("{}", e)))?;
} }
if let Some(hook) = msg.init_hook { if let Some(hook) = msg.init_hook {
Ok(InitResponse { Ok(
messages: vec![CosmosMsg::Wasm(WasmMsg::Execute { Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: hook.contract_addr, contract_addr: hook.contract_addr,
msg: hook.msg, msg: hook.msg,
send: vec![], funds: vec![],
})], })),
log: vec![], )
})
} else { } else {
Ok(InitResponse::default()) Ok(Response::default())
} }
} }
pub fn handle<S: Storage, A: Api, Q: Querier>( #[cfg_attr(not(feature = "library"), entry_point)]
deps: &mut Extern<S, A, Q>, pub fn execute(
deps: DepsMut,
env: Env, env: Env,
msg: HandleMsg, info: MessageInfo,
) -> StdResult<HandleResponse> { msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg { match msg {
// these all come from cw20-base to implement the cw20 standard // these all come from cw20-base to implement the cw20 standard
HandleMsg::Transfer { recipient, amount } => { ExecuteMsg::Transfer { recipient, amount } => {
Ok(handle_transfer(deps, env, recipient, amount)?) Ok(execute_transfer(deps, env, info, recipient, amount)?)
} }
HandleMsg::Burn { account, amount } => Ok(handle_burn_from(deps, env, account, amount)?), ExecuteMsg::Burn { account, amount } => {
HandleMsg::Send { Ok(execute_burn_from(deps, env, info, account, amount)?)
}
ExecuteMsg::Send {
contract, contract,
amount, amount,
msg, msg,
} => Ok(handle_send(deps, env, contract, amount, msg)?), } => Ok(execute_send(deps, env, info, contract, amount, msg)?),
HandleMsg::Mint { recipient, amount } => handle_mint_wrapped(deps, env, recipient, amount), ExecuteMsg::Mint { recipient, amount } => {
HandleMsg::IncreaseAllowance { execute_mint_wrapped(deps, env, info, recipient, amount)
}
ExecuteMsg::IncreaseAllowance {
spender, spender,
amount, amount,
expires, expires,
} => Ok(handle_increase_allowance( } => Ok(execute_increase_allowance(
deps, env, spender, amount, expires, deps, env, info, spender, amount, expires,
)?), )?),
HandleMsg::DecreaseAllowance { ExecuteMsg::DecreaseAllowance {
spender, spender,
amount, amount,
expires, expires,
} => Ok(handle_decrease_allowance( } => Ok(execute_decrease_allowance(
deps, env, spender, amount, expires, deps, env, info, spender, amount, expires,
)?), )?),
HandleMsg::TransferFrom { ExecuteMsg::TransferFrom {
owner, owner,
recipient, recipient,
amount, amount,
} => Ok(handle_transfer_from(deps, env, owner, recipient, amount)?), } => Ok(execute_transfer_from(
HandleMsg::BurnFrom { owner, amount } => Ok(handle_burn_from(deps, env, owner, amount)?), deps, env, info, owner, recipient, amount,
HandleMsg::SendFrom { )?),
ExecuteMsg::BurnFrom { owner, amount } => {
Ok(execute_burn_from(deps, env, info, owner, amount)?)
}
ExecuteMsg::SendFrom {
owner, owner,
contract, contract,
amount, amount,
msg, msg,
} => Ok(handle_send_from(deps, env, owner, contract, amount, msg)?), } => Ok(execute_send_from(
deps, env, info, owner, contract, amount, msg,
)?),
} }
} }
fn handle_mint_wrapped<S: Storage, A: Api, Q: Querier>( fn execute_mint_wrapped(
deps: &mut Extern<S, A, Q>, deps: DepsMut,
env: Env, env: Env,
info: MessageInfo,
recipient: HumanAddr, recipient: HumanAddr,
amount: Uint128, amount: Uint128,
) -> StdResult<HandleResponse> { ) -> Result<Response, ContractError> {
// Only bridge can mint // Only bridge can mint
let wrapped_info = wrapped_asset_info_read(&deps.storage).load()?; let wrapped_info = wrapped_asset_info_read(deps.storage).load()?;
if wrapped_info.bridge != deps.api.canonical_address(&env.message.sender)? { if wrapped_info.bridge != deps.api.addr_canonicalize(&info.sender.as_str())? {
return Err(StdError::unauthorized()); return Err(ContractError::Unauthorized {});
} }
Ok(handle_mint(deps, env, recipient, amount)?) Ok(execute_mint(deps, env, info, recipient, amount)?)
} }
pub fn query<S: Storage, A: Api, Q: Querier>( pub fn query(deps: Deps, msg: QueryMsg) -> StdResult<Binary> {
deps: &Extern<S, A, Q>,
msg: QueryMsg,
) -> StdResult<Binary> {
match msg { match msg {
QueryMsg::WrappedAssetInfo {} => to_binary(&query_wrapped_asset_info(deps)?), QueryMsg::WrappedAssetInfo {} => to_binary(&query_wrapped_asset_info(deps)?),
// inherited from cw20-base // inherited from cw20-base
@ -176,66 +195,58 @@ pub fn query<S: Storage, A: Api, Q: Querier>(
} }
} }
pub fn query_token_info<S: Storage, A: Api, Q: Querier>( pub fn query_token_info(deps: Deps) -> StdResult<TokenInfoResponse> {
deps: &Extern<S, A, Q>, let info = TOKEN_INFO.load(deps.storage)?;
) -> StdResult<TokenInfoResponse> { Ok(TokenInfoResponse {
let info = token_info_read(&deps.storage).load()?;
let res = TokenInfoResponse {
name: String::from("Wormhole:") + info.name.as_str(), name: String::from("Wormhole:") + info.name.as_str(),
symbol: String::from("wh") + info.symbol.as_str(), symbol: String::from("wh") + info.symbol.as_str(),
decimals: info.decimals, decimals: info.decimals,
total_supply: info.total_supply, total_supply: info.total_supply,
}; })
Ok(res)
} }
pub fn query_wrapped_asset_info<S: Storage, A: Api, Q: Querier>( pub fn query_wrapped_asset_info(deps: Deps) -> StdResult<WrappedAssetInfoResponse> {
deps: &Extern<S, A, Q>, let info = wrapped_asset_info_read(deps.storage).load()?;
) -> StdResult<WrappedAssetInfoResponse> { Ok(WrappedAssetInfoResponse {
let info = wrapped_asset_info_read(&deps.storage).load()?;
let res = WrappedAssetInfoResponse {
asset_chain: info.asset_chain, asset_chain: info.asset_chain,
asset_address: info.asset_address, asset_address: info.asset_address,
bridge: deps.api.human_address(&info.bridge)?, bridge: deps.api.addr_humanize(&info.bridge)?,
}; })
Ok(res)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use cosmwasm_std::{ use cosmwasm_std::testing::{
testing::{ mock_dependencies,
mock_dependencies, mock_env,
mock_env, mock_info,
},
HumanAddr,
}; };
use cw20::TokenInfoResponse; use cw20::TokenInfoResponse;
const CANONICAL_LENGTH: usize = 20; const CANONICAL_LENGTH: usize = 20;
fn get_balance<S: Storage, A: Api, Q: Querier, T: Into<HumanAddr>>( fn get_balance(deps: Deps, address: HumanAddr) -> Uint128 {
deps: &Extern<S, A, Q>, query_balance(deps, address.into()).unwrap().balance
address: T,
) -> Uint128 {
query_balance(&deps, address.into()).unwrap().balance
} }
fn do_init<S: Storage, A: Api, Q: Querier>(deps: &mut Extern<S, A, Q>, creator: &HumanAddr) { fn do_init(mut deps: DepsMut, creator: &HumanAddr) {
let init_msg = InitMsg { let init_msg = InstantiateMsg {
name: "Integers".to_string(),
symbol: "INT".to_string(),
asset_chain: 1, asset_chain: 1,
asset_address: vec![1; 32].into(), asset_address: vec![1; 32].into(),
decimals: 10, decimals: 10,
mint: None, mint: None,
init_hook: None, init_hook: None,
}; };
let env = mock_env(creator, &[]); let env = mock_env();
let res = init(deps, env, init_msg).unwrap(); let info = mock_info(creator, &[]);
let res = instantiate(deps, env, info, init_msg).unwrap();
assert_eq!(0, res.messages.len()); assert_eq!(0, res.messages.len());
assert_eq!( assert_eq!(
query_token_info(&deps).unwrap(), query_token_info(deps.as_ref()).unwrap(),
TokenInfoResponse { TokenInfoResponse {
name: "Wormhole Wrapped".to_string(), name: "Wormhole Wrapped".to_string(),
symbol: "WWT".to_string(), symbol: "WWT".to_string(),
@ -245,35 +256,36 @@ mod tests {
); );
assert_eq!( assert_eq!(
query_wrapped_asset_info(&deps).unwrap(), query_wrapped_asset_info(deps.as_ref()).unwrap(),
WrappedAssetInfoResponse { WrappedAssetInfoResponse {
asset_chain: 1, asset_chain: 1,
asset_address: vec![1; 32].into(), asset_address: vec![1; 32].into(),
bridge: creator.clone(), bridge: deps.api.addr_validate(creator).unwrap(),
} }
); );
} }
fn do_init_and_mint<S: Storage, A: Api, Q: Querier>( fn do_init_and_mint(
deps: &mut Extern<S, A, Q>, mut deps: DepsMut,
creator: &HumanAddr, creator: &HumanAddr,
mint_to: &HumanAddr, mint_to: &HumanAddr,
amount: Uint128, amount: Uint128,
) { ) {
do_init(deps, creator); do_init(deps, creator);
let msg = HandleMsg::Mint { let msg = ExecuteMsg::Mint {
recipient: mint_to.clone(), recipient: mint_to.clone(),
amount, amount,
}; };
let env = mock_env(&creator, &[]); let env = mock_env();
let res = handle(deps, env, msg.clone()).unwrap(); let info = mock_info(creator, &[]);
let res = execute(deps.as_mut(), env, info, msg.clone()).unwrap();
assert_eq!(0, res.messages.len()); assert_eq!(0, res.messages.len());
assert_eq!(get_balance(deps, mint_to), amount); assert_eq!(get_balance(deps.as_ref(), mint_to.clone(),), amount);
assert_eq!( assert_eq!(
query_token_info(&deps).unwrap(), query_token_info(deps.as_ref()).unwrap(),
TokenInfoResponse { TokenInfoResponse {
name: "Wormhole Wrapped".to_string(), name: "Wormhole Wrapped".to_string(),
symbol: "WWT".to_string(), symbol: "WWT".to_string(),
@ -285,29 +297,30 @@ mod tests {
#[test] #[test]
fn can_mint_by_minter() { fn can_mint_by_minter() {
let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]); let mut deps = mock_dependencies(&[]);
let minter = HumanAddr::from("minter"); let minter = HumanAddr::from("minter");
let recipient = HumanAddr::from("recipient"); let recipient = HumanAddr::from("recipient");
let amount = Uint128(222_222_222); let amount = Uint128::new(222_222_222);
do_init_and_mint(&mut deps, &minter, &recipient, amount); do_init_and_mint(deps.as_mut(), &minter, &recipient, amount);
} }
#[test] #[test]
fn others_cannot_mint() { fn others_cannot_mint() {
let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]); let mut deps = mock_dependencies(&[]);
let minter = HumanAddr::from("minter"); let minter = HumanAddr::from("minter");
let recipient = HumanAddr::from("recipient"); let recipient = HumanAddr::from("recipient");
do_init(&mut deps, &minter); do_init(deps.as_mut(), &minter);
let amount = Uint128(222_222_222); let amount = Uint128::new(222_222_222);
let msg = HandleMsg::Mint { let msg = ExecuteMsg::Mint {
recipient: recipient.clone(), recipient: recipient.clone(),
amount, amount,
}; };
let other_address = HumanAddr::from("other"); let other_address = HumanAddr::from("other");
let env = mock_env(&other_address, &[]); let env = mock_env();
let res = handle(&mut deps, env, msg); let info = mock_info(&other_address, &[]);
let res = execute(deps.as_mut(), env, info, msg);
assert_eq!( assert_eq!(
format!("{}", res.unwrap_err()), format!("{}", res.unwrap_err()),
format!("{}", crate::error::ContractError::Unauthorized {}) format!("{}", crate::error::ContractError::Unauthorized {})
@ -316,44 +329,46 @@ mod tests {
#[test] #[test]
fn transfer_balance_success() { fn transfer_balance_success() {
let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]); let mut deps = mock_dependencies(&[]);
let minter = HumanAddr::from("minter"); let minter = HumanAddr::from("minter");
let owner = HumanAddr::from("owner"); let owner = HumanAddr::from("owner");
let amount_initial = Uint128(222_222_222); let amount_initial = Uint128::new(222_222_222);
do_init_and_mint(&mut deps, &minter, &owner, amount_initial); do_init_and_mint(deps.as_mut(), &minter, &owner, amount_initial);
// Transfer // Transfer
let recipient = HumanAddr::from("recipient"); let recipient = HumanAddr::from("recipient");
let amount_transfer = Uint128(222_222); let amount_transfer = Uint128::new(222_222);
let msg = HandleMsg::Transfer { let msg = ExecuteMsg::Transfer {
recipient: recipient.clone(), recipient: recipient.clone(),
amount: amount_transfer, amount: amount_transfer,
}; };
let env = mock_env(&owner, &[]); let env = mock_env();
let res = handle(&mut deps, env, msg.clone()).unwrap(); let info = mock_info(&owner, &[]);
let res = execute(deps.as_mut(), env, info, msg.clone()).unwrap();
assert_eq!(0, res.messages.len()); assert_eq!(0, res.messages.len());
assert_eq!(get_balance(&deps, owner), Uint128(222_000_000)); assert_eq!(get_balance(deps.as_ref(), owner), Uint128::new(222_000_000));
assert_eq!(get_balance(&deps, recipient), amount_transfer); assert_eq!(get_balance(deps.as_ref(), recipient), amount_transfer);
} }
#[test] #[test]
fn transfer_balance_not_enough() { fn transfer_balance_not_enough() {
let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]); let mut deps = mock_dependencies(&[]);
let minter = HumanAddr::from("minter"); let minter = HumanAddr::from("minter");
let owner = HumanAddr::from("owner"); let owner = HumanAddr::from("owner");
let amount_initial = Uint128(222_221); let amount_initial = Uint128::new(222_221);
do_init_and_mint(&mut deps, &minter, &owner, amount_initial); do_init_and_mint(deps.as_mut(), &minter, &owner, amount_initial);
// Transfer // Transfer
let recipient = HumanAddr::from("recipient"); let recipient = HumanAddr::from("recipient");
let amount_transfer = Uint128(222_222); let amount_transfer = Uint128::new(222_222);
let msg = HandleMsg::Transfer { let msg = ExecuteMsg::Transfer {
recipient: recipient.clone(), recipient: recipient.clone(),
amount: amount_transfer, amount: amount_transfer,
}; };
let env = mock_env(&owner, &[]); let env = mock_env();
let _ = handle(&mut deps, env, msg.clone()).unwrap_err(); // Will panic if no error let info = mock_info(&owner, &[]);
let _ = execute(deps.as_mut(), env, info, msg.clone()).unwrap_err(); // Will panic if no error
} }
} }

View File

@ -1,9 +1,7 @@
pub mod contract;
mod error; mod error;
pub mod contract;
pub mod msg; pub mod msg;
pub mod state; pub mod state;
pub use crate::error::ContractError; pub use crate::error::ContractError;
#[cfg(all(target_arch = "wasm32", not(feature = "library")))]
cosmwasm_std::create_entry_points!(contract);

View File

@ -6,14 +6,16 @@ use serde::{
}; };
use cosmwasm_std::{ use cosmwasm_std::{
Addr,
Binary, Binary,
HumanAddr,
Uint128, Uint128,
}; };
use cw20::Expiration; use cw20::Expiration;
type HumanAddr = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InitMsg { pub struct InstantiateMsg {
pub name: String, pub name: String,
pub symbol: String, pub symbol: String,
pub asset_chain: u16, pub asset_chain: u16,
@ -37,7 +39,7 @@ pub struct InitMint {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum HandleMsg { pub enum ExecuteMsg {
/// Implements CW20. Transfer is a base message to move tokens to another account without triggering actions /// Implements CW20. Transfer is a base message to move tokens to another account without triggering actions
Transfer { Transfer {
recipient: HumanAddr, recipient: HumanAddr,
@ -50,7 +52,7 @@ pub enum HandleMsg {
Send { Send {
contract: HumanAddr, contract: HumanAddr,
amount: Uint128, amount: Uint128,
msg: Option<Binary>, msg: Binary,
}, },
/// Implements CW20 "mintable" extension. If authorized, creates amount new tokens /// Implements CW20 "mintable" extension. If authorized, creates amount new tokens
/// and adds to the recipient balance. /// and adds to the recipient balance.
@ -87,7 +89,7 @@ pub enum HandleMsg {
owner: HumanAddr, owner: HumanAddr,
contract: HumanAddr, contract: HumanAddr,
amount: Uint128, amount: Uint128,
msg: Option<Binary>, msg: Binary,
}, },
/// Implements CW20 "approval" extension. Destroys tokens forever /// Implements CW20 "approval" extension. Destroys tokens forever
BurnFrom { owner: HumanAddr, amount: Uint128 }, BurnFrom { owner: HumanAddr, amount: Uint128 },
@ -116,5 +118,5 @@ pub enum QueryMsg {
pub struct WrappedAssetInfoResponse { pub struct WrappedAssetInfoResponse {
pub asset_chain: u16, // Asset chain id pub asset_chain: u16, // Asset chain id
pub asset_address: Binary, // Asset smart contract address in the original chain pub asset_address: Binary, // Asset smart contract address in the original chain
pub bridge: HumanAddr, // Bridge address, authorized to mint and burn wrapped tokens pub bridge: Addr, // Bridge address, authorized to mint and burn wrapped tokens
} }

View File

@ -7,7 +7,6 @@ use serde::{
use cosmwasm_std::{ use cosmwasm_std::{
Binary, Binary,
CanonicalAddr, CanonicalAddr,
ReadonlyStorage,
Storage, Storage,
}; };
use cosmwasm_storage::{ use cosmwasm_storage::{
@ -27,12 +26,12 @@ pub struct WrappedAssetInfo {
pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens
} }
pub fn wrapped_asset_info<S: Storage>(storage: &mut S) -> Singleton<S, WrappedAssetInfo> { pub fn wrapped_asset_info(storage: &mut dyn Storage) -> Singleton<WrappedAssetInfo> {
singleton(storage, KEY_WRAPPED_ASSET) singleton(storage, KEY_WRAPPED_ASSET)
} }
pub fn wrapped_asset_info_read<S: ReadonlyStorage>( pub fn wrapped_asset_info_read(
storage: &S, storage: &dyn Storage,
) -> ReadonlySingleton<S, WrappedAssetInfo> { ) -> ReadonlySingleton<WrappedAssetInfo> {
singleton_read(storage, KEY_WRAPPED_ASSET) singleton_read(storage, KEY_WRAPPED_ASSET)
} }