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

Final audit revision #90

Merged
merged 11 commits into from
Feb 19, 2024
Merged

Conversation

taitruong
Copy link
Collaborator

@taitruong taitruong commented Feb 12, 2024

Changes:

  • pass nft contract, instead of overriding info.sender (commit #5059fc0e6831a6005fb7dcc0ee80295f46904984)
  • remove NFT_CONTRACT_TO_CLASS_ID and CLASS_ID_TO_NFT_CONTRACT and merge into CLASS_ID_AND_NFT_CONTRACT_INFO (commit #d6d63835ec03da75e8a45a36cf4c0401137f1492)

Comment on lines 764 to 802
// we migrate only in case CLASS_ID_AND_NFT_CONTRACT_INFO is not populated yet
// TODO once migrated:
// - this complete block can be deleted
// - legacy map 'e' and 'f' can be deleted
let is_empty = query_nft_contracts(deps.as_ref(), None, None)
.map(|nft_contracts| nft_contracts.is_empty())?;
if is_empty {
// - get legacy map and migrate it to new indexed map
let legacy_nft_contract_to_class_id: Map<Addr, ClassId> = Map::new("f");
match cw_paginate_storage::paginate_map(
deps.as_ref(),
&legacy_nft_contract_to_class_id,
None,
None,
Order::Ascending,
) {
Ok(nft_contract_and_class_id) => {
let response = response.add_attribute(
"migrated nft contracts",
nft_contract_and_class_id.len().to_string(),
);
for (nft_contract, class_id) in nft_contract_and_class_id {
let class_id_info = ClassIdInfo {
class_id: class_id.clone(),
address: nft_contract.clone(),
};
CLASS_ID_AND_NFT_CONTRACT_INFO.save(
deps.storage,
&class_id,
&class_id_info,
)?;
}
Ok(response)
}
Err(err) => Err(ContractError::Std(err)),
}
} else {
Ok(response)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the migration logic for CLASS_ID_AND_NFT_CONTRACT_INFO

Comment on lines +571 to +660
fn test_migrate() {
let mut deps = mock_dependencies();
let env = mock_env();
let info = mock_info(OWNER_ADDR, &[]);
let msg = instantiate_msg(None, None);
Ics721Contract {}
.instantiate(deps.as_mut(), env.clone(), info, msg.clone())
.unwrap();
let msg = MigrateMsg::WithUpdate {
pauser: Some("some_other_pauser".to_string()),
outgoing_proxy: Some("outgoing".to_string()),
incoming_proxy: Some("incoming".to_string()),
cw721_base_code_id: Some(1),
cw721_admin: Some("some_other_admin".to_string()),
};

// before migrate, populate legacy
let class_id_to_nft_contract: Map<ClassId, Addr> = Map::new("e");
class_id_to_nft_contract
.save(
deps.as_mut().storage,
ClassId::new(CLASS_ID_1),
&Addr::unchecked(NFT_CONTRACT_1),
)
.unwrap();
class_id_to_nft_contract
.save(
deps.as_mut().storage,
ClassId::new(CLASS_ID_2),
&Addr::unchecked(NFT_CONTRACT_2),
)
.unwrap();
let nft_contract_to_class_id: Map<Addr, ClassId> = Map::new("f");
nft_contract_to_class_id
.save(
deps.as_mut().storage,
Addr::unchecked(NFT_CONTRACT_1),
&ClassId::new(CLASS_ID_1),
)
.unwrap();
nft_contract_to_class_id
.save(
deps.as_mut().storage,
Addr::unchecked(NFT_CONTRACT_2),
&ClassId::new(CLASS_ID_2),
)
.unwrap();

// migrate
Ics721Contract {}
.migrate(deps.as_mut(), env.clone(), msg)
.unwrap();

assert_eq!(
PO.pauser.load(&deps.storage).unwrap(),
Some(Addr::unchecked("some_other_pauser"))
);
assert_eq!(
OUTGOING_PROXY.load(&deps.storage).unwrap(),
Some(Addr::unchecked("outgoing"))
);
assert_eq!(
INCOMING_PROXY.load(&deps.storage).unwrap(),
Some(Addr::unchecked("incoming"))
);
assert_eq!(CW721_CODE_ID.load(&deps.storage).unwrap(), 1);
assert_eq!(
ADMIN_USED_FOR_CW721.load(&deps.storage).unwrap(),
Some(Addr::unchecked("some_other_admin"))
);
let nft_contract_and_class_id_list = query_nft_contracts(deps.as_ref(), None, None).unwrap();
assert_eq!(nft_contract_and_class_id_list.len(), 2);
assert_eq!(nft_contract_and_class_id_list[0].0, CLASS_ID_1);
assert_eq!(nft_contract_and_class_id_list[0].1, NFT_CONTRACT_1);
assert_eq!(nft_contract_and_class_id_list[1].0, CLASS_ID_2);
assert_eq!(nft_contract_and_class_id_list[1].1, NFT_CONTRACT_2);
// test query and indexers for class id and addr are working
let nft_contract_1 =
query_nft_contract_for_class_id(&deps.storage, CLASS_ID_1.to_string()).unwrap();
assert_eq!(nft_contract_1, Some(Addr::unchecked(NFT_CONTRACT_1)));
let nft_contract_2 =
query_nft_contract_for_class_id(&deps.storage, CLASS_ID_2.to_string()).unwrap();
assert_eq!(nft_contract_2, Some(Addr::unchecked(NFT_CONTRACT_2)));
let class_id_1 =
query_class_id_for_nft_contract(deps.as_ref(), NFT_CONTRACT_1.to_string()).unwrap();
assert_eq!(class_id_1, Some(ClassId::new(CLASS_ID_1)));
let class_id_2 =
query_class_id_for_nft_contract(deps.as_ref(), NFT_CONTRACT_2.to_string()).unwrap();
assert_eq!(class_id_2, Some(ClassId::new(CLASS_ID_2)));
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

test migration by populating new CLASS_ID_AND_NFT_CONTRACT_INFO with legacy data from NFT_CONTRACT_TO_CLASS_ID. Note: CLASS_ID_TO_NFT_CONTRACT is not used, since it is redundant to NFT_CONTRACT_TO_CLASS_ID

Comment on lines -22 to -24
pub const CLASS_ID_TO_NFT_CONTRACT: Map<ClassId, Addr> = Map::new("e");
/// Maps cw721 contracts to the classID they were instantiated for.
pub const NFT_CONTRACT_TO_CLASS_ID: Map<Addr, ClassId> = Map::new("f");
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

removing redundant states...

Comment on lines +23 to +30
pub const CLASS_ID_AND_NFT_CONTRACT_INFO: IndexedMap<&str, ClassIdInfo, ClassIdInfoIndexes> =
IndexedMap::new(
"m",
ClassIdInfoIndexes {
class_id: UniqueIndex::new(|d| d.class_id.clone(), "class_id_info__class_id"),
address: UniqueIndex::new(|d| d.address.clone(), "class_id_info__address"),
},
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

... and replaced by new state using IndexedMap

Copy link
Contributor

@humanalgorithm humanalgorithm Feb 15, 2024

Choose a reason for hiding this comment

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

Why indexed map instead of the other two maps? What is benefit of putting it into new IndexedMap?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See answer below

Comment on lines 350 to 357
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
nft_contract: &Addr,
token_id: TokenId,
nft_owner: String,
msg: Binary,
) -> Result<Response<T>, ContractError> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

explicit arg for nft_contract instead of using info...

match deps.api.addr_validate(&msg.collection) {
Ok(collection_addr) => {
// set collection address as (initial) sender
info.sender = collection_addr;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

before info.sender was overridden using collection...

Comment on lines +321 to +325
.map(|msg| match deps.api.addr_validate(&msg.collection) {
Ok(nft_contract) => self.receive_nft(
deps,
env,
&nft_contract,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

...after change, collection is directly passed.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah much better

@@ -48,7 +48,9 @@ impl Ics721Execute for SgIcs721Contract {
// therefore, we use ics721 creator as owner
creator: ics721_contract_info.creator,
description: "".to_string(),
image: "https://arkprotocol.io".to_string(),
// use Stargaze icon as placeholder
image: "ipfs://bafkreie5vwrm5zts4wiq6ebtopmztgl5qzyl4uszyllgwpaizyc5w2uycm"
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this constant should be in a variable somewhere or in an external file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved to STARGAZE_ICON_PLACEHOLDER

let instantiate =
if CLASS_ID_AND_NFT_CONTRACT_INFO.has(deps.storage, class.id.to_string().as_str()) {
vec![]
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a huge block doing a lot of calculations. Can we put this in a function somewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved to create_instantiate_msg()

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome

))
);

// we migrate only in case CLASS_ID_AND_NFT_CONTRACT_INFO is not populated yet
Copy link
Contributor

@humanalgorithm humanalgorithm Feb 15, 2024

Choose a reason for hiding this comment

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

Maybe we can put this in a function so its clear that its legacy code to be deleted? It would make it more clear which code specifically is to be removed after migration.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved to migrate_legacy()

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you

}
pub fn query_class_id_for_nft_contract(deps: Deps, contract: String) -> StdResult<Option<ClassId>> {
let contract = deps.api.addr_validate(&contract)?;
load_class_id_for_nft_contract(deps.storage, &contract)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice reuse

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

🙏

@@ -2,6 +2,9 @@ use cosmwasm_schema::cw_serde;
use cosmwasm_std::ContractInfoResponse;
use sg721_base::msg::CollectionInfoResponse;

pub const STARGAZE_ICON_PLACEHOLDER: &str =
"ipfs://bafkreie5vwrm5zts4wiq6ebtopmztgl5qzyl4uszyllgwpaizyc5w2uycm";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

waiting for @jhernandezb providing another link

Comment on lines -363 to -364
NFT_CONTRACT_TO_CLASS_ID.save(deps.storage, info.sender.clone(), &class.id)?;
CLASS_ID_TO_NFT_CONTRACT.save(deps.storage, class.id.clone(), &info.sender)?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@humanalgorithm as noted in audit:

In the cw-ics721 package, specifically in state.rs at lines 22 and 25, there are two cw-storage-plus::Maps
named CLASS_ID_TO_NFT_CONTRACT and NFT_CONTRACT_TO_CLASS_ID. These maps are responsible for tracking the class_id and collection_addr in a reciprocal manner. However, this setup leads to redundant operations since any modification to these values necessitates updates in both maps.

So both maps hold redundant data - which requires redundant operations like here.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's funny, I originally proposed the IndexedMap in the original implementation but my suggestion got shot down.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Lol

Comment on lines +27 to +28
class_id: UniqueIndex::new(|d| d.class_id.clone(), "class_id_info__class_id"),
address: UniqueIndex::new(|d| d.address.clone(), "class_id_info__address"),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@humanalgorithm IndexedMap allows multiple indices. Here we need to put an index on ClassId and collection address.

Comment on lines +67 to +68
.idx
.address
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@humanalgorithm here query is done on address index

Comment on lines +82 to +83
.idx
.class_id
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@humanalgorithm here query is done on class id index

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.

Where is execute_admin_clean_and_burn_nft function being used? can't find it

@taitruong
Copy link
Collaborator Author

Where is execute_admin_clean_and_burn_nft function being used? can't find it

This is related to #83 and not part of this PR. There was a bug on packet receive in case of failure in sub msgs and states (especially OUTGOING_CLASS_TOKEN_TO_CHANNEL) not being reverted since it was NOT in a sub msg - but in in main packet receive msg.

In short: this msg can be called by admin using ExecuteMsg::AdminCleanAndBurnNft for fixing states. Please also check counterpart msg AdminCleanAndUnescrowNft:

    /// Admin msg in case something goes wrong.
    /// As a minimum it clean up states (incoming channel and token metadata), and burn NFT if exists.
    AdminCleanAndBurnNft {
        owner: String,
        token_id: String,
        class_id: String,
        collection: String,
    },

    /// Admin msg in case something goes wrong.
    /// As a minimum it clean up state (outgoing channel), and transfer NFT if exists.
    /// - transfer NFT if exists
    AdminCleanAndUnescrowNft {
        recipient: String,
        token_id: String,
        class_id: String,
        collection: String,
    },

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.

LFG

Merged via the queue into public-awesome:main with commit 65b9b78 Feb 19, 2024
2 checks 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.

4 participants