Repository: public-awesome/launchpad
Pull request #586 (2023-06-28 11:53:30)

Royalty increase option for v1 collections


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 7f604357..55cea4f3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -915,7 +915,7 @@ dependencies = [
 
 [[package]]
 name = "sg721-updatable"
-version = "0.8.3"
+version = "0.8.4"
 dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
diff --git a/contracts/royalty-group/src/contract.rs b/contracts/royalty-group/src/contract.rs
index ac574f92..c97b8844 100644
--- a/contracts/royalty-group/src/contract.rs
+++ b/contracts/royalty-group/src/contract.rs
@@ -173,7 +173,7 @@ pub fn execute_distribute_funds(
     let attributes = vec![
         attr("action", "distribute_funds"),
         attr("funds", funds.to_string()),
-        attr("sender", &info.sender),
+        attr("sender", info.sender),
     ];
 
     Ok(Response::new()
diff --git a/contracts/sg721-updatable/Cargo.toml b/contracts/sg721-updatable/Cargo.toml
index e4da37af..f0a28ab9 100644
--- a/contracts/sg721-updatable/Cargo.toml
+++ b/contracts/sg721-updatable/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name    = "sg721-updatable"
-version = "0.8.3"
+version = "0.8.4"
 authors = ["John Y <j@noreply.publicawesome.com>"]
 edition = "2018"
 
diff --git a/contracts/sg721-updatable/schema/collection_info_response.json b/contracts/sg721-updatable/schema/collection_info_response.json
index d285d111..326778c7 100644
--- a/contracts/sg721-updatable/schema/collection_info_response.json
+++ b/contracts/sg721-updatable/schema/collection_info_response.json
@@ -51,8 +51,30 @@
         },
         "share": {
           "$ref": "#/definitions/Decimal"
+        },
+        "updated_at": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Timestamp"
+            },
+            {
+              "type": "null"
+            }
+          ]
         }
       }
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
     }
   }
 }
diff --git a/contracts/sg721-updatable/schema/instantiate_msg.json b/contracts/sg721-updatable/schema/instantiate_msg.json
index fb28071b..f187ad52 100644
--- a/contracts/sg721-updatable/schema/instantiate_msg.json
+++ b/contracts/sg721-updatable/schema/instantiate_msg.json
@@ -74,8 +74,30 @@
         },
         "share": {
           "$ref": "#/definitions/Decimal"
+        },
+        "updated_at": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Timestamp"
+            },
+            {
+              "type": "null"
+            }
+          ]
         }
       }
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
     }
   }
 }
diff --git a/contracts/sg721-updatable/src/contract.rs b/contracts/sg721-updatable/src/contract.rs
index 3ba80803..72e372d8 100644
--- a/contracts/sg721-updatable/src/contract.rs
+++ b/contracts/sg721-updatable/src/contract.rs
@@ -7,8 +7,7 @@ use cosmwasm_std::{
     Uint128,
 };
 use cw2::set_contract_version;
-
-use cw_utils::nonpayable;
+use cw_utils::{nonpayable, Expiration, DAY};
 use sg_std::fees::burn_and_distribute_fee;
 use sg_std::StargazeMsgWrapper;
 
@@ -27,13 +26,14 @@ const CONTRACT_NAME: &str = "crates.io:sg721-updatable";
 const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
 
 const CREATION_FEE: u128 = 1_000_000_000;
+const MAX_ROYALTY_SHARE_BPS: u64 = 1_000;
 
 // Disable instantiation for production build
 #[cfg(target = "wasm32-unknown-unknown")]
 const ENABLE_INSTANTIATE: bool = false;
 
 #[cfg(not(target = "wasm32-unknown-unknown"))]
-const ENABLE_INSTANTIATE: bool = false;
+const ENABLE_INSTANTIATE: bool = true;
 
 type Response = cosmwasm_std::Response<StargazeMsgWrapper>;
 pub type Sg721Contract<'a> = cw721_base::Cw721Contract<'a, Empty, StargazeMsgWrapper>;
@@ -50,7 +50,7 @@ pub fn instantiate(
     }
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
 
