diff --git a/contracts/cw20-base/src/contract.rs b/contracts/cw20-base/src/contract.rs index 2bdf482cb..09904fe15 100644 --- a/contracts/cw20-base/src/contract.rs +++ b/contracts/cw20-base/src/contract.rs @@ -393,7 +393,7 @@ pub fn execute_update_minter( deps: DepsMut, _env: Env, info: MessageInfo, - new_minter: String, + new_minter: Option, ) -> Result { let mut config = TOKEN_INFO .may_load(deps.storage)? @@ -404,18 +404,27 @@ pub fn execute_update_minter( return Err(ContractError::Unauthorized {}); } - let minter = deps.api.addr_validate(&new_minter)?; - let minter_data = MinterData { - minter, - cap: mint.cap, - }; - config.mint = Some(minter_data); + let minter_data = new_minter + .map(|new_minter| deps.api.addr_validate(&new_minter)) + .transpose()? + .map(|minter| MinterData { + minter, + cap: mint.cap, + }); + + config.mint = minter_data; TOKEN_INFO.save(deps.storage, &config)?; Ok(Response::default() .add_attribute("action", "update_minter") - .add_attribute("new_minter", new_minter)) + .add_attribute( + "new_minter", + config + .mint + .map(|m| m.minter.into_string()) + .unwrap_or_else(|| "None".to_string()), + )) } pub fn execute_update_marketing( @@ -927,7 +936,7 @@ mod tests { let new_minter = "new_minter"; let msg = ExecuteMsg::UpdateMinter { - new_minter: new_minter.to_string(), + new_minter: Some(new_minter.to_string()), }; let info = mock_info(&minter, &[]); @@ -956,7 +965,7 @@ mod tests { ); let msg = ExecuteMsg::UpdateMinter { - new_minter: String::from("new_minter"), + new_minter: Some("new_minter".to_string()), }; let info = mock_info("not the minter", &[]); @@ -965,6 +974,43 @@ mod tests { assert_eq!(err, ContractError::Unauthorized {}); } + #[test] + fn unset_minter() { + let mut deps = mock_dependencies(); + let minter = String::from("minter"); + let cap = None; + do_instantiate_with_minter( + deps.as_mut(), + &String::from("genesis"), + Uint128::new(1234), + &minter, + cap, + ); + + let msg = ExecuteMsg::UpdateMinter { new_minter: None }; + + let info = mock_info(&minter, &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env.clone(), info, msg); + assert!(res.is_ok()); + let query_minter_msg = QueryMsg::Minter {}; + let res = query(deps.as_ref(), env, query_minter_msg); + let mint: Option = from_binary(&res.unwrap()).unwrap(); + + // Check that mint information was removed. + assert_eq!(mint, None); + + // Check that old minter can no longer mint. + let msg = ExecuteMsg::Mint { + recipient: String::from("lucky"), + amount: Uint128::new(222), + }; + let info = mock_info("minter", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + #[test] fn no_one_mints_if_minter_unset() { let mut deps = mock_dependencies(); diff --git a/packages/cw20/README.md b/packages/cw20/README.md index 970db35ff..ee0862f0d 100644 --- a/packages/cw20/README.md +++ b/packages/cw20/README.md @@ -128,6 +128,11 @@ minter address and handle updating the ACL there. this will create `amount` new tokens (updating total supply) and add them to the balance of `recipient`, as long as it does not exceed the cap. +`UpdateMinter { new_minter: Option }` - Callable only by the +current minter. If `new_minter` is `Some(address)` the minter is set +to the specified address, otherwise the minter is removed and no +future minters may be set. + ### Queries `Minter{}` - Returns who and how much can be minted. Return type is diff --git a/packages/cw20/src/msg.rs b/packages/cw20/src/msg.rs index 4cd6fba63..f2f9708c9 100644 --- a/packages/cw20/src/msg.rs +++ b/packages/cw20/src/msg.rs @@ -55,8 +55,10 @@ pub enum Cw20ExecuteMsg { /// Only with the "mintable" extension. If authorized, creates amount new tokens /// and adds to the recipient balance. Mint { recipient: String, amount: Uint128 }, - /// Only with the "mintable" extension. The current minter may set a new minter. - UpdateMinter { new_minter: String }, + /// Only with the "mintable" extension. The current minter may set + /// a new minter. Setting the minter to None will remove the + /// token's minter forever. + UpdateMinter { new_minter: Option }, /// Only with the "marketing" extension. If authorized, updates marketing metadata. /// Setting None/null for any of these will leave it unchanged. /// Setting Some("") will clear this field on the contract storage