-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
receive callback fix: query NFT contract from storage (and not instantiate2) #98
Conversation
// here we know NFT contract is always instantiated, so it is safe getting it from storage | ||
// NEVER use instantiate2 to get the NFT contract address here, since code id may change! | ||
Ok(CLASS_ID_AND_NFT_CONTRACT_INFO | ||
.load(deps.storage, &local_class_id)? | ||
.address) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the bug fix. Before it used instantiate2
for retrieving nft contract, but in case cw721 code id has changed, it returns an non-existent, unknown address.
Code before:
let cw721_code_id = CW721_CODE_ID.load(deps.storage)?;
// for creating a predictable nft contract using, using instantiate2, we need: checksum, creator, and salt:
// - using class id as salt for instantiating nft contract guarantees a) predictable address and b) uniqueness
// for this salt must be of length 32 bytes, so we use sha256 to hash class id
let mut hasher = Sha256::new();
hasher.update(local_class_id.as_bytes());
let salt = hasher.finalize().to_vec();
get_instantiate2_address(deps, env.contract.address.as_str(), &salt, cw721_code_id)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did we use salt to get address before? How to guarantee contract will always exist on retrieval?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Callback happens after collection has been instantiated. ICS721 instantiates it and stores it in CLASS_ID_AND_NFT_CONTRACT_INFO
.
salt is required and part of instantiate2
. Using class id as salt guarantees that address always results in same escrowed nft contract (predictable). This is used durint instantiation of contract, which then stores predictable address in CLASS_ID_AND_NFT_CONTRACT_INFO
. CLASS_ID_AND_NFT_CONTRACT_INFO
is then used by mint and callbacks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah so for instantiate2 we need the salt, but here we are now using local_class_id
to retrieve from CLASS_ID_AND_NFT_CONTRACT_INFO
. Just wondering where do we get local_class_Id
from? Seems to be passed into the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class_id is passed from source to target chain (e.g. nft contract stars1xyz
) as part pf , then target chain attaches destination port and channel e.g. wasm.osmo1ics721/channel-xyz/stars1xyz
). This is stored as key and nft contract as value in CLASS_ID_AND_NFT_CONTRACT_INFO
map.
#[returns(::cosmwasm_std::Addr)] | ||
GetInstantiate2NftContract { | ||
class_id: String, | ||
cw721_code_id: Option<u64>, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added new GetInstantiate2NftContract
, which will be super helpful - e.g. for WLing collections without doing an initial transfer!
let (class_id_info, instantiate) = | ||
self.create_instantiate_msg(deps, &env, class.clone())?; | ||
|
||
let token_ids = format!("{:?}", tokens); | ||
let event = Event::new("ics721_receive_create_vouchers") | ||
.add_attribute("class_id", class_id_info.class_id) | ||
.add_attribute("nft_contract", class_id_info.address) | ||
.add_attribute("token_ids", token_ids); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 3 new events, for better processing (e.g. for constellations):
- event when NFTs are created/minted...
let token_ids_string = format!("{:?}", token_ids); | ||
let event = Event::new("ics721_receive_redeem_vouchers") | ||
.add_attribute("class_id", class.id) | ||
.add_attribute("nft_contract", nft_contract.clone()) | ||
.add_attribute("token_ids", token_ids_string); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- event when NFTs are unescrowed/returned...
packages/ics721/src/ibc.rs
Outdated
let token_ids = format!("{:?}", msg.token_ids); | ||
let event = Event::new("ics721_ack_burn_vouchers") | ||
.add_attribute("class_id", msg.class_id.to_string()) | ||
.add_attribute("nft_contract", nft_contract.clone()) | ||
.add_attribute("token_ids", token_ids.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- event when NFTs are burned
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just 2 mid-ish issues.
let salt = hasher.finalize().to_vec(); | ||
|
||
get_instantiate2_address(deps, env.contract.address.as_str(), &salt, cw721_code_id) | ||
// here we know NFT contract is always instantiated, so it is safe getting it from storage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we?
Because CLASS_ID_AND_NFT_CONTRACT_INFO
storage is only updated on the create_voucher_and_channel_messages
, which is executed AFTER create_callback_msg
, meaning when we try to load from storage here, its still might be missing which should panic, on first transferred NFT.
We should check here if we have it in storage, get it, if we don't, get the init2 of it, which works because if we don't have it storage, it means we are creating it right now, which means we use the correct code id for it.
packages/ics721/src/query.rs
Outdated
@@ -88,6 +96,28 @@ pub fn query_nft_contract_for_class_id( | |||
.map(|e| e.map(|(_, v)| v.address)) | |||
} | |||
|
|||
pub fn query_get_nft_contract_for_class_id( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would change the name to reflect that it uses init2 (which might not be the actual address if wrong info is given).
Also why we even need it? we have the storage, if the contract doesn't exists in storage, means we didn't init it yet, and we can use init2 with the saved code in storage.
let nft_contract = | ||
match query_nft_contract_for_class_id(deps.storage, local_class_id.clone()) { | ||
Ok(nft_contract) => nft_contract, | ||
Err(_) => None, // not found, occurs on initial transfer when we don't have the contract address | ||
}; | ||
match nft_contract { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
match query_nft_contract_for_class_id(deps.storage, local_class_id.clone()).ok_or(None)
, can this work?
UPDATE: most code changes was done in e2e tests. Huge shoutout to @magiodev!
code for fix is quite small. We felt the need to refactor e2e tests and add more and deeper tests using Go.
For receive callback, nft contract needs to be passed. Before this fix, it was using instantiated2 for retrieving nft contract. As part of instantiate2 it uses cw721's code id for predicting nft contract address:
But if cw721 code id changes, then it returns wrong address. In this case whole TX fails and NFT gets reverted/returned to sender.
Fix is getting address from
CLASS_ID_AND_NFT_CONTRACT_INFO
storage: