diff --git a/.gitignore b/.gitignore index ded8580..9292387 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ tests/test-data.json .env .scannerwork/ -clippy-report.json \ No newline at end of file +clippy-report.json +.DS_Store \ No newline at end of file diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 85fbdf5..e87b409 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -166,6 +166,9 @@ pub fn execute( args, } => ibc_hooks_receive(deps, env, info, func, orai_receiver, args), ExecuteMsg::RegisterDenom(msg) => register_denom(deps, info, msg), + ExecuteMsg::WithdrawAsset { coin, receiver } => { + execute_withdraw_asset(deps, info, coin, receiver) + } } } @@ -178,6 +181,24 @@ pub fn is_caller_contract(caller: Addr, contract_addr: Addr) -> StdResult<()> { Ok(()) } +// withdraw stuck coin and transfer back to user +// only owner can execute +fn execute_withdraw_asset( + deps: DepsMut, + info: MessageInfo, + coin: Amount, + receiver: Option, +) -> Result { + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; + let receiver = receiver.unwrap_or(info.sender); + + let msg = coin.send_amount(receiver.to_string(), None); + + Ok(Response::new() + .add_attributes(vec![("action", "withdraw_asset")]) + .add_message(msg)) +} + pub fn register_denom( deps: DepsMut, info: MessageInfo, diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index 2211fd2..34a972f 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -105,6 +105,10 @@ pub enum ExecuteMsg { args: Binary, }, RegisterDenom(RegisterDenomMsg), + WithdrawAsset { + coin: Amount, + receiver: Option, + }, } #[cw_serde] diff --git a/contracts/cw-ics20-latest/src/testing/ibc_tests.rs b/contracts/cw-ics20-latest/src/testing/ibc_tests.rs index a7ceab6..040fb22 100644 --- a/contracts/cw-ics20-latest/src/testing/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/testing/ibc_tests.rs @@ -28,7 +28,7 @@ use cosmwasm_std::{ use crate::error::ContractError; use crate::state::{ - get_key_ics20_ibc_denom, increase_channel_balance, reduce_channel_balance, Config, + get_key_ics20_ibc_denom, increase_channel_balance, reduce_channel_balance, Config, ADMIN, CHANNEL_REVERSE_STATE, CONFIG, RELAYER_FEE, REPLY_ARGS, TOKEN_FEE, }; use cw20::{Cw20CoinVerified, Cw20ExecuteMsg, Cw20ReceiveMsg}; @@ -2487,3 +2487,52 @@ pub fn test_get_follow_up_msg() { ); // case 4: call universal swap (todo) } + +#[test] +fn test_withdraw_stuck_asset() { + let mut deps = mock_dependencies(); + ADMIN + .set(deps.as_mut(), Some(Addr::unchecked("admin"))) + .unwrap(); + + // case 1: unauthorized + let err = execute( + deps.as_mut(), + mock_env(), + mock_info("addr000", &[]), + ExecuteMsg::WithdrawAsset { + coin: Amount::Native(Coin { + denom: "orai".to_string(), + amount: Uint128::new(1000000), + }), + receiver: Some(Addr::unchecked("receiver")), + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Admin(AdminError::NotAdmin {})); + + // case 2: success + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + ExecuteMsg::WithdrawAsset { + coin: Amount::Native(Coin { + denom: "orai".to_string(), + amount: Uint128::new(1000000), + }), + receiver: Some(Addr::unchecked("receiver")), + }, + ) + .unwrap(); + assert_eq!( + res.messages, + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "receiver".to_string(), + amount: vec![Coin { + denom: "orai".to_string(), + amount: Uint128::new(1000000) + }] + }))] + ); +}