-    let fee_msgs = burn_and_distribute_fee(env, &info, CREATION_FEE)?;
+    let fee_msgs = burn_and_distribute_fee(env.clone(), &info, CREATION_FEE)?;
 
     // cw721 instantiation
     let info = ContractInfoResponse {
@@ -81,6 +81,7 @@ pub fn instantiate(
         Some(royalty_info) => Some(RoyaltyInfo {
             payment_address: deps.api.addr_validate(&royalty_info.payment_address)?,
             share: royalty_info.share_validate()?,
+            updated_at: Some(env.block.time),
         }),
         None => None,
     };
@@ -122,7 +123,7 @@ pub fn execute(
         ExecuteMsg::UpdateRoyaltyInfo {
             payment_address,
             share_bps,
-        } => execute_update_royalty_info(deps, info, payment_address, share_bps),
+        } => execute_update_royalty_info(deps, info, env, payment_address, share_bps),
         _ => Sg721Contract::default()
             .execute(deps, env, info, msg.into())
             .map_err(|e| e.into()),
@@ -132,6 +133,7 @@ pub fn execute(
 pub fn execute_update_royalty_info(
     deps: DepsMut,
     info: MessageInfo,
+    env: Env,
     payment_address: String,
     share_bps: u64,
 ) -> Result<Response, ContractError> {
@@ -144,21 +146,35 @@ pub fn execute_update_royalty_info(
 
     let payment_addr = deps.api.addr_validate(&payment_address)?;
     let share = Decimal::percent(share_bps) / Uint128::from(100u128);
+    let max_share_limit = Decimal::percent(MAX_ROYALTY_SHARE_BPS) / Uint128::from(100u128);
+
+    if share > max_share_limit {
+        return Err(ContractError::RoyaltyShareIncreasedTooMuch {});
+    }
 
     let current_royalty_info = collection_info.royalty_info;
 
-    if let Some(current_royalty_info) = current_royalty_info {
-        if share > current_royalty_info.share {
-            return Err(ContractError::RoyaltyShareIncreased {});
+    if let Some(curr_royalty_info) = current_royalty_info {
+        let updated_at = match curr_royalty_info.updated_at {
+            Some(updated_at) => updated_at,
+            // If current royalty info updated_at is None, it might be an older version that has not been migrated properly.
+            None => return Err(ContractError::RoyaltyInfoInvalid {}),
+        };
+
+        // make sure the update time is after 24 hours
+        let end = (Expiration::AtTime(updated_at) + DAY)?;
+        let update_too_soon = !end.is_expired(&env.block);
+
+        if share > curr_royalty_info.share && update_too_soon {
+            return Err(ContractError::RoyaltyUpdateTooSoon {});
         }
-    } else {
-        return Err(ContractError::RoyaltyShareIncreased {});
     }
-
     collection_info.royalty_info = Some(RoyaltyInfo {
         payment_address: payment_addr,
         share,
+        updated_at: Some(env.block.time),
     });
+
     COLLECTION_INFO.save(deps.storage, &collection_info)?;
 
     let event = Event::new("update_royalty_info")
@@ -240,6 +256,7 @@ fn query_config(deps: Deps) -> StdResult<CollectionInfoResponse> {
         Some(royalty_info) => Some(RoyaltyInfoResponse {
             payment_address: royalty_info.payment_address.to_string(),
             share: royalty_info.share,
+            updated_at: royalty_info.updated_at,
         }),
         None => None,
     };
@@ -279,6 +296,18 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result<Response, Contra
 
     FROZEN_TOKEN_METADATA.save(deps.storage, &false)?;
 
+    let mut collection_info = COLLECTION_INFO.load(deps.storage)?;
+    let current_royalty_info = collection_info.royalty_info;
+    if let Some(royalty_info) = current_royalty_info {
+        let new_royalty_info = RoyaltyInfo {
+            payment_address: royalty_info.payment_address,
+            share: royalty_info.share,
+            // setting updated_at to 1 day ago to make updating royalties possible right after a migration
+            updated_at: Some(_env.block.time.minus_seconds(86400)),
+        };
+        collection_info.royalty_info = Some(new_royalty_info);
+        COLLECTION_INFO.save(deps.storage, &collection_info)?;
+    }
     // set new contract version
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
     Ok(Response::new())
@@ -346,6 +375,7 @@ mod tests {
                 royalty_info: Some(RoyaltyInfoResponse {
                     payment_address: creator.clone(),
                     share: Decimal::percent(10),
+                    updated_at: None,
                 }),
             },
         };
@@ -362,6 +392,7 @@ mod tests {
             Some(RoyaltyInfoResponse {
                 payment_address: creator,
                 share: Decimal::percent(10),
+                updated_at: Some(mock_env().block.time),
             }),
             value.royalty_info
         );
diff --git a/contracts/sg721-updatable/src/error.rs b/contracts/sg721-updatable/src/error.rs
index 651a6aa4..041f68c9 100644
--- a/contracts/sg721-updatable/src/error.rs
+++ b/contracts/sg721-updatable/src/error.rs
@@ -30,14 +30,20 @@ pub enum ContractError {
     #[error("Approval not found for: {spender}")]
     ApprovalNotFound { spender: String },
 
-    #[error("Invalid Royalities")]
-    InvalidRoyalities {},
+    #[error("Invalid Royalties")]
+    InvalidRoyalties {},
 
     #[error("Description too long")]
     DescriptionTooLong {},
 
-    #[error("Royalty share increased")]
-    RoyaltyShareIncreased {},
+    #[error("Royalty share percentage cannot exceed 10%")]
+    RoyaltyShareIncreasedTooMuch {},
+
+    #[error("Royalty share percentage can only be increased a day after the last update")]
+    RoyaltyUpdateTooSoon {},
+
+    #[error("Royalty Info invalid")]
+    RoyaltyInfoInvalid {},
 
     #[error("{0}")]
     Payment(#[from] PaymentError),
diff --git a/contracts/sg721-updatable/src/msg.rs b/contracts/sg721-updatable/src/msg.rs
index 96269d5c..34c0b3ce 100644
--- a/contracts/sg721-updatable/src/msg.rs
+++ b/contracts/sg721-updatable/src/msg.rs
@@ -19,12 +19,13 @@ pub type Cw721ExecuteMsg = cw721_base::msg::ExecuteMsg<Empty>;
 pub struct RoyaltyInfoResponse {
     pub payment_address: String,
     pub share: Decimal,
+    pub updated_at: Option<Timestamp>,
 }
 
 impl RoyaltyInfoResponse {
     pub fn share_validate(&self) -> Result<Decimal, ContractError> {
         if self.share > Decimal::one() {
-            return Err(ContractError::InvalidRoyalities {});
+            return Err(ContractError::InvalidRoyalties {});
         }
 
         Ok(self.share)
@@ -81,8 +82,8 @@ pub enum ExecuteMsg<T> {
 
 impl<T> From<ExecuteMsg<T>> for Cw721ExecuteMsg
 where
-    T: Clone + PartialEq + Into<Option<cosmwasm_std::Empty>>,
-    Option<cosmwasm_std::Empty>: From<T>,
+    T: Clone + PartialEq + Into<Option<Empty>>,
+    Option<Empty>: From<T>,
 {
     fn from(msg: ExecuteMsg<T>) -> Cw721ExecuteMsg {
         match msg {
diff --git a/contracts/sg721-updatable/src/state.rs b/contracts/sg721-updatable/src/state.rs
index 7a66330a..227694e1 100644
--- a/contracts/sg721-updatable/src/state.rs
+++ b/contracts/sg721-updatable/src/state.rs
@@ -1,4 +1,4 @@
-use cosmwasm_std::{Addr, Decimal};
+use cosmwasm_std::{Addr, Decimal, Timestamp};
 use cw_storage_plus::Item;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -16,6 +16,7 @@ pub struct CollectionInfo<T> {
 pub struct RoyaltyInfo {
     pub payment_address: Addr,
     pub share: Decimal,
+    pub updated_at: Option<Timestamp>,
 }
 
 pub const FROZEN_TOKEN_METADATA: Item<bool> = Item::new("frozen_token_metadata");
diff --git a/contracts/sg721/src/error.rs b/contracts/sg721/src/error.rs
index e2f7abfa..80659376 100644
--- a/contracts/sg721/src/error.rs
+++ b/contracts/sg721/src/error.rs
@@ -25,8 +25,8 @@ pub enum ContractError {
     #[error("Approval not found for: {spender}")]
     ApprovalNotFound { spender: String },
 
-    #[error("Invalid Royalities")]
-    InvalidRoyalities {},
+    #[error("Invalid Royalties")]
+    InvalidRoyalties {},
 
     #[error("Description too long")]
     DescriptionTooLong {},
diff --git a/contracts/sg721/src/msg.rs b/contracts/sg721/src/msg.rs
index 522b6cb9..69e2a3f5 100644
--- a/contracts/sg721/src/msg.rs
+++ b/contracts/sg721/src/msg.rs
@@ -21,7 +21,7 @@ pub struct RoyaltyInfoResponse {
 impl RoyaltyInfoResponse {
     pub fn share_validate(&self) -> Result<Decimal, ContractError> {
         if self.share > Decimal::one() {
-            return Err(ContractError::InvalidRoyalities {});
+            return Err(ContractError::InvalidRoyalties {});
         }
 
         Ok(self.share)


Test patch:
diff --git a/contracts/minter/src/contract_updatable_tests.rs b/contracts/minter/src/contract_updatable_tests.rs
index 4f348429..3e1c2f08 100644
--- a/contracts/minter/src/contract_updatable_tests.rs
+++ b/contracts/minter/src/contract_updatable_tests.rs
@@ -62,6 +62,15 @@ pub fn contract_sg721() -> Box<dyn Contract<StargazeMsgWrapper>> {
     Box::new(contract)
 }
 
+pub fn contract_sg721_base() -> Box<dyn Contract<StargazeMsgWrapper>> {
+    let contract = ContractWrapper::new(
+        sg721::contract::execute,
+        sg721::contract::instantiate,
+        sg721::contract::query,
+    );
+    Box::new(contract)
+}
+
 fn setup_whitelist_contract(router: &mut StargazeApp, creator: &Addr) -> Addr {
     let whitelist_code_id = router.store_code(contract_whitelist());
 
@@ -1450,7 +1459,7 @@ fn test_update_none_royalties() {
     let mut router = custom_mock_app();
     let (creator, buyer) = setup_accounts(&mut router);
     let num_tokens = 10;
-
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME);
     // Upload contract code
     let sg721_code_id = router.store_code(contract_sg721());
     let minter_code_id = router.store_code(contract_minter());
@@ -1513,9 +1522,15 @@ fn test_update_none_royalties() {
         sg721_updatable::ContractError::Unauthorized {}.to_string()
     );
 
+    let update_royalty_msg: Sg721UpdatableExecuteMsg<Extension> =
+        Sg721UpdatableExecuteMsg::UpdateRoyaltyInfo {
+            payment_address: "creator2".to_string(),
+            share_bps: 1200,
+        };
+
     let res = router.execute_contract(
-        creator,
-        Addr::unchecked(sg71_address),
+        creator.clone(),
+        Addr::unchecked(sg71_address.clone()),
         &update_royalty_msg,
         &[],
     );
@@ -1523,8 +1538,97 @@ fn test_update_none_royalties() {
 
     assert_eq!(
         err.source().unwrap().to_string(),
-        sg721_updatable::ContractError::RoyaltyShareIncreased {}.to_string()
+        sg721_updatable::ContractError::RoyaltyShareIncreasedTooMuch {}.to_string()
+    );
+
+    let update_royalty_msg: Sg721UpdatableExecuteMsg<Extension> =
+        Sg721UpdatableExecuteMsg::UpdateRoyaltyInfo {
+            payment_address: "creator2".to_string(),
+            share_bps: 400,
+        };
+
+    let res = router
+        .execute_contract(
+            creator.clone(),
+            Addr::unchecked(sg71_address.clone()),
+            &update_royalty_msg,
+            &[],
+        )
+        .unwrap();
+
+    assert_eq!(res.events[1].attributes[0].key, "_contract_addr");
+    assert_eq!(res.events[1].attributes[0].value, "contract1");
+    assert_eq!(res.events[1].attributes[1].key, "sender");
+    assert_eq!(res.events[1].attributes[1].value, "creator");
+    assert_eq!(res.events[1].attributes[2].key, "payment_address");
+    assert_eq!(res.events[1].attributes[2].value, "creator2");
+    assert_eq!(res.events[1].attributes[3].key, "share");
+    assert_eq!(res.events[1].attributes[3].value, "0.04");
+
+    let update_royalty_msg: Sg721UpdatableExecuteMsg<Extension> =
+        Sg721UpdatableExecuteMsg::UpdateRoyaltyInfo {
+            payment_address: "creator2".to_string(),
+            share_bps: 500,
+        };
+
+    let res = router.execute_contract(
+        creator.clone(),
+        Addr::unchecked(sg71_address.clone()),
+        &update_royalty_msg,
+        &[],
     );
+    let err = res.unwrap_err();
+
+    assert_eq!(
+        err.source().unwrap().to_string(),
+        sg721_updatable::ContractError::RoyaltyUpdateTooSoon {}.to_string()
+    );
+
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME + 86400000000000);
+
+    let res = router
+        .execute_contract(
+            creator.clone(),
+            Addr::unchecked(sg71_address.clone()),
+            &update_royalty_msg,
+            &[],
+        )
+        .unwrap();
+
+    assert_eq!(res.events[1].attributes[0].key, "_contract_addr");
+    assert_eq!(res.events[1].attributes[0].value, "contract1");
+    assert_eq!(res.events[1].attributes[1].key, "sender");
+    assert_eq!(res.events[1].attributes[1].value, "creator");
+    assert_eq!(res.events[1].attributes[2].key, "payment_address");
+    assert_eq!(res.events[1].attributes[2].value, "creator2");
+    assert_eq!(res.events[1].attributes[3].key, "share");
+    assert_eq!(res.events[1].attributes[3].value, "0.05");
+
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME + 86500000000000);
+
+    let update_royalty_msg: Sg721UpdatableExecuteMsg<Extension> =
+        Sg721UpdatableExecuteMsg::UpdateRoyaltyInfo {
+            payment_address: "creator2".to_string(),
+            share_bps: 300,
+        };
+
+    let res = router
+        .execute_contract(
+            creator,
+            Addr::unchecked(sg71_address),
+            &update_royalty_msg,
+            &[],
+        )
+        .unwrap();
+
+    assert_eq!(res.events[1].attributes[0].key, "_contract_addr");
+    assert_eq!(res.events[1].attributes[0].value, "contract1");
+    assert_eq!(res.events[1].attributes[1].key, "sender");
+    assert_eq!(res.events[1].attributes[1].value, "creator");
+    assert_eq!(res.events[1].attributes[2].key, "payment_address");
+    assert_eq!(res.events[1].attributes[2].value, "creator2");
+    assert_eq!(res.events[1].attributes[3].key, "share");
+    assert_eq!(res.events[1].attributes[3].value, "0.03");
 }
 
 #[test]
@@ -1532,7 +1636,7 @@ fn test_update_royalties() {
     let mut router = custom_mock_app();
     let (creator, _) = setup_accounts(&mut router);
     let num_tokens = 10;
-
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME);
     // Upload contract code
     let sg721_code_id = router.store_code(contract_sg721());
     let minter_code_id = router.store_code(contract_minter());
@@ -1558,7 +1662,7 @@ fn test_update_royalties() {
                 external_link: Some("https://example.com/external.html".to_string()),
                 royalty_info: Some(RoyaltyInfoResponse {
                     payment_address: creator.to_string(),
-                    share: Decimal::percent(10),
+                    share: Decimal::percent(5),
                 }),
             },
         },
@@ -1595,7 +1699,7 @@ fn test_update_royalties() {
 
     assert_eq!(
         err.source().unwrap().to_string(),
-        sg721_updatable::ContractError::RoyaltyShareIncreased {}.to_string()
+        sg721_updatable::ContractError::RoyaltyShareIncreasedTooMuch {}.to_string()
     );
 
     let update_royalty_msg: sg721_updatable::msg::ExecuteMsg<Extension> =
@@ -1603,6 +1707,37 @@ fn test_update_royalties() {
             payment_address: "creator2".to_string(),
             share_bps: 900,
         };
+    let res = router.execute_contract(
+        creator.clone(),
+        Addr::unchecked(sg71_address.clone()),
+        &update_royalty_msg,
+        &[],
+    );
+
+    let err = res.unwrap_err();
+
+    assert_eq!(
+        err.source().unwrap().to_string(),
+        sg721_updatable::ContractError::RoyaltyUpdateTooSoon {}.to_string()
+    );
+
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME + 86400000000000);
+
+    let res = router.execute_contract(
+        creator.clone(),
+        Addr::unchecked(sg71_address.clone()),
+        &update_royalty_msg,
+        &[],
+    );
+    assert!(res.is_ok());
+
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME + 86500000000000);
+
+    let update_royalty_msg: sg721_updatable::msg::ExecuteMsg<Extension> =
+        sg721_updatable::msg::ExecuteMsg::UpdateRoyaltyInfo {
+            payment_address: "creator2".to_string(),
+            share_bps: 800,
+        };
     let res = router.execute_contract(
         creator,
         Addr::unchecked(sg71_address),
@@ -1725,14 +1860,84 @@ fn try_migrate() {
     );
 
     // // no op when same version
-    set_contract_version(
-        &mut deps.storage,
-        "crates.io:sg-721",
-        env!("CARGO_PKG_VERSION"),
-    )
-    .unwrap();
-    sg721_updatable::contract::migrate(deps.as_mut(), env.clone(), Empty {}).unwrap();
-
-    set_contract_version(&mut deps.storage, "crates.io:sg721-updatable", "0.8.0").unwrap();
+    set_contract_version(&mut deps.storage, "crates.io:sg-721", "0.8.4").unwrap();
     sg721_updatable::contract::migrate(deps.as_mut(), env, Empty {}).unwrap();
+
+    // Migration happy path
+    let mut router = custom_mock_app();
+    let (creator, _) = setup_accounts(&mut router);
+    let num_tokens = 10;
+    // Upload contract code
+    let sg721_base_code_id = router.store_code(contract_sg721_base());
+    let sg721_code_id = router.store_code(contract_sg721());
+    let minter_code_id = router.store_code(contract_minter());
+    let creation_fee = coins(CREATION_FEE, NATIVE_DENOM);
+
+    // Instantiate minter
+    let msg = InstantiateMsg {
+        unit_price: coin(UNIT_PRICE, NATIVE_DENOM),
+        num_tokens,
+        start_time: Timestamp::from_nanos(GENESIS_MINT_START_TIME),
+        per_address_limit: 5,
+        whitelist: None,
+        base_token_uri: "ipfs://QmYxw1rURvnbQbBRTfmVaZtxSrkrfsbodNzibgBrVrUrtN".to_string(),
+        sg721_code_id: sg721_base_code_id,
+        sg721_instantiate_msg: Sg721InstantiateMsg {
+            name: String::from("TEST"),
+            symbol: String::from("TEST"),
+            minter: creator.to_string(),
+            collection_info: CollectionInfo {
+                creator: creator.to_string(),
+                description: String::from("Stargaze Monkeys"),
+                image: "https://example.com/image.png".to_string(),
+                external_link: Some("https://example.com/external.html".to_string()),
+                royalty_info: Some(RoyaltyInfoResponse {
+                    payment_address: creator.to_string(),
+                    share: Decimal::percent(5),
+                }),
+            },
+        },
+    };
+    let minter_addr = router
+        .instantiate_contract(
+            minter_code_id,
+            creator.clone(),
+            &msg,
+            &creation_fee,
+            "Minter",
+            None,
+        )
+        .unwrap();
+    let config: ConfigResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr, &QueryMsg::Config {})
+        .unwrap();
+
+    let sg721_base_address = config.sg721_address;
+
+    let collection_info: CollectionInfo<sg721_updatable::msg::RoyaltyInfoResponse> = router
+        .wrap()
+        .query_wasm_smart(
+            sg721_base_address.clone(),
+            &sg721::msg::QueryMsg::CollectionInfo {},
+        )
+        .unwrap();
+
+    assert!(collection_info.royalty_info.unwrap().updated_at.is_none());
+
+    router
+        .migrate_contract(
+            creator,
+            Addr::unchecked(sg721_base_address.clone()),
+            &Empty {},
+            sg721_code_id,
+        )
+        .unwrap();
+
+    let collection_info: CollectionInfo<sg721_updatable::msg::RoyaltyInfoResponse> = router
+        .wrap()
+        .query_wasm_smart(sg721_base_address, &sg721::msg::QueryMsg::CollectionInfo {})
+        .unwrap();
+
+    assert!(collection_info.royalty_info.unwrap().updated_at.is_some(),);
 }