Skip to content
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

Merged
merged 20 commits into from
Jun 20, 2024

Conversation

taitruong
Copy link
Collaborator

@taitruong taitruong commented May 31, 2024

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:

            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)

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:

            Ok(CLASS_ID_AND_NFT_CONTRACT_INFO
                .load(deps.storage, &local_class_id)?
                .address)

Comment on lines 229 to 233
// 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)
Copy link
Collaborator Author

@taitruong taitruong May 31, 2024

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)

Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Comment on lines +130 to +134
#[returns(::cosmwasm_std::Addr)]
GetInstantiate2NftContract {
class_id: String,
cw721_code_id: Option<u64>,
},
Copy link
Collaborator Author

@taitruong taitruong May 31, 2024

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!

Comment on lines +532 to +539
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);
Copy link
Collaborator Author

@taitruong taitruong May 31, 2024

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):

  1. event when NFTs are created/minted...

Comment on lines +643 to +647
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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. event when NFTs are unescrowed/returned...

Comment on lines 165 to 169
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());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. event when NFTs are burned

@taitruong taitruong changed the title Class id fix fix: query NFT contract from storage (and not instantiate2) May 31, 2024
@taitruong taitruong changed the title fix: query NFT contract from storage (and not instantiate2) receive callback fix: query NFT contract from storage (and not instantiate2) May 31, 2024
Copy link
Collaborator

@Art3miX Art3miX left a 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
Copy link
Collaborator

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.

@@ -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(
Copy link
Collaborator

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.

Comment on lines 234 to 239
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 {
Copy link
Collaborator

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?

@shanev shanev added this pull request to the merge queue Jun 20, 2024
Merged via the queue into public-awesome:main with commit 8e8de23 Jun 20, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants