diff --git a/.changelog/v3.6.11/bug-fixes/395-fix-move-adapter-funds-deposit-denom.md b/.changelog/v3.6.11/bug-fixes/395-fix-move-adapter-funds-deposit-denom.md new file mode 100644 index 00000000..5644d85b --- /dev/null +++ b/.changelog/v3.6.11/bug-fixes/395-fix-move-adapter-funds-deposit-denom.md @@ -0,0 +1,2 @@ +- Fix `MoveAdapterFunds` failing with `InsufficientBalance` when moving the vault's deposit denom between adapters. + ([\#395](https://github.com/informalsystems/hydro/pull/395)) diff --git a/.changelog/v3.6.11/features/413-add-remove-admin-standard-interface.md b/.changelog/v3.6.11/features/413-add-remove-admin-standard-interface.md new file mode 100644 index 00000000..d7a65576 --- /dev/null +++ b/.changelog/v3.6.11/features/413-add-remove-admin-standard-interface.md @@ -0,0 +1 @@ +- Add `AddAdmin`/`RemoveAdmin` execute messages and `Admins` query to the standard adapter interface, implemented across all inflow adapters (CCTP, IBC, Skip, Mars). Add `AddWhitelistAdmin`/`RemoveWhitelistAdmin` to the Hydro contract. ([\#413](https://github.com/informalsystems/hydro/pull/413)) diff --git a/.changelog/v3.6.11/summary.md b/.changelog/v3.6.11/summary.md new file mode 100644 index 00000000..30647a90 --- /dev/null +++ b/.changelog/v3.6.11/summary.md @@ -0,0 +1 @@ +Date: April 16th, 2026 diff --git a/CHANGELOG b/CHANGELOG index 76a323bb..68afc935 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,18 @@ # CHANGELOG +## v3.6.11 + +Date: April 16th, 2026 + +### BUG FIXES + +- Fix `MoveAdapterFunds` failing with `InsufficientBalance` when moving the vault's deposit denom between adapters. + ([\#395](https://github.com/informalsystems/hydro/pull/395)) + +### FEATURES + +- Add `AddAdmin`/`RemoveAdmin` execute messages and `Admins` query to the standard adapter interface, implemented across all inflow adapters (CCTP, IBC, Skip, Mars). Add `AddWhitelistAdmin`/`RemoveWhitelistAdmin` to the Hydro contract. ([\#413](https://github.com/informalsystems/hydro/pull/413)) + ## v3.6.10 Date: April 2nd, 2026 @@ -383,12 +396,12 @@ Date: November 18th, 2024 ### FEATURES +- Add a minimum liquidity request value to proposals. + ([\#164](https://github.com/informalsystems/hydro/pull/164)) - Adjusts tributes to only be claimable if their proposal received a non-zero fund deployment. ([\#164](https://github.com/informalsystems/hydro/pull/164)) - Allow whitelist admins to register performed liquidity deployments in the Hydro contract. ([\#164](https://github.com/informalsystems/hydro/pull/164)) -- Add a minimum liquidity request value to proposals. - ([\#164](https://github.com/informalsystems/hydro/pull/164)) - Allow bids to set a custom duration they would like to receive liquidity for. ([\#165](https://github.com/informalsystems/hydro/pull/165)) diff --git a/Cargo.lock b/Cargo.lock index 89f39ed0..007b6bdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,7 +566,7 @@ dependencies = [ [[package]] name = "cctp-adapter" -version = "3.6.10" +version = "3.6.11" dependencies = [ "bech32 0.11.0", "cosmwasm-schema 2.2.2", @@ -614,7 +614,7 @@ dependencies = [ [[package]] name = "claim" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -644,7 +644,7 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "control-center" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmos-sdk-proto 0.20.0", "cosmwasm-schema 2.2.2", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "cw-orch-interface" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cw-orch", "hydro", @@ -1377,7 +1377,7 @@ dependencies = [ [[package]] name = "d-token-info-provider" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -1418,7 +1418,7 @@ dependencies = [ [[package]] name = "dao-voting-adapter" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 1.5.11", "cosmwasm-std 1.5.11", @@ -1842,7 +1842,7 @@ dependencies = [ [[package]] name = "gatekeeper" -version = "3.6.10" +version = "3.6.11" dependencies = [ "bech32 0.11.0", "cosmwasm-schema 2.2.2", @@ -2114,7 +2114,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hydro" -version = "3.6.10" +version = "3.6.11" dependencies = [ "bech32 0.11.0", "cosmos-sdk-proto 0.20.0", @@ -2293,7 +2293,7 @@ dependencies = [ [[package]] name = "ibc-adapter" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -2464,7 +2464,7 @@ dependencies = [ [[package]] name = "interface" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -2605,7 +2605,7 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "liquid-collateral" -version = "3.6.10" +version = "3.6.11" dependencies = [ "anyhow", "bigdecimal", @@ -2649,7 +2649,7 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lsm-token-info-provider" -version = "3.6.10" +version = "3.6.11" dependencies = [ "bech32 0.11.0", "cosmos-sdk-proto 0.20.0", @@ -2670,7 +2670,7 @@ dependencies = [ [[package]] name = "marketplace" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -2686,7 +2686,7 @@ dependencies = [ [[package]] name = "mars-adapter" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -3357,7 +3357,7 @@ dependencies = [ [[package]] name = "proxy" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -4140,7 +4140,7 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "skip-adapter" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "st-token-info-provider" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -4593,7 +4593,7 @@ dependencies = [ [[package]] name = "test-e2e" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-std 2.2.2", "cw-orch", @@ -4620,7 +4620,7 @@ dependencies = [ [[package]] name = "test-utils" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-std 2.2.2", ] @@ -4976,7 +4976,7 @@ dependencies = [ [[package]] name = "tribute" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -5047,7 +5047,7 @@ dependencies = [ [[package]] name = "user-registry" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmwasm-schema 2.2.2", "cosmwasm-std 2.2.2", @@ -5078,7 +5078,7 @@ dependencies = [ [[package]] name = "vault" -version = "3.6.10" +version = "3.6.11" dependencies = [ "cosmos-sdk-proto 0.20.0", "cosmwasm-schema 2.2.2", diff --git a/Cargo.toml b/Cargo.toml index 4e90a97e..e3279f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ members = [ resolver = "2" [workspace.package] -version = "3.6.10" +version = "3.6.11" edition = "2021" [profile.release] diff --git a/artifacts/cctp_adapter.wasm b/artifacts/cctp_adapter.wasm index 4813415c..397e4421 100644 Binary files a/artifacts/cctp_adapter.wasm and b/artifacts/cctp_adapter.wasm differ diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt index e94fe980..d422fad2 100644 --- a/artifacts/checksums.txt +++ b/artifacts/checksums.txt @@ -1,18 +1,18 @@ -6ee316fdd35cfa9e50247e476c69aa2c57317f68903df562189d409988ca9e64 cctp_adapter.wasm -808cb0615cb2e5611d8f4353b94cbfeb49d2ba4e32c0968a958f26650ba8f913 claim.wasm -b70eaf19f74148336c5933dbbfc6897fb11eaa1d846329472e19471aa161cfa3 control_center.wasm -c808f1c2ec62c78df9da4e5307a83eef5a984371d5cee8f6d5413e0a04a6a748 d_token_info_provider.wasm -c1f01653cc73736e1cb10a7f748fa4013428cadf36261aa898ddd098516e872f dao_voting_adapter.wasm -73768aa4ae15a4d918789a7ec7402b1a2c5097c195093aecd8e79a09e69b2988 gatekeeper.wasm -b6c6057990ebcc26c4c998b26727678f8fa344919e4d4ec54438d42428853efb hydro.wasm -5c54a2addc1a08c08164d85f3d43ce8e7ff71b03cc387b183de9117377ce1217 ibc_adapter.wasm -967e145fb8b5efd35b88bcab68920127f612518a295e8ec3430cba690a2f31e4 liquid_collateral.wasm -12d31df1fbc4ecf387f3ae586ae68230dfe85f9dc31755fca62a6dcc12866ba6 lsm_token_info_provider.wasm -8a1c6362491a25d9c12b15112055186de66f37e4fce98da95c478dbab8eb76e2 marketplace.wasm -4d00589d54ef0b361c62ff7a81b508ceede9532ddb6a38ecddc89dd74d5546cb mars_adapter.wasm -21e482b7a36a7fd23b17190aee1438e7d49cf5cd82b779003eb93f23b24ff8b8 proxy.wasm -802737769fc9d8c75fc2d03d392fa4ed0a4c807c530bc6f3d40cc635ebf49f16 skip_adapter.wasm -53efc96d37391028c708c533ac9b50ebbce6320ef3d155c2d67b300243231993 st_token_info_provider.wasm -d083454c951dc4a87cbeb59d5f0ee6f8097aadc83c59ac6df50ded5f37eb4da9 tribute.wasm -e0add67ee207ec20a725d7400d99014d429c59f8e692e03fc58aa6fb140eebcc user_registry.wasm -3a1ac6a46a0e3b272a98640e9b2c71bc5cc7ab56da36a5fe2828527bd32d66fa vault.wasm +c3c8b876ab02edc59110580470a0840974bc37ca1cfe04f895a23fa30b98614a cctp_adapter.wasm +1aa378952a7ea3581f47d59822ef6b656e2f01e218f6ad83e1353a5aa50a5f9d claim.wasm +d1248b2e3061ec4c7fb54e9725d9cd1fbcaf5c12b90756d77b70714af19d2c4b control_center.wasm +1723a6b619c482f886f3d49a3264d8cdeecd67b4e6c112db52eabc834aee1278 d_token_info_provider.wasm +3bd0f6d3f1e604479c75e25061ca239747eedc590adbdfe00d6e1c88723b1f76 dao_voting_adapter.wasm +56b8837b21c9abfa6837d3d8ede380a660bfb5841380a0c242c9230da9b5f84b gatekeeper.wasm +3d32d354e72d235301f313f2d23c374fcea55207f52a65f183d84febe9479933 hydro.wasm +833aa55a52afae315f6cbcc490fa5f468fe11462a30adf042e7b7f602056d30e ibc_adapter.wasm +5ca57a70eaf69e7f92aaa3739799fc7c043874216c6e924c94cccb273c3310ba liquid_collateral.wasm +1ed52471fd9f4fb88be4ba021a53ce7ecbab1dfa3788f9dd596bde230a03e45d lsm_token_info_provider.wasm +dfe70e515ce38d6cb44f3a8b9d398f82988524f58e1ba8eb21f4bff297c7e7bb marketplace.wasm +24fe99f9b50a9e35466d55377e6db9056304dcb5511c7cec871bfd607165adbc mars_adapter.wasm +78db0c847199c3cdc686db64eb5a8d661d848a529abba043b88a28d7bd9149f5 proxy.wasm +1b885c15bbe9de8ca8d2f13e32f8736787a1ac585d83c3e1000cb8c4ac6f1030 skip_adapter.wasm +7ccb9370aeadb7b651592713f7dc13747896e72f8a19a933a40ae541b7137c14 st_token_info_provider.wasm +ea2c1da6dbedacd8f32b098905dd5790b095075a1a7457bfbf123c86465987eb tribute.wasm +f904e2d69a8c43984590ddba3855079754d09a7e09b76c1557d89d6e96e8199d user_registry.wasm +13f741a2d18ef5f6f6bf71521ecd25947b7e840886d102cec507b0818419a9eb vault.wasm diff --git a/artifacts/claim.wasm b/artifacts/claim.wasm index 286dc233..44a34350 100644 Binary files a/artifacts/claim.wasm and b/artifacts/claim.wasm differ diff --git a/artifacts/control_center.wasm b/artifacts/control_center.wasm index ce083e2a..ed3b755f 100644 Binary files a/artifacts/control_center.wasm and b/artifacts/control_center.wasm differ diff --git a/artifacts/d_token_info_provider.wasm b/artifacts/d_token_info_provider.wasm index 86c451f7..064c2a6f 100644 Binary files a/artifacts/d_token_info_provider.wasm and b/artifacts/d_token_info_provider.wasm differ diff --git a/artifacts/dao_voting_adapter.wasm b/artifacts/dao_voting_adapter.wasm index 02f9dfd5..1b7dd631 100644 Binary files a/artifacts/dao_voting_adapter.wasm and b/artifacts/dao_voting_adapter.wasm differ diff --git a/artifacts/gatekeeper.wasm b/artifacts/gatekeeper.wasm index b5561602..fab63ea5 100644 Binary files a/artifacts/gatekeeper.wasm and b/artifacts/gatekeeper.wasm differ diff --git a/artifacts/hydro.wasm b/artifacts/hydro.wasm index 3330294f..c53b8a9e 100644 Binary files a/artifacts/hydro.wasm and b/artifacts/hydro.wasm differ diff --git a/artifacts/ibc_adapter.wasm b/artifacts/ibc_adapter.wasm index 51320c22..92e960a2 100644 Binary files a/artifacts/ibc_adapter.wasm and b/artifacts/ibc_adapter.wasm differ diff --git a/artifacts/liquid_collateral.wasm b/artifacts/liquid_collateral.wasm index 7cfeb80c..a1b4244c 100755 Binary files a/artifacts/liquid_collateral.wasm and b/artifacts/liquid_collateral.wasm differ diff --git a/artifacts/lsm_token_info_provider.wasm b/artifacts/lsm_token_info_provider.wasm index dbec06f0..452da543 100644 Binary files a/artifacts/lsm_token_info_provider.wasm and b/artifacts/lsm_token_info_provider.wasm differ diff --git a/artifacts/marketplace.wasm b/artifacts/marketplace.wasm index a9624f49..b5d8247c 100644 Binary files a/artifacts/marketplace.wasm and b/artifacts/marketplace.wasm differ diff --git a/artifacts/mars_adapter.wasm b/artifacts/mars_adapter.wasm index 229797a2..46e56e33 100644 Binary files a/artifacts/mars_adapter.wasm and b/artifacts/mars_adapter.wasm differ diff --git a/artifacts/proxy.wasm b/artifacts/proxy.wasm index d3f6ba94..8ccf350b 100644 Binary files a/artifacts/proxy.wasm and b/artifacts/proxy.wasm differ diff --git a/artifacts/skip_adapter.wasm b/artifacts/skip_adapter.wasm index 8338e371..adb1ed8e 100644 Binary files a/artifacts/skip_adapter.wasm and b/artifacts/skip_adapter.wasm differ diff --git a/artifacts/st_token_info_provider.wasm b/artifacts/st_token_info_provider.wasm index 57c89263..cf7626c6 100644 Binary files a/artifacts/st_token_info_provider.wasm and b/artifacts/st_token_info_provider.wasm differ diff --git a/artifacts/tribute.wasm b/artifacts/tribute.wasm index 5f3b59ad..67313ca6 100644 Binary files a/artifacts/tribute.wasm and b/artifacts/tribute.wasm differ diff --git a/artifacts/user_registry.wasm b/artifacts/user_registry.wasm index 37e4d013..dcda6bab 100644 Binary files a/artifacts/user_registry.wasm and b/artifacts/user_registry.wasm differ diff --git a/artifacts/vault.wasm b/artifacts/vault.wasm index 63b0ac71..fc7b42cd 100644 Binary files a/artifacts/vault.wasm and b/artifacts/vault.wasm differ diff --git a/contracts/claim/schema/claim.json b/contracts/claim/schema/claim.json index 3cb96328..9af04c44 100644 --- a/contracts/claim/schema/claim.json +++ b/contracts/claim/schema/claim.json @@ -1,6 +1,6 @@ { "contract_name": "claim", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/dao-voting-adapter/schema/dao-voting-adapter.json b/contracts/dao-voting-adapter/schema/dao-voting-adapter.json index 29f2ba93..9d80b63d 100644 --- a/contracts/dao-voting-adapter/schema/dao-voting-adapter.json +++ b/contracts/dao-voting-adapter/schema/dao-voting-adapter.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-adapter", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/gatekeeper/schema/gatekeeper.json b/contracts/gatekeeper/schema/gatekeeper.json index b788388a..1d86b2d4 100644 --- a/contracts/gatekeeper/schema/gatekeeper.json +++ b/contracts/gatekeeper/schema/gatekeeper.json @@ -1,6 +1,6 @@ { "contract_name": "gatekeeper", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/hydro/schema/hydro.json b/contracts/hydro/schema/hydro.json index da5d50d2..4bd8c96d 100644 --- a/contracts/hydro/schema/hydro.json +++ b/contracts/hydro/schema/hydro.json @@ -1,6 +1,6 @@ { "contract_name": "hydro", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -623,6 +623,48 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "add_whitelist_admin" + ], + "properties": { + "add_whitelist_admin": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_whitelist_admin" + ], + "properties": { + "remove_whitelist_admin": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ diff --git a/contracts/hydro/schema/raw/execute.json b/contracts/hydro/schema/raw/execute.json index 56c892f5..678b1405 100644 --- a/contracts/hydro/schema/raw/execute.json +++ b/contracts/hydro/schema/raw/execute.json @@ -299,6 +299,48 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "add_whitelist_admin" + ], + "properties": { + "add_whitelist_admin": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_whitelist_admin" + ], + "properties": { + "remove_whitelist_admin": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ diff --git a/contracts/hydro/src/contract.rs b/contracts/hydro/src/contract.rs index f79c2273..c4587b0d 100644 --- a/contracts/hydro/src/contract.rs +++ b/contracts/hydro/src/contract.rs @@ -284,6 +284,10 @@ pub fn execute( ExecuteMsg::RemoveAccountFromWhitelist { address } => { remove_from_whitelist(deps, env, info, address) } + ExecuteMsg::AddWhitelistAdmin { address } => add_whitelist_admin(deps, env, info, address), + ExecuteMsg::RemoveWhitelistAdmin { address } => { + remove_whitelist_admin(deps, env, info, address) + } ExecuteMsg::UpdateConfig { config } => update_config(deps, env, info, config), ExecuteMsg::DeleteConfigs { timestamps } => delete_configs(deps, &env, info, timestamps), ExecuteMsg::Pause {} => pause_contract(deps, &env, info), @@ -2012,6 +2016,66 @@ fn remove_from_whitelist( .add_attribute("removed_whitelist_address", whitelist_account_addr)) } +// Adds a new address to the whitelist admins. +fn add_whitelist_admin( + deps: DepsMut, + _env: Env, + info: MessageInfo, + address: String, +) -> Result { + validate_sender_is_whitelist_admin(&deps, &info)?; + + let mut whitelist_admins = WHITELIST_ADMINS.load(deps.storage)?; + let new_admin_addr = deps.api.addr_validate(&address)?; + + if whitelist_admins.contains(&new_admin_addr) { + return Err(ContractError::Std(StdError::generic_err( + "Address is already a whitelist admin", + ))); + } + + whitelist_admins.push(new_admin_addr.clone()); + WHITELIST_ADMINS.save(deps.storage, &whitelist_admins)?; + + Ok(Response::new() + .add_attribute("action", "add_whitelist_admin") + .add_attribute("sender", info.sender) + .add_attribute("added_admin", new_admin_addr)) +} + +// Removes an address from the whitelist admins. +fn remove_whitelist_admin( + deps: DepsMut, + _env: Env, + info: MessageInfo, + address: String, +) -> Result { + validate_sender_is_whitelist_admin(&deps, &info)?; + + let mut whitelist_admins = WHITELIST_ADMINS.load(deps.storage)?; + let admin_addr = deps.api.addr_validate(&address)?; + + if !whitelist_admins.contains(&admin_addr) { + return Err(ContractError::Std(StdError::generic_err( + "Address is not a whitelist admin", + ))); + } + + if whitelist_admins.len() <= 1 { + return Err(ContractError::Std(StdError::generic_err( + "Cannot remove the last whitelist admin", + ))); + } + + whitelist_admins.retain(|a| a != admin_addr); + WHITELIST_ADMINS.save(deps.storage, &whitelist_admins)?; + + Ok(Response::new() + .add_attribute("action", "remove_whitelist_admin") + .add_attribute("sender", info.sender) + .add_attribute("removed_admin", admin_addr)) +} + fn update_config( deps: DepsMut, env: Env, diff --git a/contracts/hydro/src/migration/migrate.rs b/contracts/hydro/src/migration/migrate.rs index d128f5fc..0d7373dc 100644 --- a/contracts/hydro/src/migration/migrate.rs +++ b/contracts/hydro/src/migration/migrate.rs @@ -16,7 +16,7 @@ pub struct MigrateMsg {} pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { check_contract_version(deps.storage)?; - // No state migrations needed from v3.6.9 to v3.6.10 + // No state migrations needed from v3.6.10 to v3.6.11 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; diff --git a/contracts/hydro/src/msg.rs b/contracts/hydro/src/msg.rs index 51df3418..0d888981 100644 --- a/contracts/hydro/src/msg.rs +++ b/contracts/hydro/src/msg.rs @@ -166,6 +166,12 @@ pub enum ExecuteMsg { RemoveAccountFromWhitelist { address: String, }, + AddWhitelistAdmin { + address: String, + }, + RemoveWhitelistAdmin { + address: String, + }, UpdateConfig { config: UpdateConfigData, }, diff --git a/contracts/hydro/src/testing.rs b/contracts/hydro/src/testing.rs index 074a764b..140ab31b 100644 --- a/contracts/hydro/src/testing.rs +++ b/contracts/hydro/src/testing.rs @@ -14,6 +14,7 @@ use crate::state::{ VOTE_MAP_V2, }; +use crate::error::ContractError; use crate::testing_mocks::{ contract_info_mock, denom_trace_grpc_query_mock, mock_dependencies, no_op_grpc_query_mock, token_info_providers_mock, MockQuerier, MockWasmQuerier, RoundsValidators, WasmQueryFunc, @@ -77,6 +78,7 @@ pub const THREE_MONTHS_IN_NANO_SECONDS: u64 = 3 * ONE_MONTH_IN_NANO_SECONDS; pub const DERIVATIVE_TOKEN_PROVIDER_ADDR: &str = "derivative_token_info_provider"; pub const LSM_TOKEN_PROVIDER_ADDR: &str = "lsm_token_info_provider"; +pub const DEFAULT_WHITELIST_ADMIN: &str = "admin"; pub fn get_default_power_schedule_vec() -> Vec<(u64, Decimal)> { vec![ @@ -129,6 +131,7 @@ pub fn build_reply_msg(payload: Binary, encoded_msg_data: Vec) -> Reply { pub fn get_default_instantiate_msg(mock_api: &MockApi) -> InstantiateMsg { let user_address = get_address_as_str(mock_api, "addr0000"); + let whitelist_admin_address = get_address_as_str(mock_api, DEFAULT_WHITELIST_ADMIN); let slashed_tokens_receiver_address = get_address_as_str(mock_api, "addr0000"); InstantiateMsg { @@ -141,7 +144,7 @@ pub fn get_default_instantiate_msg(mock_api: &MockApi) -> InstantiateMsg { first_round_start: mock_env().block.time, max_locked_tokens: Uint128::new(1000000), initial_whitelist: vec![user_address.clone()], - whitelist_admins: vec![], + whitelist_admins: vec![whitelist_admin_address], max_deployment_duration: 12, round_lock_power_schedule: get_default_power_schedule_vec(), token_info_providers: vec![get_default_lsm_token_info_provider_init_msg()], @@ -3780,3 +3783,182 @@ fn test_set_gatekeeper() { } } } + +// ============================================================================ +// WHITELIST ADMIN MANAGEMENT TESTS +// ============================================================================ + +#[test] +fn test_add_whitelist_admin_success() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let msg = get_default_instantiate_msg(&deps.api); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let existing_admin = get_address_as_str(&deps.api, DEFAULT_WHITELIST_ADMIN); + let new_admin = get_address_as_str(&deps.api, "new_admin"); + let info = get_message_info(&deps.api, DEFAULT_WHITELIST_ADMIN, &[]); + let msg = ExecuteMsg::AddWhitelistAdmin { + address: new_admin.clone(), + }; + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(res.attributes[0].value, "add_whitelist_admin"); + + let admins = query_whitelist_admins(deps.as_ref()).unwrap().admins; + assert!(admins.iter().any(|a| a.as_str() == new_admin)); + assert!(admins.iter().any(|a| a.as_str() == existing_admin)); +} + +#[test] +fn test_add_whitelist_admin_unauthorized() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let msg = get_default_instantiate_msg(&deps.api); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let new_admin = get_address_as_str(&deps.api, "new_admin"); + let info = get_message_info(&deps.api, "non_admin", &[]); + let msg = ExecuteMsg::AddWhitelistAdmin { address: new_admin }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Unauthorized)); +} + +#[test] +fn test_add_whitelist_admin_duplicate() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let msg = get_default_instantiate_msg(&deps.api); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let existing_admin = get_address_as_str(&deps.api, DEFAULT_WHITELIST_ADMIN); + let info = get_message_info(&deps.api, DEFAULT_WHITELIST_ADMIN, &[]); + let msg = ExecuteMsg::AddWhitelistAdmin { + address: existing_admin, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(_))); + assert!(err + .to_string() + .contains("Address is already a whitelist admin")); +} + +#[test] +fn test_remove_whitelist_admin_success() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let mut msg = get_default_instantiate_msg(&deps.api); + let admin2 = get_address_as_str(&deps.api, "admin2"); + msg.whitelist_admins.push(admin2.clone()); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let admin1 = get_address_as_str(&deps.api, DEFAULT_WHITELIST_ADMIN); + let info = get_message_info(&deps.api, DEFAULT_WHITELIST_ADMIN, &[]); + let msg = ExecuteMsg::RemoveWhitelistAdmin { + address: admin2.clone(), + }; + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_whitelist_admin"); + + let admins = query_whitelist_admins(deps.as_ref()).unwrap().admins; + assert_eq!(admins.len(), 1); + assert_eq!(admins[0].as_str(), admin1); +} + +#[test] +fn test_remove_whitelist_admin_unauthorized() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let mut msg = get_default_instantiate_msg(&deps.api); + let admin2 = get_address_as_str(&deps.api, "admin2"); + msg.whitelist_admins.push(admin2.clone()); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let info = get_message_info(&deps.api, "non_admin", &[]); + let msg = ExecuteMsg::RemoveWhitelistAdmin { address: admin2 }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Unauthorized)); +} + +#[test] +fn test_remove_whitelist_admin_not_found() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let mut msg = get_default_instantiate_msg(&deps.api); + let admin2 = get_address_as_str(&deps.api, "admin2"); + msg.whitelist_admins.push(admin2); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let non_existent = get_address_as_str(&deps.api, "non_existent"); + let info = get_message_info(&deps.api, DEFAULT_WHITELIST_ADMIN, &[]); + let msg = ExecuteMsg::RemoveWhitelistAdmin { + address: non_existent, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(_))); + assert!(err.to_string().contains("Address is not a whitelist admin")); +} + +#[test] +fn test_remove_last_whitelist_admin_fails() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let msg = get_default_instantiate_msg(&deps.api); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let admin = get_address_as_str(&deps.api, DEFAULT_WHITELIST_ADMIN); + let info = get_message_info(&deps.api, DEFAULT_WHITELIST_ADMIN, &[]); + let msg = ExecuteMsg::RemoveWhitelistAdmin { address: admin }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(_))); + assert!(err + .to_string() + .contains("Cannot remove the last whitelist admin")); +} + +#[test] +fn test_new_admin_can_perform_admin_actions() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let msg = get_default_instantiate_msg(&deps.api); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let new_admin = get_address_as_str(&deps.api, "new_admin"); + let info = get_message_info(&deps.api, DEFAULT_WHITELIST_ADMIN, &[]); + let add_msg = ExecuteMsg::AddWhitelistAdmin { + address: new_admin.clone(), + }; + execute(deps.as_mut(), env.clone(), info, add_msg).unwrap(); + + // New admin can add someone to the proposal whitelist + let new_proposer = get_address_as_str(&deps.api, "new_proposer"); + let info = get_message_info(&deps.api, "new_admin", &[]); + let whitelist_msg = ExecuteMsg::AddAccountToWhitelist { + address: new_proposer.clone(), + }; + let res = execute(deps.as_mut(), env, info, whitelist_msg).unwrap(); + assert_eq!(res.attributes[0].value, "add_to_whitelist"); +} + +#[test] +fn test_removed_admin_loses_access() { + let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env()); + let info = get_message_info(&deps.api, "addr0000", &[]); + let mut msg = get_default_instantiate_msg(&deps.api); + let admin2 = get_address_as_str(&deps.api, "admin2"); + msg.whitelist_admins.push(admin2.clone()); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // admin2 removes itself + let info = get_message_info(&deps.api, "admin2", &[]); + let remove_msg = ExecuteMsg::RemoveWhitelistAdmin { + address: admin2.clone(), + }; + execute(deps.as_mut(), env.clone(), info, remove_msg).unwrap(); + + // admin2 can no longer add to whitelist + let some_addr = get_address_as_str(&deps.api, "some_addr"); + let info = get_message_info(&deps.api, "admin2", &[]); + let try_add = ExecuteMsg::AddWhitelistAdmin { address: some_addr }; + let err = execute(deps.as_mut(), env, info, try_add).unwrap_err(); + assert!(matches!(err, ContractError::Unauthorized)); +} diff --git a/contracts/inflow/cctp-adapter/schema/cctp-adapter.json b/contracts/inflow/cctp-adapter/schema/cctp-adapter.json index 755bcd63..4e1c1d68 100644 --- a/contracts/inflow/cctp-adapter/schema/cctp-adapter.json +++ b/contracts/inflow/cctp-adapter/schema/cctp-adapter.json @@ -1,6 +1,6 @@ { "contract_name": "cctp-adapter", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -357,6 +357,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -464,50 +508,6 @@ }, "additionalProperties": false }, - { - "description": "Add a new admin (admin only)", - "type": "object", - "required": [ - "add_admin" - ], - "properties": { - "add_admin": { - "type": "object", - "required": [ - "admin_address" - ], - "properties": { - "admin_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Remove an admin (admin only)", - "type": "object", - "required": [ - "remove_admin" - ], - "properties": { - "remove_admin": { - "type": "object", - "required": [ - "admin_address" - ], - "properties": { - "admin_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Register or update chain configuration (config admin only)", "type": "object", @@ -956,6 +956,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -1012,20 +1026,6 @@ }, "additionalProperties": false }, - { - "description": "Get list of all admins", - "type": "object", - "required": [ - "admins" - ], - "properties": { - "admins": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Get depositor capabilities", "type": "object", diff --git a/contracts/inflow/cctp-adapter/schema/raw/execute.json b/contracts/inflow/cctp-adapter/schema/raw/execute.json index 0a99b988..2d04cac6 100644 --- a/contracts/inflow/cctp-adapter/schema/raw/execute.json +++ b/contracts/inflow/cctp-adapter/schema/raw/execute.json @@ -158,6 +158,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -265,50 +309,6 @@ }, "additionalProperties": false }, - { - "description": "Add a new admin (admin only)", - "type": "object", - "required": [ - "add_admin" - ], - "properties": { - "add_admin": { - "type": "object", - "required": [ - "admin_address" - ], - "properties": { - "admin_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Remove an admin (admin only)", - "type": "object", - "required": [ - "remove_admin" - ], - "properties": { - "remove_admin": { - "type": "object", - "required": [ - "admin_address" - ], - "properties": { - "admin_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Register or update chain configuration (config admin only)", "type": "object", diff --git a/contracts/inflow/cctp-adapter/schema/raw/query.json b/contracts/inflow/cctp-adapter/schema/raw/query.json index 04304b96..8ddaba6c 100644 --- a/contracts/inflow/cctp-adapter/schema/raw/query.json +++ b/contracts/inflow/cctp-adapter/schema/raw/query.json @@ -209,6 +209,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -265,20 +279,6 @@ }, "additionalProperties": false }, - { - "description": "Get list of all admins", - "type": "object", - "required": [ - "admins" - ], - "properties": { - "admins": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Get depositor capabilities", "type": "object", diff --git a/contracts/inflow/cctp-adapter/src/contract.rs b/contracts/inflow/cctp-adapter/src/contract.rs index f75dc3ea..8e8afe8d 100644 --- a/contracts/inflow/cctp-adapter/src/contract.rs +++ b/contracts/inflow/cctp-adapter/src/contract.rs @@ -28,8 +28,8 @@ use crate::validation::{ validate_depositor_caller, validate_executor_caller, }; -const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -194,6 +194,12 @@ fn dispatch_execute_standard( depositor_address, enabled, } => execute_set_depositor_enabled(deps, info, depositor_address, enabled), + AdapterInterfaceMsg::AddAdmin { admin_address } => { + execute_add_admin(deps, info, admin_address) + } + AdapterInterfaceMsg::RemoveAdmin { admin_address } => { + execute_remove_admin(deps, info, admin_address) + } } } @@ -216,10 +222,6 @@ fn dispatch_execute_custom( CctpAdapterMsg::RemoveExecutor { executor_address } => { execute_remove_executor(deps, info, executor_address) } - CctpAdapterMsg::AddAdmin { admin_address } => execute_add_admin(deps, info, admin_address), - CctpAdapterMsg::RemoveAdmin { admin_address } => { - execute_remove_admin(deps, info, admin_address) - } CctpAdapterMsg::RegisterChain { chain_config } => { execute_register_chain(deps, info, chain_config) } @@ -845,6 +847,7 @@ fn dispatch_query_standard( AdapterInterfaceQueryMsg::RegisteredDepositors { enabled } => { to_json_binary(&query_registered_depositors(deps, enabled)?) } + AdapterInterfaceQueryMsg::Admins {} => to_json_binary(&query_admins(deps)?), } } @@ -856,7 +859,6 @@ fn dispatch_query_custom(deps: Deps, msg: CctpAdapterQueryMsg) -> } CctpAdapterQueryMsg::AllChains {} => to_json_binary(&query_all_chains(deps)?), CctpAdapterQueryMsg::Executors {} => to_json_binary(&query_executors(deps)?), - CctpAdapterQueryMsg::Admins {} => to_json_binary(&query_admins(deps)?), CctpAdapterQueryMsg::DepositorCapabilities { depositor_address } => { to_json_binary(&query_depositor_capabilities(deps, depositor_address)?) } diff --git a/contracts/inflow/cctp-adapter/src/lib.rs b/contracts/inflow/cctp-adapter/src/lib.rs index a3d2ac2c..9a277752 100644 --- a/contracts/inflow/cctp-adapter/src/lib.rs +++ b/contracts/inflow/cctp-adapter/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; pub mod error; pub mod ibc; +pub mod migration; pub mod msg; pub mod noble; pub mod state; diff --git a/contracts/inflow/cctp-adapter/src/migration/migrate.rs b/contracts/inflow/cctp-adapter/src/migration/migrate.rs new file mode 100644 index 00000000..91b9e848 --- /dev/null +++ b/contracts/inflow/cctp-adapter/src/migration/migrate.rs @@ -0,0 +1,41 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DepsMut, Env, Response, StdError}; +use cw2::{get_contract_version, set_contract_version}; +// entry_point is being used but for some reason clippy doesn't see that, hence the allow attribute here +#[allow(unused_imports)] +use cosmwasm_std::entry_point; +use neutron_sdk::bindings::msg::NeutronMsg; +use neutron_sdk::bindings::query::NeutronQuery; + +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::error::ContractError; + +#[cw_serde] +pub struct MigrateMsg {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> Result, ContractError> { + check_contract_version(deps.storage)?; + + // No state migrations needed from v3.6.10 to v3.6.11 + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new()) +} + +fn check_contract_version(storage: &dyn cosmwasm_std::Storage) -> Result<(), ContractError> { + let contract_version = get_contract_version(storage)?; + + if contract_version.version == CONTRACT_VERSION { + return Err(ContractError::Std(StdError::generic_err( + "Contract is already migrated to the newest version.", + ))); + } + + Ok(()) +} diff --git a/contracts/inflow/cctp-adapter/src/migration/mod.rs b/contracts/inflow/cctp-adapter/src/migration/mod.rs new file mode 100644 index 00000000..2bec015c --- /dev/null +++ b/contracts/inflow/cctp-adapter/src/migration/mod.rs @@ -0,0 +1,4 @@ +pub mod migrate; + +#[cfg(test)] +mod testing; diff --git a/contracts/inflow/cctp-adapter/src/migration/testing.rs b/contracts/inflow/cctp-adapter/src/migration/testing.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/inflow/cctp-adapter/src/migration/testing.rs @@ -0,0 +1 @@ + diff --git a/contracts/inflow/cctp-adapter/src/msg.rs b/contracts/inflow/cctp-adapter/src/msg.rs index f35c768c..e43ae778 100644 --- a/contracts/inflow/cctp-adapter/src/msg.rs +++ b/contracts/inflow/cctp-adapter/src/msg.rs @@ -5,9 +5,9 @@ use crate::state::{ChainConfig, Config, DepositorCapabilities, TransferFundsInst // Re-export adapter interface types and response types pub use interface::inflow_adapter::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllPositionsResponse, AvailableAmountResponse, - DepositorPositionResponse, DepositorPositionsResponse, RegisteredDepositorInfo, - RegisteredDepositorsResponse, TimeEstimateResponse, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllPositionsResponse, + AvailableAmountResponse, DepositorPositionResponse, DepositorPositionsResponse, + RegisteredDepositorInfo, RegisteredDepositorsResponse, TimeEstimateResponse, }; /// Initial depositor configuration for instantiation #[cw_serde] @@ -83,12 +83,6 @@ pub enum CctpAdapterMsg { /// Remove an executor (config admin only) RemoveExecutor { executor_address: String }, - /// Add a new admin (admin only) - AddAdmin { admin_address: String }, - - /// Remove an admin (admin only) - RemoveAdmin { admin_address: String }, - /// Register or update chain configuration (config admin only) RegisterChain { chain_config: ChainConfig }, @@ -139,10 +133,6 @@ pub enum CctpAdapterQueryMsg { #[returns(ExecutorsResponse)] Executors {}, - /// Get list of all admins - #[returns(AdminsResponse)] - Admins {}, - /// Get depositor capabilities #[returns(DepositorCapabilitiesResponse)] DepositorCapabilities { depositor_address: String }, @@ -192,8 +182,3 @@ pub struct DepositorCapabilitiesResponse { pub struct AllowedDestinationAddressesResponse { pub addresses: Vec, } - -#[cw_serde] -pub struct AdminsResponse { - pub admins: Vec, -} diff --git a/contracts/inflow/cctp-adapter/src/testing_custom_adapter.rs b/contracts/inflow/cctp-adapter/src/testing_custom_adapter.rs index 033a54e0..dd76eee7 100644 --- a/contracts/inflow/cctp-adapter/src/testing_custom_adapter.rs +++ b/contracts/inflow/cctp-adapter/src/testing_custom_adapter.rs @@ -142,213 +142,6 @@ mod custom_adapter_tests { ); } - // ============================================================================ - // ADMIN MANAGEMENT TESTS - // ============================================================================ - - #[test] - fn test_add_admin_success() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - let info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - - let msg = ExecuteMsg::CustomAction(CctpAdapterMsg::AddAdmin { - admin_address: test_data.admin2.to_string(), - }); - - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - assert_eq!(res.attributes[0].value, "add_admin"); - assert_eq!(res.attributes[2].value, test_data.admin2.to_string()); - - // Verify admin was added - let query_msg = QueryMsg::CustomQuery(CctpAdapterQueryMsg::Admins {}); - let res = query(deps.as_ref(), env, query_msg).unwrap(); - let admins: AdminsResponse = from_json(&res).unwrap(); - assert_eq!(admins.admins.len(), 2); - } - - #[test] - fn test_add_admin_unauthorized() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - let info = MessageInfo { - sender: test_data.non_admin.clone(), - funds: vec![], - }; - - let msg = ExecuteMsg::CustomAction(CctpAdapterMsg::AddAdmin { - admin_address: test_data.admin2.to_string(), - }); - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::UnauthorizedAdmin {}); - } - - #[test] - fn test_add_admin_duplicate() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - let info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - - let msg = ExecuteMsg::CustomAction(CctpAdapterMsg::AddAdmin { - admin_address: test_data.admin.to_string(), - }); - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::AdminAlreadyExists { .. })); - } - - #[test] - fn test_remove_admin_success() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - // First add a second admin - let add_info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - let add_msg = ExecuteMsg::CustomAction(CctpAdapterMsg::AddAdmin { - admin_address: test_data.admin2.to_string(), - }); - execute(deps.as_mut(), env.clone(), add_info, add_msg).unwrap(); - - // Now remove the first admin - let remove_info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - let remove_msg = ExecuteMsg::CustomAction(CctpAdapterMsg::RemoveAdmin { - admin_address: test_data.admin.to_string(), - }); - - let res = execute(deps.as_mut(), env.clone(), remove_info, remove_msg).unwrap(); - assert_eq!(res.attributes[0].value, "remove_admin"); - - // Verify admin was removed - let query_msg = QueryMsg::CustomQuery(CctpAdapterQueryMsg::Admins {}); - let res = query(deps.as_ref(), env, query_msg).unwrap(); - let admins: AdminsResponse = from_json(&res).unwrap(); - assert_eq!(admins.admins.len(), 1); - assert_eq!(admins.admins[0], test_data.admin2.to_string()); - } - - #[test] - fn test_remove_admin_unauthorized() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - let info = MessageInfo { - sender: test_data.non_admin.clone(), - funds: vec![], - }; - - let msg = ExecuteMsg::CustomAction(CctpAdapterMsg::RemoveAdmin { - admin_address: test_data.admin.to_string(), - }); - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::UnauthorizedAdmin {}); - } - - #[test] - fn test_remove_admin_not_found() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - let info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - - let msg = ExecuteMsg::CustomAction(CctpAdapterMsg::RemoveAdmin { - admin_address: test_data.non_admin.to_string(), - }); - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::AdminNotFound { .. })); - } - - #[test] - fn test_remove_last_admin_fails() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - let info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - - let msg = ExecuteMsg::CustomAction(CctpAdapterMsg::RemoveAdmin { - admin_address: test_data.admin.to_string(), - }); - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::CannotRemoveLastAdmin {}); - } - - #[test] - fn test_admin_self_removal() { - let (mut deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - // Add second admin - let add_info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - let add_msg = ExecuteMsg::CustomAction(CctpAdapterMsg::AddAdmin { - admin_address: test_data.admin2.to_string(), - }); - execute(deps.as_mut(), env.clone(), add_info, add_msg).unwrap(); - - // Admin removes themselves - let remove_info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - let remove_msg = ExecuteMsg::CustomAction(CctpAdapterMsg::RemoveAdmin { - admin_address: test_data.admin.to_string(), - }); - - let res = execute(deps.as_mut(), env.clone(), remove_info, remove_msg).unwrap(); - assert_eq!(res.attributes[0].value, "remove_admin"); - - // Verify they can no longer perform admin actions - let try_add_info = MessageInfo { - sender: test_data.admin.clone(), - funds: vec![], - }; - let try_add_msg = ExecuteMsg::CustomAction(CctpAdapterMsg::AddAdmin { - admin_address: deps.api.addr_make("new_admin").to_string(), - }); - - let err = execute(deps.as_mut(), env, try_add_info, try_add_msg).unwrap_err(); - assert_eq!(err, ContractError::UnauthorizedAdmin {}); - } - - #[test] - fn test_query_admins() { - let (deps, test_data) = setup_contract_with_defaults(); - let env = mock_env(); - - let msg = QueryMsg::CustomQuery(CctpAdapterQueryMsg::Admins {}); - let res = query(deps.as_ref(), env, msg).unwrap(); - let admins: AdminsResponse = from_json(&res).unwrap(); - - assert_eq!(admins.admins.len(), 1); - assert_eq!(admins.admins[0], test_data.admin.to_string()); - } - // ============================================================================ // CHAIN MANAGEMENT TESTS // ============================================================================ diff --git a/contracts/inflow/cctp-adapter/src/testing_standard_adapter.rs b/contracts/inflow/cctp-adapter/src/testing_standard_adapter.rs index 7350b6f4..70f4819c 100644 --- a/contracts/inflow/cctp-adapter/src/testing_standard_adapter.rs +++ b/contracts/inflow/cctp-adapter/src/testing_standard_adapter.rs @@ -86,7 +86,7 @@ mod standard_adapter_tests { assert!(res.messages.is_empty()); // Query admins to verify deduplication - let query_msg = QueryMsg::CustomQuery(CctpAdapterQueryMsg::Admins {}); + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); let res = query(deps.as_ref(), env, query_msg).unwrap(); let admins_response: AdminsResponse = from_json(&res).unwrap(); assert_eq!(admins_response.admins.len(), 1); @@ -669,4 +669,211 @@ mod standard_adapter_tests { assert!(caps.capabilities.can_withdraw); } + + // ============================================================================ + // ADMIN MANAGEMENT TESTS + // ============================================================================ + + #[test] + fn test_add_admin_success() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: test_data.admin2.to_string(), + }); + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(res.attributes[0].value, "add_admin"); + assert_eq!(res.attributes[2].value, test_data.admin2.to_string()); + + // Verify admin was added + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 2); + } + + #[test] + fn test_add_admin_unauthorized() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.non_admin.clone(), + funds: vec![], + }; + + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: test_data.admin2.to_string(), + }); + + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_add_admin_duplicate() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: test_data.admin.to_string(), + }); + + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminAlreadyExists { .. })); + } + + #[test] + fn test_remove_admin_success() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + // First add a second admin + let add_info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: test_data.admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), add_info, add_msg).unwrap(); + + // Now remove the first admin + let remove_info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + + let res = execute(deps.as_mut(), env.clone(), remove_info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + // Verify admin was removed + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], test_data.admin2.to_string()); + } + + #[test] + fn test_remove_admin_unauthorized() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.non_admin.clone(), + funds: vec![], + }; + + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_remove_admin_not_found() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.non_admin.to_string(), + }); + + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminNotFound { .. })); + } + + #[test] + fn test_remove_last_admin_fails() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotRemoveLastAdmin {}); + } + + #[test] + fn test_admin_self_removal() { + let (mut deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + // Add second admin + let add_info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: test_data.admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), add_info, add_msg).unwrap(); + + // Admin removes themselves + let remove_info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + + let res = execute(deps.as_mut(), env.clone(), remove_info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + // Verify they can no longer perform admin actions + let try_add_info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let try_add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: deps.api.addr_make("new_admin").to_string(), + }); + + let err = execute(deps.as_mut(), env, try_add_info, try_add_msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_query_admins() { + let (deps, test_data) = setup_contract_with_defaults(); + let env = mock_env(); + + let msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], test_data.admin.to_string()); + } } diff --git a/contracts/inflow/control-center/schema/control-center.json b/contracts/inflow/control-center/schema/control-center.json index 36fe8aae..1c2367b9 100644 --- a/contracts/inflow/control-center/schema/control-center.json +++ b/contracts/inflow/control-center/schema/control-center.json @@ -1,6 +1,6 @@ { "contract_name": "control-center", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/inflow/control-center/src/migration/migrate.rs b/contracts/inflow/control-center/src/migration/migrate.rs index 73aea42e..0bbdd395 100644 --- a/contracts/inflow/control-center/src/migration/migrate.rs +++ b/contracts/inflow/control-center/src/migration/migrate.rs @@ -19,7 +19,7 @@ pub fn migrate( ) -> Result, ContractError> { check_contract_version(deps.storage)?; - // No state migrations needed from v3.6.9 to v3.6.10 + // No state migrations needed from v3.6.10 to v3.6.11 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; diff --git a/contracts/inflow/ibc-adapter/schema/ibc-adapter.json b/contracts/inflow/ibc-adapter/schema/ibc-adapter.json index 5aa354d7..7dedfcc4 100644 --- a/contracts/inflow/ibc-adapter/schema/ibc-adapter.json +++ b/contracts/inflow/ibc-adapter/schema/ibc-adapter.json @@ -1,6 +1,6 @@ { "contract_name": "ibc-adapter", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -336,6 +336,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -863,6 +907,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/ibc-adapter/schema/raw/execute.json b/contracts/inflow/ibc-adapter/schema/raw/execute.json index 87b72ed7..502e7f5c 100644 --- a/contracts/inflow/ibc-adapter/schema/raw/execute.json +++ b/contracts/inflow/ibc-adapter/schema/raw/execute.json @@ -158,6 +158,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/ibc-adapter/schema/raw/query.json b/contracts/inflow/ibc-adapter/schema/raw/query.json index af174a30..818168b0 100644 --- a/contracts/inflow/ibc-adapter/schema/raw/query.json +++ b/contracts/inflow/ibc-adapter/schema/raw/query.json @@ -209,6 +209,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/ibc-adapter/src/contract.rs b/contracts/inflow/ibc-adapter/src/contract.rs index e2eab27d..dcd949d2 100644 --- a/contracts/inflow/ibc-adapter/src/contract.rs +++ b/contracts/inflow/ibc-adapter/src/contract.rs @@ -9,10 +9,10 @@ use neutron_sdk::bindings::query::NeutronQuery; use crate::error::ContractError; use crate::ibc::{calculate_timeout, create_ibc_transfer_msg}; use crate::msg::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllChainsResponse, AllPositionsResponse, - AllTokensResponse, AvailableAmountResponse, ChainConfigResponse, DepositorCapabilitiesResponse, - DepositorPositionResponse, DepositorPositionsResponse, ExecuteMsg, - ExecutorCapabilitiesResponse, ExecutorInfo, ExecutorsResponse, IbcAdapterMsg, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllChainsResponse, + AllPositionsResponse, AllTokensResponse, AvailableAmountResponse, ChainConfigResponse, + DepositorCapabilitiesResponse, DepositorPositionResponse, DepositorPositionsResponse, + ExecuteMsg, ExecutorCapabilitiesResponse, ExecutorInfo, ExecutorsResponse, IbcAdapterMsg, IbcAdapterQueryMsg, IbcConfigResponse, InstantiateMsg, QueryMsg, RegisteredDepositorInfo, RegisteredDepositorsResponse, TimeEstimateResponse, TokenConfigResponse, }; @@ -27,8 +27,8 @@ use crate::validation::{ validate_recipient_for_chain, }; -const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // ========== INSTANTIATE ========== @@ -182,6 +182,12 @@ fn dispatch_execute_standard( depositor_address, enabled, } => execute_set_depositor_enabled(deps, info, depositor_address, enabled), + AdapterInterfaceMsg::AddAdmin { admin_address } => { + execute_add_admin(deps, info, admin_address) + } + AdapterInterfaceMsg::RemoveAdmin { admin_address } => { + execute_remove_admin(deps, info, admin_address) + } } } @@ -527,6 +533,60 @@ fn execute_set_depositor_enabled( .add_attribute("enabled", enabled.to_string())) } +fn execute_add_admin( + deps: DepsMut, + info: MessageInfo, + admin_address: String, +) -> Result, ContractError> { + validate_admin_caller(&deps.as_ref(), &info)?; + + let admin_addr = deps.api.addr_validate(&admin_address)?; + let mut admins = ADMINS.load(deps.storage)?; + + if admins.contains(&admin_addr) { + return Err(ContractError::AdminAlreadyExists { + admin: admin_address, + }); + } + + admins.push(admin_addr.clone()); + ADMINS.save(deps.storage, &admins)?; + + Ok(Response::new() + .add_attribute("action", "add_admin") + .add_attribute("sender", info.sender) + .add_attribute("admin", admin_addr)) +} + +fn execute_remove_admin( + deps: DepsMut, + info: MessageInfo, + admin_address: String, +) -> Result, ContractError> { + validate_admin_caller(&deps.as_ref(), &info)?; + + let admin_addr = deps.api.addr_validate(&admin_address)?; + let mut admins = ADMINS.load(deps.storage)?; + + if !admins.contains(&admin_addr) { + return Err(ContractError::AdminNotFound { + admin: admin_address, + }); + } + + if admins.len() <= 1 { + return Err(ContractError::CannotRemoveLastAdmin {}); + } + + admins.retain(|a| a != admin_addr); + ADMINS.save(deps.storage, &admins)?; + + Ok(Response::new() + .add_attribute("action", "remove_admin") + .add_attribute("sender", info.sender) + .add_attribute("admin", admin_addr)) +} + // ========== QUERY ========== #[cfg_attr(not(feature = "library"), entry_point)] @@ -568,6 +628,7 @@ fn dispatch_query_standard( AdapterInterfaceQueryMsg::RegisteredDepositors { enabled } => { to_json_binary(&query_registered_depositors(deps, enabled)?) } + AdapterInterfaceQueryMsg::Admins {} => to_json_binary(&query_admins(deps)?), } } @@ -675,6 +736,13 @@ fn query_registered_depositors( Ok(RegisteredDepositorsResponse { depositors }) } +fn query_admins(deps: Deps) -> StdResult { + let admins = ADMINS.load(deps.storage)?; + Ok(AdminsResponse { + admins: admins.into_iter().map(|a| a.to_string()).collect(), + }) +} + // ========== IBC ADAPTER CUSTOM EXECUTE HANDLERS ========== fn execute_add_executor( diff --git a/contracts/inflow/ibc-adapter/src/error.rs b/contracts/inflow/ibc-adapter/src/error.rs index 6d32d664..8288173b 100644 --- a/contracts/inflow/ibc-adapter/src/error.rs +++ b/contracts/inflow/ibc-adapter/src/error.rs @@ -51,6 +51,15 @@ pub enum ContractError { #[error("Executor not found: {executor}")] ExecutorNotFound { executor: String }, + #[error("Admin already exists: {admin}")] + AdminAlreadyExists { admin: String }, + + #[error("Admin not found: {admin}")] + AdminNotFound { admin: String }, + + #[error("Cannot remove the last admin")] + CannotRemoveLastAdmin {}, + #[error("Unauthorized - only executors or admins can call this function")] UnauthorizedExecutor {}, diff --git a/contracts/inflow/ibc-adapter/src/lib.rs b/contracts/inflow/ibc-adapter/src/lib.rs index 97297bbd..24056cfb 100644 --- a/contracts/inflow/ibc-adapter/src/lib.rs +++ b/contracts/inflow/ibc-adapter/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; pub mod error; pub mod ibc; +pub mod migration; pub mod msg; pub mod state; pub mod validation; diff --git a/contracts/inflow/ibc-adapter/src/migration/migrate.rs b/contracts/inflow/ibc-adapter/src/migration/migrate.rs new file mode 100644 index 00000000..91b9e848 --- /dev/null +++ b/contracts/inflow/ibc-adapter/src/migration/migrate.rs @@ -0,0 +1,41 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DepsMut, Env, Response, StdError}; +use cw2::{get_contract_version, set_contract_version}; +// entry_point is being used but for some reason clippy doesn't see that, hence the allow attribute here +#[allow(unused_imports)] +use cosmwasm_std::entry_point; +use neutron_sdk::bindings::msg::NeutronMsg; +use neutron_sdk::bindings::query::NeutronQuery; + +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::error::ContractError; + +#[cw_serde] +pub struct MigrateMsg {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> Result, ContractError> { + check_contract_version(deps.storage)?; + + // No state migrations needed from v3.6.10 to v3.6.11 + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new()) +} + +fn check_contract_version(storage: &dyn cosmwasm_std::Storage) -> Result<(), ContractError> { + let contract_version = get_contract_version(storage)?; + + if contract_version.version == CONTRACT_VERSION { + return Err(ContractError::Std(StdError::generic_err( + "Contract is already migrated to the newest version.", + ))); + } + + Ok(()) +} diff --git a/contracts/inflow/ibc-adapter/src/migration/mod.rs b/contracts/inflow/ibc-adapter/src/migration/mod.rs new file mode 100644 index 00000000..2bec015c --- /dev/null +++ b/contracts/inflow/ibc-adapter/src/migration/mod.rs @@ -0,0 +1,4 @@ +pub mod migrate; + +#[cfg(test)] +mod testing; diff --git a/contracts/inflow/ibc-adapter/src/migration/testing.rs b/contracts/inflow/ibc-adapter/src/migration/testing.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/inflow/ibc-adapter/src/migration/testing.rs @@ -0,0 +1 @@ + diff --git a/contracts/inflow/ibc-adapter/src/msg.rs b/contracts/inflow/ibc-adapter/src/msg.rs index e0b33c49..1b571705 100644 --- a/contracts/inflow/ibc-adapter/src/msg.rs +++ b/contracts/inflow/ibc-adapter/src/msg.rs @@ -8,9 +8,9 @@ use crate::state::{ // Re-export adapter interface types and response types pub use interface::inflow_adapter::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllPositionsResponse, AvailableAmountResponse, - DepositorPositionResponse, DepositorPositionsResponse, RegisteredDepositorInfo, - RegisteredDepositorsResponse, TimeEstimateResponse, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllPositionsResponse, + AvailableAmountResponse, DepositorPositionResponse, DepositorPositionsResponse, + RegisteredDepositorInfo, RegisteredDepositorsResponse, TimeEstimateResponse, }; /// Initial depositor configuration for instantiation diff --git a/contracts/inflow/ibc-adapter/src/testing.rs b/contracts/inflow/ibc-adapter/src/testing.rs index be130c3c..bee25058 100644 --- a/contracts/inflow/ibc-adapter/src/testing.rs +++ b/contracts/inflow/ibc-adapter/src/testing.rs @@ -7,8 +7,8 @@ mod tests { use crate::contract::{execute, instantiate, query}; use crate::error::ContractError; use crate::msg::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AvailableAmountResponse, ExecuteMsg, - IbcAdapterMsg, InitialDepositor, InitialExecutor, InstantiateMsg, QueryMsg, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AvailableAmountResponse, + ExecuteMsg, IbcAdapterMsg, InitialDepositor, InitialExecutor, InstantiateMsg, QueryMsg, }; use crate::state::{ ChainConfig, DepositorCapabilities, ExecutorCapabilities, TokenConfig, @@ -404,4 +404,207 @@ mod tests { // Should return MAX for IBC adapter assert_eq!(response.amount, Uint128::MAX); } + + // ============================================================================ + // ADMIN MANAGEMENT TESTS + // ============================================================================ + + #[test] + fn test_add_admin_success() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let admin2 = deps.api.addr_make("admin2"); + let info = get_message_info(&deps.api, ADMIN, &[]); + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(res.attributes[0].value, "add_admin"); + assert_eq!(res.attributes[2].value, admin2.to_string()); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 2); + } + + #[test] + fn test_add_admin_unauthorized() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let non_admin = deps.api.addr_make("non_admin"); + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: non_admin, + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_add_admin_duplicate() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let admin = deps.api.addr_make(ADMIN); + let info = get_message_info(&deps.api, ADMIN, &[]); + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminAlreadyExists { .. })); + } + + #[test] + fn test_remove_admin_success() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let admin2 = deps.api.addr_make("admin2"); + // Add second admin + let info = get_message_info(&deps.api, ADMIN, &[]); + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), info, add_msg).unwrap(); + + // Remove first admin + let admin = deps.api.addr_make(ADMIN); + let info = get_message_info(&deps.api, ADMIN, &[]); + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: admin.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], admin2.to_string()); + } + + #[test] + fn test_remove_admin_unauthorized() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let non_admin = deps.api.addr_make("non_admin"); + let admin = deps.api.addr_make(ADMIN); + let info = MessageInfo { + sender: non_admin, + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_remove_admin_not_found() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let non_admin = deps.api.addr_make("non_admin"); + let info = get_message_info(&deps.api, ADMIN, &[]); + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: non_admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminNotFound { .. })); + } + + #[test] + fn test_remove_last_admin_fails() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let admin = deps.api.addr_make(ADMIN); + let info = get_message_info(&deps.api, ADMIN, &[]); + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotRemoveLastAdmin {}); + } + + #[test] + fn test_admin_self_removal() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let admin2 = deps.api.addr_make("admin2"); + // Add second admin + let info = get_message_info(&deps.api, ADMIN, &[]); + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), info, add_msg).unwrap(); + + // Admin removes themselves + let admin = deps.api.addr_make(ADMIN); + let info = get_message_info(&deps.api, ADMIN, &[]); + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: admin.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + // Verify removed admin can no longer perform admin actions + let new_admin = deps.api.addr_make("new_admin"); + let info = get_message_info(&deps.api, ADMIN, &[]); + let try_add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: new_admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, try_add_msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_query_admins() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = get_default_instantiate_msg(&deps.api); + let info = get_message_info(&deps.api, ADMIN, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], deps.api.addr_make(ADMIN).to_string()); + } } diff --git a/contracts/inflow/mars-adapter/schema/mars-adapter.json b/contracts/inflow/mars-adapter/schema/mars-adapter.json index a04d8155..a59aeee1 100644 --- a/contracts/inflow/mars-adapter/schema/mars-adapter.json +++ b/contracts/inflow/mars-adapter/schema/mars-adapter.json @@ -1,6 +1,6 @@ { "contract_name": "mars-adapter", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -212,6 +212,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -500,6 +544,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/mars-adapter/schema/raw/execute.json b/contracts/inflow/mars-adapter/schema/raw/execute.json index 5e48955a..b219457c 100644 --- a/contracts/inflow/mars-adapter/schema/raw/execute.json +++ b/contracts/inflow/mars-adapter/schema/raw/execute.json @@ -158,6 +158,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/mars-adapter/schema/raw/query.json b/contracts/inflow/mars-adapter/schema/raw/query.json index a3105042..a6274354 100644 --- a/contracts/inflow/mars-adapter/schema/raw/query.json +++ b/contracts/inflow/mars-adapter/schema/raw/query.json @@ -209,6 +209,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/mars-adapter/src/contract.rs b/contracts/inflow/mars-adapter/src/contract.rs index 90d6e7b5..46a3453f 100644 --- a/contracts/inflow/mars-adapter/src/contract.rs +++ b/contracts/inflow/mars-adapter/src/contract.rs @@ -8,17 +8,17 @@ use std::collections::HashMap; use crate::error::ContractError; use crate::mars; use crate::msg::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllPositionsResponse, AvailableAmountResponse, - DepositorPositionResponse, DepositorPositionsResponse, ExecuteMsg, InstantiateMsg, - MarsAdapterMsg, MarsAdapterQueryMsg, MarsConfigResponse, QueryMsg, RegisteredDepositorInfo, - RegisteredDepositorsResponse, TimeEstimateResponse, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllPositionsResponse, + AvailableAmountResponse, DepositorPositionResponse, DepositorPositionsResponse, ExecuteMsg, + InstantiateMsg, MarsAdapterMsg, MarsAdapterQueryMsg, MarsConfigResponse, QueryMsg, + RegisteredDepositorInfo, RegisteredDepositorsResponse, TimeEstimateResponse, }; use crate::state::{Config, Depositor, ADMINS, CONFIG, PENDING_DEPOSITORS, WHITELISTED_DEPOSITORS}; /// Contract name that is used for migration -const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); /// Contract version that is used for migration -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Reply ID for CreateCreditAccount submessage const REPLY_CREATE_CREDIT_ACCOUNT_ID: u64 = 1; @@ -159,6 +159,12 @@ fn dispatch_execute_interface( depositor_address, enabled, } => execute_set_depositor_enabled(deps, env, info, depositor_address, enabled), + AdapterInterfaceMsg::AddAdmin { admin_address } => { + execute_add_admin(deps, info, admin_address) + } + AdapterInterfaceMsg::RemoveAdmin { admin_address } => { + execute_remove_admin(deps, info, admin_address) + } } } @@ -444,6 +450,60 @@ fn execute_set_depositor_enabled( .add_attribute("enabled", enabled.to_string())) } +fn execute_add_admin( + deps: DepsMut, + info: MessageInfo, + admin_address: String, +) -> Result { + validate_admin_caller(&deps, &info)?; + + let admin_addr = deps.api.addr_validate(&admin_address)?; + let mut admins = ADMINS.load(deps.storage)?; + + if admins.contains(&admin_addr) { + return Err(ContractError::AdminAlreadyExists { + admin: admin_address, + }); + } + + admins.push(admin_addr.clone()); + ADMINS.save(deps.storage, &admins)?; + + Ok(Response::new() + .add_attribute("action", "add_admin") + .add_attribute("sender", info.sender) + .add_attribute("admin", admin_addr)) +} + +fn execute_remove_admin( + deps: DepsMut, + info: MessageInfo, + admin_address: String, +) -> Result { + validate_admin_caller(&deps, &info)?; + + let admin_addr = deps.api.addr_validate(&admin_address)?; + let mut admins = ADMINS.load(deps.storage)?; + + if !admins.contains(&admin_addr) { + return Err(ContractError::AdminNotFound { + admin: admin_address, + }); + } + + if admins.len() <= 1 { + return Err(ContractError::CannotRemoveLastAdmin {}); + } + + admins.retain(|a| a != admin_addr); + ADMINS.save(deps.storage, &admins)?; + + Ok(Response::new() + .add_attribute("action", "remove_admin") + .add_attribute("sender", info.sender) + .add_attribute("admin", admin_addr)) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -487,6 +547,7 @@ fn dispatch_query_interface(deps: Deps, msg: AdapterInterfaceQueryMsg) -> StdRes AdapterInterfaceQueryMsg::DepositorPositions { depositor_address } => { to_json_binary(&query_depositor_positions(deps, depositor_address)?) } + AdapterInterfaceQueryMsg::Admins {} => to_json_binary(&query_admins(deps)?), } } @@ -705,6 +766,13 @@ fn query_depositor_positions( }) } +fn query_admins(deps: Deps) -> StdResult { + let admins = ADMINS.load(deps.storage)?; + Ok(AdminsResponse { + admins: admins.into_iter().map(|a| a.to_string()).collect(), + }) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { match msg.id { diff --git a/contracts/inflow/mars-adapter/src/error.rs b/contracts/inflow/mars-adapter/src/error.rs index 2f9eecfc..c21b97b8 100644 --- a/contracts/inflow/mars-adapter/src/error.rs +++ b/contracts/inflow/mars-adapter/src/error.rs @@ -38,4 +38,13 @@ pub enum ContractError { #[error("Mars protocol error: {msg}")] MarsProtocolError { msg: String }, + + #[error("Admin already exists: {admin}")] + AdminAlreadyExists { admin: String }, + + #[error("Admin not found: {admin}")] + AdminNotFound { admin: String }, + + #[error("Cannot remove the last admin")] + CannotRemoveLastAdmin {}, } diff --git a/contracts/inflow/mars-adapter/src/lib.rs b/contracts/inflow/mars-adapter/src/lib.rs index e1aa6b3e..6496c56a 100644 --- a/contracts/inflow/mars-adapter/src/lib.rs +++ b/contracts/inflow/mars-adapter/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; pub mod error; pub mod mars; +pub mod migration; pub mod msg; pub mod state; diff --git a/contracts/inflow/mars-adapter/src/migration/migrate.rs b/contracts/inflow/mars-adapter/src/migration/migrate.rs new file mode 100644 index 00000000..18db8a8f --- /dev/null +++ b/contracts/inflow/mars-adapter/src/migration/migrate.rs @@ -0,0 +1,35 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DepsMut, Env, Response, StdError}; +use cw2::{get_contract_version, set_contract_version}; +// entry_point is being used but for some reason clippy doesn't see that, hence the allow attribute here +#[allow(unused_imports)] +use cosmwasm_std::entry_point; + +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::error::ContractError; + +#[cw_serde] +pub struct MigrateMsg {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + check_contract_version(deps.storage)?; + + // No state migrations needed from v3.6.10 to v3.6.11 + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new()) +} + +fn check_contract_version(storage: &dyn cosmwasm_std::Storage) -> Result<(), ContractError> { + let contract_version = get_contract_version(storage)?; + + if contract_version.version == CONTRACT_VERSION { + return Err(ContractError::Std(StdError::generic_err( + "Contract is already migrated to the newest version.", + ))); + } + + Ok(()) +} diff --git a/contracts/inflow/mars-adapter/src/migration/mod.rs b/contracts/inflow/mars-adapter/src/migration/mod.rs new file mode 100644 index 00000000..2bec015c --- /dev/null +++ b/contracts/inflow/mars-adapter/src/migration/mod.rs @@ -0,0 +1,4 @@ +pub mod migrate; + +#[cfg(test)] +mod testing; diff --git a/contracts/inflow/mars-adapter/src/migration/testing.rs b/contracts/inflow/mars-adapter/src/migration/testing.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/inflow/mars-adapter/src/migration/testing.rs @@ -0,0 +1 @@ + diff --git a/contracts/inflow/mars-adapter/src/msg.rs b/contracts/inflow/mars-adapter/src/msg.rs index 4fe7e846..56a21e25 100644 --- a/contracts/inflow/mars-adapter/src/msg.rs +++ b/contracts/inflow/mars-adapter/src/msg.rs @@ -2,9 +2,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; // Re-export adapter interface types and response types pub use interface::inflow_adapter::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllPositionsResponse, AvailableAmountResponse, - DepositorPositionResponse, DepositorPositionsResponse, RegisteredDepositorInfo, - RegisteredDepositorsResponse, TimeEstimateResponse, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllPositionsResponse, + AvailableAmountResponse, DepositorPositionResponse, DepositorPositionsResponse, + RegisteredDepositorInfo, RegisteredDepositorsResponse, TimeEstimateResponse, }; /// Message for instantiating the Mars adapter contract diff --git a/contracts/inflow/mars-adapter/src/testing.rs b/contracts/inflow/mars-adapter/src/testing.rs index f7bdb344..3b29791e 100644 --- a/contracts/inflow/mars-adapter/src/testing.rs +++ b/contracts/inflow/mars-adapter/src/testing.rs @@ -12,9 +12,9 @@ use crate::mars::{ MarsCreditManagerQueryMsg, MarsParamsQueryMsg, PositionsResponse, TotalDepositResponse, }; use crate::msg::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllPositionsResponse, AvailableAmountResponse, - DepositorPositionResponse, DepositorPositionsResponse, ExecuteMsg, InstantiateMsg, - MarsAdapterMsg, MarsConfigResponse, QueryMsg, RegisteredDepositorsResponse, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllPositionsResponse, + AvailableAmountResponse, DepositorPositionResponse, DepositorPositionsResponse, ExecuteMsg, + InstantiateMsg, MarsAdapterMsg, MarsConfigResponse, QueryMsg, RegisteredDepositorsResponse, TimeEstimateResponse, }; use crate::state::{Depositor, ADMINS, CONFIG, WHITELISTED_DEPOSITORS}; @@ -2218,3 +2218,205 @@ fn query_depositor_position_isolation_between_depositors() { let response2: DepositorPositionResponse = from_json(&res2).unwrap(); assert_eq!(response2.amount, Uint128::new(300)); } + +// ============================================================================ +// ADMIN MANAGEMENT TESTS +// ============================================================================ + +#[test] +fn test_add_admin_success() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(res.attributes[0].value, "add_admin"); + assert_eq!(res.attributes[2].value, admin2.to_string()); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 2); +} + +#[test] +fn test_add_admin_unauthorized() { + let mut deps = mock_dependencies(); + setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let non_admin = deps.api.addr_make("non_admin"); + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: non_admin, + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); +} + +#[test] +fn test_add_admin_duplicate() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: test_data.admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminAlreadyExists { .. })); +} + +#[test] +fn test_remove_admin_success() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), info, add_msg).unwrap(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], admin2.to_string()); +} + +#[test] +fn test_remove_admin_unauthorized() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let non_admin = deps.api.addr_make("non_admin"); + let info = MessageInfo { + sender: non_admin, + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); +} + +#[test] +fn test_remove_admin_not_found() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let non_admin = deps.api.addr_make("non_admin"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: non_admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminNotFound { .. })); +} + +#[test] +fn test_remove_last_admin_fails() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotRemoveLastAdmin {}); +} + +#[test] +fn test_admin_self_removal() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), info, add_msg).unwrap(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + let new_admin = deps.api.addr_make("new_admin"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let try_add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: new_admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, try_add_msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); +} + +#[test] +fn test_query_admins() { + let mut deps = mock_dependencies(); + let test_data = setup_contract(&mut deps, "account1"); + let env = mock_env(); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = from_json(&res).unwrap(); + + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], test_data.admin.to_string()); +} diff --git a/contracts/inflow/proxy/schema/proxy.json b/contracts/inflow/proxy/schema/proxy.json index 465c9e60..73d78688 100644 --- a/contracts/inflow/proxy/schema/proxy.json +++ b/contracts/inflow/proxy/schema/proxy.json @@ -1,6 +1,6 @@ { "contract_name": "proxy", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/inflow/skip-adapter/schema/raw/execute.json b/contracts/inflow/skip-adapter/schema/raw/execute.json index bb5f3d9e..f4699ecd 100644 --- a/contracts/inflow/skip-adapter/schema/raw/execute.json +++ b/contracts/inflow/skip-adapter/schema/raw/execute.json @@ -158,6 +158,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/skip-adapter/schema/raw/query.json b/contracts/inflow/skip-adapter/schema/raw/query.json index aae6c969..dee262a9 100644 --- a/contracts/inflow/skip-adapter/schema/raw/query.json +++ b/contracts/inflow/skip-adapter/schema/raw/query.json @@ -209,6 +209,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/skip-adapter/schema/skip-adapter.json b/contracts/inflow/skip-adapter/schema/skip-adapter.json index ac0f3e79..0abbb611 100644 --- a/contracts/inflow/skip-adapter/schema/skip-adapter.json +++ b/contracts/inflow/skip-adapter/schema/skip-adapter.json @@ -1,6 +1,6 @@ { "contract_name": "skip-adapter", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -393,6 +393,50 @@ } }, "additionalProperties": false + }, + { + "description": "Add a new admin (admin only)", + "type": "object", + "required": [ + "add_admin" + ], + "properties": { + "add_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an admin (admin only)", + "type": "object", + "required": [ + "remove_admin" + ], + "properties": { + "remove_admin": { + "type": "object", + "required": [ + "admin_address" + ], + "properties": { + "admin_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -1003,6 +1047,20 @@ } }, "additionalProperties": false + }, + { + "description": "Returns list of admins", + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/inflow/skip-adapter/src/contract.rs b/contracts/inflow/skip-adapter/src/contract.rs index 46691ac4..1a07601d 100644 --- a/contracts/inflow/skip-adapter/src/contract.rs +++ b/contracts/inflow/skip-adapter/src/contract.rs @@ -11,11 +11,11 @@ use crate::cross_chain::{ }; use crate::error::ContractError; use crate::msg::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllPositionsResponse, AllRoutesResponse, - AvailableAmountResponse, DepositorPositionResponse, DepositorPositionsResponse, ExecuteMsg, - ExecutorsResponse, InstantiateMsg, QueryMsg, RegisteredDepositorInfo, - RegisteredDepositorsResponse, RouteResponse, SkipAdapterMsg, SkipAdapterQueryMsg, - SkipConfigResponse, SwapParams, TimeEstimateResponse, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllPositionsResponse, + AllRoutesResponse, AvailableAmountResponse, DepositorPositionResponse, + DepositorPositionsResponse, ExecuteMsg, ExecutorsResponse, InstantiateMsg, QueryMsg, + RegisteredDepositorInfo, RegisteredDepositorsResponse, RouteResponse, SkipAdapterMsg, + SkipAdapterQueryMsg, SkipConfigResponse, SwapParams, TimeEstimateResponse, }; use crate::skip::create_local_swap_and_action_msg; use crate::state::{ @@ -27,8 +27,8 @@ use crate::validation::{ validate_route_config, }; -const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const MAX_SLIPPAGE_BPS: u64 = 1000; // 10% // ========== INSTANTIATE ========== @@ -164,6 +164,14 @@ fn dispatch_execute_standard( validate_config_admin(&deps, &info)?; execute_set_depositor_enabled(deps, depositor_address, enabled) } + AdapterInterfaceMsg::AddAdmin { admin_address } => { + validate_config_admin(&deps, &info)?; + execute_add_admin(deps, info, admin_address) + } + AdapterInterfaceMsg::RemoveAdmin { admin_address } => { + validate_config_admin(&deps, &info)?; + execute_remove_admin(deps, info, admin_address) + } } } @@ -619,6 +627,56 @@ fn execute_update_config( Ok(Response::new().add_attribute("action", "update_config")) } +fn execute_add_admin( + deps: DepsMut, + info: MessageInfo, + admin_address: String, +) -> Result, ContractError> { + let admin_addr = deps.api.addr_validate(&admin_address)?; + let mut admins = ADMINS.load(deps.storage)?; + + if admins.contains(&admin_addr) { + return Err(ContractError::AdminAlreadyExists { + admin: admin_address, + }); + } + + admins.push(admin_addr.clone()); + ADMINS.save(deps.storage, &admins)?; + + Ok(Response::new() + .add_attribute("action", "add_admin") + .add_attribute("sender", info.sender) + .add_attribute("admin", admin_addr)) +} + +fn execute_remove_admin( + deps: DepsMut, + info: MessageInfo, + admin_address: String, +) -> Result, ContractError> { + let admin_addr = deps.api.addr_validate(&admin_address)?; + let mut admins = ADMINS.load(deps.storage)?; + + if !admins.contains(&admin_addr) { + return Err(ContractError::AdminNotFound { + admin: admin_address, + }); + } + + if admins.len() <= 1 { + return Err(ContractError::CannotRemoveLastAdmin {}); + } + + admins.retain(|a| a != admin_addr); + ADMINS.save(deps.storage, &admins)?; + + Ok(Response::new() + .add_attribute("action", "remove_admin") + .add_attribute("sender", info.sender) + .add_attribute("admin", admin_addr)) +} + // ========== QUERY ========== #[cfg_attr(not(feature = "library"), entry_point)] @@ -671,6 +729,7 @@ fn dispatch_query_standard( AdapterInterfaceQueryMsg::RegisteredDepositors { enabled } => { to_json_binary(&query_registered_depositors(deps, enabled)?) } + AdapterInterfaceQueryMsg::Admins {} => to_json_binary(&query_admins(deps)?), } } @@ -799,3 +858,10 @@ fn query_available_for_withdraw( Ok(AvailableAmountResponse { amount }) } + +fn query_admins(deps: Deps) -> StdResult { + let admins = ADMINS.load(deps.storage)?; + Ok(AdminsResponse { + admins: admins.into_iter().map(|a| a.to_string()).collect(), + }) +} diff --git a/contracts/inflow/skip-adapter/src/error.rs b/contracts/inflow/skip-adapter/src/error.rs index 60397f2f..c1f4a59e 100644 --- a/contracts/inflow/skip-adapter/src/error.rs +++ b/contracts/inflow/skip-adapter/src/error.rs @@ -54,6 +54,15 @@ pub enum ContractError { #[error("Executor not found: {executor}")] ExecutorNotFound { executor: String }, + #[error("Admin already exists: {admin}")] + AdminAlreadyExists { admin: String }, + + #[error("Admin not found: {admin}")] + AdminNotFound { admin: String }, + + #[error("Cannot remove the last admin")] + CannotRemoveLastAdmin {}, + #[error("Invalid slippage: {bps} basis points exceeds maximum of {max_bps} (10%)")] InvalidSlippage { bps: u64, max_bps: u64 }, } diff --git a/contracts/inflow/skip-adapter/src/lib.rs b/contracts/inflow/skip-adapter/src/lib.rs index 21ced0f8..e4ac6c13 100644 --- a/contracts/inflow/skip-adapter/src/lib.rs +++ b/contracts/inflow/skip-adapter/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; pub mod cross_chain; pub mod error; +pub mod migration; pub mod msg; pub mod skip; pub mod state; diff --git a/contracts/inflow/skip-adapter/src/migration/migrate.rs b/contracts/inflow/skip-adapter/src/migration/migrate.rs new file mode 100644 index 00000000..18db8a8f --- /dev/null +++ b/contracts/inflow/skip-adapter/src/migration/migrate.rs @@ -0,0 +1,35 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DepsMut, Env, Response, StdError}; +use cw2::{get_contract_version, set_contract_version}; +// entry_point is being used but for some reason clippy doesn't see that, hence the allow attribute here +#[allow(unused_imports)] +use cosmwasm_std::entry_point; + +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::error::ContractError; + +#[cw_serde] +pub struct MigrateMsg {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + check_contract_version(deps.storage)?; + + // No state migrations needed from v3.6.10 to v3.6.11 + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new()) +} + +fn check_contract_version(storage: &dyn cosmwasm_std::Storage) -> Result<(), ContractError> { + let contract_version = get_contract_version(storage)?; + + if contract_version.version == CONTRACT_VERSION { + return Err(ContractError::Std(StdError::generic_err( + "Contract is already migrated to the newest version.", + ))); + } + + Ok(()) +} diff --git a/contracts/inflow/skip-adapter/src/migration/mod.rs b/contracts/inflow/skip-adapter/src/migration/mod.rs new file mode 100644 index 00000000..2bec015c --- /dev/null +++ b/contracts/inflow/skip-adapter/src/migration/mod.rs @@ -0,0 +1,4 @@ +pub mod migrate; + +#[cfg(test)] +mod testing; diff --git a/contracts/inflow/skip-adapter/src/migration/testing.rs b/contracts/inflow/skip-adapter/src/migration/testing.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/inflow/skip-adapter/src/migration/testing.rs @@ -0,0 +1 @@ + diff --git a/contracts/inflow/skip-adapter/src/msg.rs b/contracts/inflow/skip-adapter/src/msg.rs index aa5d6f0b..05dedd1e 100644 --- a/contracts/inflow/skip-adapter/src/msg.rs +++ b/contracts/inflow/skip-adapter/src/msg.rs @@ -7,9 +7,9 @@ use crate::state::{SwapVenue, UnifiedRoute}; // Re-export adapter interface types pub use interface::inflow_adapter::{ - AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AllPositionsResponse, AvailableAmountResponse, - DepositorPositionResponse, DepositorPositionsResponse, RegisteredDepositorInfo, - RegisteredDepositorsResponse, TimeEstimateResponse, + AdapterInterfaceMsg, AdapterInterfaceQueryMsg, AdminsResponse, AllPositionsResponse, + AvailableAmountResponse, DepositorPositionResponse, DepositorPositionsResponse, + RegisteredDepositorInfo, RegisteredDepositorsResponse, TimeEstimateResponse, }; // Re-export SwapOperation from state for convenience diff --git a/contracts/inflow/skip-adapter/src/testing.rs b/contracts/inflow/skip-adapter/src/testing.rs index 3203c405..69d5262f 100644 --- a/contracts/inflow/skip-adapter/src/testing.rs +++ b/contracts/inflow/skip-adapter/src/testing.rs @@ -510,6 +510,199 @@ mod contract_tests { assert_eq!(routes.routes.len(), 1); assert_eq!(routes.routes[0].1.venue, SwapVenue::NeutronAstroport); } + + // ============================================================================ + // ADMIN MANAGEMENT TESTS + // ============================================================================ + + #[test] + fn test_add_admin_success() { + let (mut deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(res.attributes[0].value, "add_admin"); + assert_eq!(res.attributes[2].value, admin2.to_string()); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = cosmwasm_std::from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 2); + } + + #[test] + fn test_add_admin_unauthorized() { + let (mut deps, _) = setup_contract_with_depositor(); + let env = mock_env(); + + let non_admin = deps.api.addr_make("non_admin"); + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: non_admin, + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_add_admin_duplicate() { + let (mut deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: test_data.admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminAlreadyExists { .. })); + } + + #[test] + fn test_remove_admin_success() { + let (mut deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), info, add_msg).unwrap(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = cosmwasm_std::from_json(&res).unwrap(); + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], admin2.to_string()); + } + + #[test] + fn test_remove_admin_unauthorized() { + let (mut deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let non_admin = deps.api.addr_make("non_admin"); + let info = MessageInfo { + sender: non_admin, + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_remove_admin_not_found() { + let (mut deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let non_admin = deps.api.addr_make("non_admin"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: non_admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::AdminNotFound { .. })); + } + + #[test] + fn test_remove_last_admin_fails() { + let (mut deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotRemoveLastAdmin {}); + } + + #[test] + fn test_admin_self_removal() { + let (mut deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let admin2 = deps.api.addr_make("admin2"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: admin2.to_string(), + }); + execute(deps.as_mut(), env.clone(), info, add_msg).unwrap(); + + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let remove_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::RemoveAdmin { + admin_address: test_data.admin.to_string(), + }); + let res = execute(deps.as_mut(), env.clone(), info, remove_msg).unwrap(); + assert_eq!(res.attributes[0].value, "remove_admin"); + + let new_admin = deps.api.addr_make("new_admin"); + let info = MessageInfo { + sender: test_data.admin.clone(), + funds: vec![], + }; + let try_add_msg = ExecuteMsg::StandardAction(AdapterInterfaceMsg::AddAdmin { + admin_address: new_admin.to_string(), + }); + let err = execute(deps.as_mut(), env, info, try_add_msg).unwrap_err(); + assert_eq!(err, ContractError::UnauthorizedAdmin {}); + } + + #[test] + fn test_query_admins() { + let (deps, test_data) = setup_contract_with_depositor(); + let env = mock_env(); + + let query_msg = QueryMsg::StandardQuery(AdapterInterfaceQueryMsg::Admins {}); + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let admins: AdminsResponse = cosmwasm_std::from_json(&res).unwrap(); + + assert_eq!(admins.admins.len(), 1); + assert_eq!(admins.admins[0], test_data.admin.to_string()); + } } #[cfg(test)] diff --git a/contracts/inflow/user-registry/schema/user-registry.json b/contracts/inflow/user-registry/schema/user-registry.json index 02877870..2d8bf0a1 100644 --- a/contracts/inflow/user-registry/schema/user-registry.json +++ b/contracts/inflow/user-registry/schema/user-registry.json @@ -1,6 +1,6 @@ { "contract_name": "user-registry", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/inflow/vault/schema/vault.json b/contracts/inflow/vault/schema/vault.json index cf93ebd0..4345440d 100644 --- a/contracts/inflow/vault/schema/vault.json +++ b/contracts/inflow/vault/schema/vault.json @@ -1,6 +1,6 @@ { "contract_name": "vault", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/inflow/vault/src/contract.rs b/contracts/inflow/vault/src/contract.rs index a9c6ced9..becc88fe 100644 --- a/contracts/inflow/vault/src/contract.rs +++ b/contracts/inflow/vault/src/contract.rs @@ -214,7 +214,7 @@ pub fn execute( ExecuteMsg::DepositToAdapter { adapter_name, amount, - } => deposit_to_adapter(deps, env, info, &config, adapter_name, amount), + } => deposit_to_adapter(deps, env, info, &config, adapter_name, amount, false), ExecuteMsg::MoveAdapterFunds { from_adapter, to_adapter, @@ -1328,6 +1328,7 @@ fn deposit_to_adapter( config: &Config, adapter_name: String, amount: Uint128, + skip_vault_balance_check: bool, ) -> Result, ContractError> { // Validate caller is whitelisted validate_address_is_whitelisted(&deps, info.sender.clone())?; @@ -1339,16 +1340,19 @@ fn deposit_to_adapter( name: adapter_name.clone(), })?; - // Check vault has sufficient balance - let vault_balance = deps - .querier - .query_balance(&env.contract.address, &config.deposit_denom)?; - - if vault_balance.amount < amount { - return Err(ContractError::InsufficientBalance { - available: vault_balance.amount, - required: amount, - }); + // Check vault has sufficient balance (skipped when moving between adapters, + // since the funds will arrive from the source adapter before this executes) + if !skip_vault_balance_check { + let vault_balance = deps + .querier + .query_balance(&env.contract.address, &config.deposit_denom)?; + + if vault_balance.amount < amount { + return Err(ContractError::InsufficientBalance { + available: vault_balance.amount, + required: amount, + }); + } } let mut messages = vec![]; @@ -1424,7 +1428,9 @@ fn move_adapter_funds( coin.amount, )?; - // Deposit to destination adapter (handles DeploymentTracking automatically) + // Deposit to destination adapter (handles DeploymentTracking automatically). + // Skip the vault balance check: funds are still in the source adapter during + // message building and will arrive before the deposit message executes on-chain. let deposit_response = deposit_to_adapter( deps, env, @@ -1432,6 +1438,7 @@ fn move_adapter_funds( config, to_adapter.clone(), coin.amount, + true, )?; // Combine messages and attributes from both operations diff --git a/contracts/inflow/vault/src/migration/migrate.rs b/contracts/inflow/vault/src/migration/migrate.rs index e2f8cb8d..5e5d325a 100644 --- a/contracts/inflow/vault/src/migration/migrate.rs +++ b/contracts/inflow/vault/src/migration/migrate.rs @@ -21,7 +21,7 @@ pub fn migrate( ) -> Result, ContractError> { check_contract_version(deps.storage)?; - // No state migrations needed from v3.6.9 to v3.6.10 + // No state migrations needed from v3.6.10 to v3.6.11 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; diff --git a/contracts/inflow/vault/src/testing_adapters.rs b/contracts/inflow/vault/src/testing_adapters.rs index 07d53739..e2f67d4b 100644 --- a/contracts/inflow/vault/src/testing_adapters.rs +++ b/contracts/inflow/vault/src/testing_adapters.rs @@ -3031,3 +3031,110 @@ fn test_move_adapter_funds_deposit_denom_with_tracked_deployment() { assert_eq!(res.attributes[2].value, "mars_adapter"); assert_eq!(res.attributes[3].value, "osmosis_adapter"); } + +// Regression test: previously move_adapter_funds would check the vault balance during message +// building and fail with InsufficientBalance, even though the funds are held in the source +// adapter and will only arrive in the vault once the withdraw message executes on-chain. +#[test] +fn test_move_adapter_funds_succeeds_with_zero_vault_balance() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + + let vault_contract_addr = deps.api.addr_make("vault"); + env.contract.address = vault_contract_addr.clone(); + + let whitelist_addr = deps.api.addr_make(WHITELIST_ADDR); + let adapter1_addr = deps.api.addr_make("adapter1"); + let adapter2_addr = deps.api.addr_make("adapter2"); + let control_center_contract_addr = deps.api.addr_make(CONTROL_CENTER); + let token_info_provider_contract_addr = deps.api.addr_make(TOKEN_INFO_PROVIDER); + + // Instantiate contract + let instantiate_msg = get_default_instantiate_msg( + DEPOSIT_DENOM, + whitelist_addr.clone(), + control_center_contract_addr.clone(), + token_info_provider_contract_addr.clone(), + ); + let info = get_message_info(&deps.api, "creator", &[]); + instantiate(deps.as_mut(), env.clone(), info, instantiate_msg).unwrap(); + + // Register two adapters + let info = get_message_info(&deps.api, WHITELIST_ADDR, &[]); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::RegisterAdapter { + name: "ibc_adapter".to_string(), + address: adapter1_addr.to_string(), + description: None, + allocation_mode: AllocationMode::Manual, + deployment_tracking: DeploymentTracking::NotTracked, + }, + ) + .unwrap(); + + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::RegisterAdapter { + name: "skip_adapter".to_string(), + address: adapter2_addr.to_string(), + description: None, + allocation_mode: AllocationMode::Manual, + deployment_tracking: DeploymentTracking::NotTracked, + }, + ) + .unwrap(); + + // Vault balance is zero — all funds are in the source adapter. + // This must not prevent the move from building its messages successfully. + + let res = execute( + deps.as_mut(), + env, + info, + ExecuteMsg::MoveAdapterFunds { + from_adapter: "ibc_adapter".to_string(), + to_adapter: "skip_adapter".to_string(), + coin: Coin { + denom: DEPOSIT_DENOM.to_string(), + amount: Uint128::new(2_500_000_000), + }, + }, + ) + .unwrap(); + + // Two messages: withdraw from ibc_adapter, then deposit to skip_adapter + assert_eq!(res.messages.len(), 2); + + // First message: withdraw from source adapter (no funds attached) + match &res.messages[0].msg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + funds, + .. + }) => { + assert_eq!(contract_addr, &adapter1_addr.to_string()); + assert!(funds.is_empty()); + } + _ => panic!("Expected WasmMsg::Execute for withdraw"), + } + + // Second message: deposit to destination adapter (funds attached) + match &res.messages[1].msg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + funds, + .. + }) => { + assert_eq!(contract_addr, &adapter2_addr.to_string()); + assert_eq!(funds.len(), 1); + assert_eq!(funds[0].denom, DEPOSIT_DENOM); + assert_eq!(funds[0].amount, Uint128::new(2_500_000_000)); + } + _ => panic!("Expected WasmMsg::Execute for deposit"), + } +} diff --git a/contracts/marketplace/schema/marketplace.json b/contracts/marketplace/schema/marketplace.json index 462f9dfc..7af63c00 100644 --- a/contracts/marketplace/schema/marketplace.json +++ b/contracts/marketplace/schema/marketplace.json @@ -1,6 +1,6 @@ { "contract_name": "marketplace", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/token-info-providers/d-token-info-provider/schema/d-token-info-provider.json b/contracts/token-info-providers/d-token-info-provider/schema/d-token-info-provider.json index 251bcdce..4fc6dd98 100644 --- a/contracts/token-info-providers/d-token-info-provider/schema/d-token-info-provider.json +++ b/contracts/token-info-providers/d-token-info-provider/schema/d-token-info-provider.json @@ -1,6 +1,6 @@ { "contract_name": "d-token-info-provider", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/token-info-providers/d-token-info-provider/src/migrate.rs b/contracts/token-info-providers/d-token-info-provider/src/migrate.rs index 2ac59a1b..62ff1523 100644 --- a/contracts/token-info-providers/d-token-info-provider/src/migrate.rs +++ b/contracts/token-info-providers/d-token-info-provider/src/migrate.rs @@ -14,7 +14,7 @@ pub struct MigrateMsg {} pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { check_contract_version(deps.storage)?; - // No state migrations needed from v3.6.9 to v3.6.10 + // No state migrations needed from v3.6.10 to v3.6.11 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; diff --git a/contracts/token-info-providers/lsm-token-info-provider/schema/lsm-token-info-provider.json b/contracts/token-info-providers/lsm-token-info-provider/schema/lsm-token-info-provider.json index f4a4a2a3..e6e5a8d3 100644 --- a/contracts/token-info-providers/lsm-token-info-provider/schema/lsm-token-info-provider.json +++ b/contracts/token-info-providers/lsm-token-info-provider/schema/lsm-token-info-provider.json @@ -1,6 +1,6 @@ { "contract_name": "lsm-token-info-provider", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/token-info-providers/lsm-token-info-provider/src/migrate.rs b/contracts/token-info-providers/lsm-token-info-provider/src/migrate.rs index 2576c46b..c4f12a43 100644 --- a/contracts/token-info-providers/lsm-token-info-provider/src/migrate.rs +++ b/contracts/token-info-providers/lsm-token-info-provider/src/migrate.rs @@ -19,7 +19,7 @@ pub fn migrate( ) -> Result { check_contract_version(deps.storage)?; - // No state migrations needed from v3.6.9 to v3.6.10 + // No state migrations needed from v3.6.10 to v3.6.11 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; diff --git a/contracts/token-info-providers/st-token-info-provider/schema/st-token-info-provider.json b/contracts/token-info-providers/st-token-info-provider/schema/st-token-info-provider.json index f398206a..0549cc3e 100644 --- a/contracts/token-info-providers/st-token-info-provider/schema/st-token-info-provider.json +++ b/contracts/token-info-providers/st-token-info-provider/schema/st-token-info-provider.json @@ -1,6 +1,6 @@ { "contract_name": "st-token-info-provider", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/token-info-providers/st-token-info-provider/src/migrate.rs b/contracts/token-info-providers/st-token-info-provider/src/migrate.rs index c14202c0..fd83f122 100644 --- a/contracts/token-info-providers/st-token-info-provider/src/migrate.rs +++ b/contracts/token-info-providers/st-token-info-provider/src/migrate.rs @@ -19,7 +19,7 @@ pub fn migrate( ) -> Result, ContractError> { check_contract_version(deps.storage)?; - // No state migrations needed from v3.6.9 to v3.6.10 + // No state migrations needed from v3.6.10 to v3.6.11 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; diff --git a/contracts/tribute/schema/tribute.json b/contracts/tribute/schema/tribute.json index b1f7c458..efb216a8 100644 --- a/contracts/tribute/schema/tribute.json +++ b/contracts/tribute/schema/tribute.json @@ -1,6 +1,6 @@ { "contract_name": "tribute", - "contract_version": "3.6.10", + "contract_version": "3.6.11", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/tribute/src/migration/migrate.rs b/contracts/tribute/src/migration/migrate.rs index 2ac59a1b..62ff1523 100644 --- a/contracts/tribute/src/migration/migrate.rs +++ b/contracts/tribute/src/migration/migrate.rs @@ -14,7 +14,7 @@ pub struct MigrateMsg {} pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { check_contract_version(deps.storage)?; - // No state migrations needed from v3.6.9 to v3.6.10 + // No state migrations needed from v3.6.10 to v3.6.11 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; diff --git a/packages/interface/src/inflow_adapter.rs b/packages/interface/src/inflow_adapter.rs index ffb9a5b9..f6928259 100644 --- a/packages/interface/src/inflow_adapter.rs +++ b/packages/interface/src/inflow_adapter.rs @@ -40,6 +40,12 @@ pub enum AdapterInterfaceMsg { depositor_address: String, enabled: bool, }, + + /// Add a new admin (admin only) + AddAdmin { admin_address: String }, + + /// Remove an admin (admin only) + RemoveAdmin { admin_address: String }, } /// Standard query messages that all protocol adapters should support @@ -86,6 +92,9 @@ pub enum AdapterInterfaceQueryMsg { /// Returns all positions for a specific depositor across all denoms DepositorPositions { depositor_address: String }, + + /// Returns list of admins + Admins {}, } // Response Types @@ -127,6 +136,11 @@ pub struct RegisteredDepositorsResponse { pub depositors: Vec, } +#[cw_serde] +pub struct AdminsResponse { + pub admins: Vec, +} + // ========== SERIALIZATION HELPERS FOR CALLING ADAPTERS ========== /// Helper to serialize AdapterInterfaceMsg for calling adapters from external contracts diff --git a/ts_types/HydroBase.client.ts b/ts_types/HydroBase.client.ts index b4a7f0d6..940f22ba 100644 --- a/ts_types/HydroBase.client.ts +++ b/ts_types/HydroBase.client.ts @@ -1040,6 +1040,16 @@ export interface HydroBaseInterface extends HydroBaseReadOnlyInterface { }: { address: string; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; + addWhitelistAdmin: ({ + address + }: { + address: string; + }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; + removeWhitelistAdmin: ({ + address + }: { + address: string; + }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; updateConfig: ({ config }: { @@ -1235,6 +1245,8 @@ export class HydroBaseClient extends HydroBaseQueryClient implements HydroBaseIn this.unvote = this.unvote.bind(this); this.addAccountToWhitelist = this.addAccountToWhitelist.bind(this); this.removeAccountFromWhitelist = this.removeAccountFromWhitelist.bind(this); + this.addWhitelistAdmin = this.addWhitelistAdmin.bind(this); + this.removeWhitelistAdmin = this.removeWhitelistAdmin.bind(this); this.updateConfig = this.updateConfig.bind(this); this.deleteConfigs = this.deleteConfigs.bind(this); this.pause = this.pause.bind(this); @@ -1401,6 +1413,28 @@ export class HydroBaseClient extends HydroBaseQueryClient implements HydroBaseIn } }, fee, memo, _funds); }; + addWhitelistAdmin = async ({ + address + }: { + address: string; + }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + add_whitelist_admin: { + address + } + }, fee, memo, _funds); + }; + removeWhitelistAdmin = async ({ + address + }: { + address: string; + }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + remove_whitelist_admin: { + address + } + }, fee, memo, _funds); + }; updateConfig = async ({ config }: { diff --git a/ts_types/HydroBase.types.ts b/ts_types/HydroBase.types.ts index 59478adb..f4901183 100644 --- a/ts_types/HydroBase.types.ts +++ b/ts_types/HydroBase.types.ts @@ -113,6 +113,14 @@ export type ExecuteMsg = { remove_account_from_whitelist: { address: string; }; +} | { + add_whitelist_admin: { + address: string; + }; +} | { + remove_whitelist_admin: { + address: string; + }; } | { update_config: { config: UpdateConfigData; diff --git a/ts_types/InflowCCTPAdapterBase.types.ts b/ts_types/InflowCCTPAdapterBase.types.ts index 18cb16df..c51580f7 100644 --- a/ts_types/InflowCCTPAdapterBase.types.ts +++ b/ts_types/InflowCCTPAdapterBase.types.ts @@ -62,6 +62,14 @@ export type AdapterInterfaceMsg = { depositor_address: string; enabled: boolean; }; +} | { + add_admin: { + admin_address: string; + }; +} | { + remove_admin: { + admin_address: string; + }; }; export type Uint128 = string; export type Binary = string; @@ -77,14 +85,6 @@ export type CctpAdapterMsg = { remove_executor: { executor_address: string; }; -} | { - add_admin: { - admin_address: string; - }; -} | { - remove_admin: { - admin_address: string; - }; } | { register_chain: { chain_config: ChainConfig; @@ -163,6 +163,8 @@ export type AdapterInterfaceQueryMsg = { depositor_positions: { depositor_address: string; }; +} | { + admins: {}; }; export type CctpAdapterQueryMsg = { chain_config: { @@ -172,8 +174,6 @@ export type CctpAdapterQueryMsg = { all_chains: {}; } | { executors: {}; -} | { - admins: {}; } | { depositor_capabilities: { depositor_address: string; diff --git a/ts_types/InflowIBCAdapterBase.types.ts b/ts_types/InflowIBCAdapterBase.types.ts index 6dcd7d5c..5e86a137 100644 --- a/ts_types/InflowIBCAdapterBase.types.ts +++ b/ts_types/InflowIBCAdapterBase.types.ts @@ -58,6 +58,14 @@ export type AdapterInterfaceMsg = { depositor_address: string; enabled: boolean; }; +} | { + add_admin: { + admin_address: string; + }; +} | { + remove_admin: { + admin_address: string; + }; }; export type Uint128 = string; export type IbcAdapterMsg = { @@ -150,6 +158,8 @@ export type AdapterInterfaceQueryMsg = { depositor_positions: { depositor_address: string; }; +} | { + admins: {}; }; export type IbcAdapterQueryMsg = { chain_config: { diff --git a/ts_types/InflowMarsAdapterBase.types.ts b/ts_types/InflowMarsAdapterBase.types.ts index 8d02bee8..8e9eac2c 100644 --- a/ts_types/InflowMarsAdapterBase.types.ts +++ b/ts_types/InflowMarsAdapterBase.types.ts @@ -37,6 +37,14 @@ export type AdapterInterfaceMsg = { depositor_address: string; enabled: boolean; }; +} | { + add_admin: { + admin_address: string; + }; +} | { + remove_admin: { + admin_address: string; + }; }; export type Uint128 = string; export type Binary = string; @@ -89,5 +97,7 @@ export type AdapterInterfaceQueryMsg = { depositor_positions: { depositor_address: string; }; +} | { + admins: {}; }; export type MarsAdapterQueryMsg = string; \ No newline at end of file diff --git a/ts_types/InflowSkipAdapterBase.types.ts b/ts_types/InflowSkipAdapterBase.types.ts index 166a5b06..2d776c14 100644 --- a/ts_types/InflowSkipAdapterBase.types.ts +++ b/ts_types/InflowSkipAdapterBase.types.ts @@ -63,6 +63,14 @@ export type AdapterInterfaceMsg = { depositor_address: string; enabled: boolean; }; +} | { + add_admin: { + admin_address: string; + }; +} | { + remove_admin: { + admin_address: string; + }; }; export type Uint128 = string; export type SkipAdapterMsg = { @@ -145,6 +153,8 @@ export type AdapterInterfaceQueryMsg = { depositor_positions: { depositor_address: string; }; +} | { + admins: {}; }; export type SkipAdapterQueryMsg = { route: {