Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement simple protocol fee hook #89

Merged
merged 29 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2ed7425
Add pausable ism with tests
yorhodes Jan 9, 2024
a6c890a
Fix paused query error case
yorhodes Jan 9, 2024
ee01584
Run CI against all PRs
yorhodes Jan 9, 2024
476b777
Add pausable ISM to README
yorhodes Jan 9, 2024
c2c2804
Build wasm
nambrot Jan 9, 2024
ef9c6f3
Fix scripts
yorhodes Jan 9, 2024
8a9aa1b
Allow threshold == set size and add tests
nambrot Jan 9, 2024
63c62bb
Upload artifacts
nambrot Jan 9, 2024
ae2706d
Force
nambrot Jan 9, 2024
46d3e36
Move into makefile
nambrot Jan 10, 2024
f7af326
Install rename
nambrot Jan 11, 2024
280fb07
Rename properly
nambrot Jan 11, 2024
d5666cd
Update test.yaml
nambrot Jan 12, 2024
282430c
Implement simple fee hook_
yorhodes Jan 16, 2024
6bd5efa
Make merkle hook not ownable
yorhodes Jan 16, 2024
9e335f4
Address pr comments
yorhodes Jan 16, 2024
1b0e3c9
Fix unit tests
yorhodes Jan 16, 2024
cfe3377
Merge pull request #5 from hyperlane-xyz/merkle-not-ownable
yorhodes Jan 16, 2024
7973533
Merge pull request #1 from hyperlane-xyz/pausable-ism
yorhodes Jan 16, 2024
26f040f
Merge pull request #3 from hyperlane-xyz/nambrot/fix-multisig-ism
yorhodes Jan 16, 2024
04468ba
Fix renaming
yorhodes Jan 16, 2024
ddb174b
Fix makefile indentation
yorhodes Jan 16, 2024
189ecf3
Force cargo install
yorhodes Jan 16, 2024
2cd2bfc
Merge pull request #2 from hyperlane-xyz/nambrot/ci-wasm-build
yorhodes Jan 16, 2024
0b9b3a8
Merge branch 'hypmain' into simple-fee-hook
yorhodes Jan 17, 2024
d65a08a
Fix fee hook tests
yorhodes Jan 17, 2024
189ee15
Make set fee only owner
yorhodes Jan 17, 2024
317ef61
Implement remaining unit tests
yorhodes Jan 19, 2024
5b82038
Fix merkle integration test use
yorhodes Jan 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,25 @@ jobs:
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Install Rust
run: rustup update stable
run: rustup update 1.72

- name: Install target
run: rustup target add wasm32-unknown-unknown
- name: Install rename
run: sudo apt-get install -y rename

- name: Install rust deps
run: make install

- run: cargo test --workspace --exclude hpl-tests
- name: Run tests
run: cargo test --workspace --exclude hpl-tests

- name: Build wasm
run: make ci-build

- name: Upload wasm archive
uses: actions/upload-artifact@v2
with:
name: wasm_codes
path: wasm_codes.zip

coverage:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ hpl-mailbox = { path = "./contracts/core/mailbox" }
hpl-validator-announce = { path = "./contracts/core/va" }

hpl-hook-merkle = { path = "./contracts/hooks/merkle" }
hpl-hook-fee = { path = "./contracts/hooks/fee" }
hpl-hook-pausable = { path = "./contracts/hooks/pausable" }
hpl-hook-routing = { path = "./contracts/hooks/routing" }
hpl-hook-routing-custom = { path = "./contracts/hooks/routing-custom" }
Expand Down
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@

clean:
@cargo clean
@rm -rf ./artifacts

install:
cargo install --force cw-optimizoor cosmwasm-check beaker
rustup target add wasm32-unknown-unknown

schema:
ls ./contracts | xargs -n 1 -t beaker wasm ts-gen

build:
cargo build
cargo wasm

build-dev: clean
cargo cw-optimizoor
rename --force 's/(.*)-(.*)\.wasm/$$1\.wasm/d' artifacts/*

check: build-dev
check: build
ls -d ./artifacts/*.wasm | xargs -I x cosmwasm-check x

ci-build: check
zip -jr wasm_codes.zip artifacts
42 changes: 42 additions & 0 deletions contracts/hooks/fee/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "hpl-hook-fee"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
keywords.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]

[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []

[dependencies]
cosmwasm-std.workspace = true
cosmwasm-storage.workspace = true
cosmwasm-schema.workspace = true

cw-storage-plus.workspace = true
cw2.workspace = true
cw-utils.workspace = true

schemars.workspace = true
serde-json-wasm.workspace = true

thiserror.workspace = true

hpl-ownable.workspace = true
hpl-interface.workspace = true

[dev-dependencies]
rstest.workspace = true
ibcx-test-utils.workspace = true

anyhow.workspace = true
275 changes: 275 additions & 0 deletions contracts/hooks/fee/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
ensure, ensure_eq, BankMsg, Coin, CosmosMsg, Deps, DepsMut, Env, Event, MessageInfo,
QueryResponse, Response, StdError,
};
use cw_storage_plus::Item;
use hpl_interface::{
hook::{
fee::{ExecuteMsg, FeeHookMsg, FeeHookQueryMsg, FeeResponse, InstantiateMsg, QueryMsg},
HookQueryMsg, MailboxResponse, QuoteDispatchResponse,
},
to_binary,
};

#[derive(thiserror::Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),

#[error("{0}")]
PaymentError(#[from] cw_utils::PaymentError),

#[error("unauthorized")]
Unauthorized {},

#[error("hook paused")]
Paused {},
}

// version info for migration info
pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

pub const COIN_FEE_KEY: &str = "coin_fee";
pub const COIN_FEE: Item<Coin> = Item::new(COIN_FEE_KEY);

fn new_event(name: &str) -> Event {
Event::new(format!("hpl_hook_fee::{}", name))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

let owner = deps.api.addr_validate(&msg.owner)?;

hpl_ownable::initialize(deps.storage, &owner)?;
COIN_FEE.save(deps.storage, &msg.fee)?;

Ok(Response::new().add_event(
new_event("initialize")
.add_attribute("sender", info.sender)
.add_attribute("owner", owner)
.add_attribute("fee_denom", msg.fee.denom)
.add_attribute("fee_amount", msg.fee.amount),
))
}

fn get_fee(deps: Deps) -> Result<FeeResponse, ContractError> {
let fee = COIN_FEE.load(deps.storage)?;

Ok(FeeResponse { fee })
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Ownable(msg) => Ok(hpl_ownable::handle(deps, env, info, msg)?),
ExecuteMsg::FeeHook(msg) => match msg {
FeeHookMsg::SetFee { fee } => {
let owner = hpl_ownable::get_owner(deps.storage)?;
ensure_eq!(owner, info.sender, StdError::generic_err("unauthorized"));

COIN_FEE.save(deps.storage, &fee)?;

Ok(Response::new().add_event(
new_event("set_fee")
.add_attribute("fee_denom", fee.denom)
.add_attribute("fee_amount", fee.amount),
))
}
FeeHookMsg::Claim { recipient } => {
let owner = hpl_ownable::get_owner(deps.storage)?;
ensure_eq!(owner, info.sender, StdError::generic_err("unauthorized"));

let recipient = recipient.unwrap_or(owner);
let balances = deps.querier.query_all_balances(&env.contract.address)?;

let claim_msg: CosmosMsg = BankMsg::Send {
to_address: recipient.into_string(),
amount: balances,
}
.into();

Ok(Response::new()
.add_message(claim_msg)
.add_event(new_event("claim")))
}
},
ExecuteMsg::PostDispatch(_) => {
let fee = COIN_FEE.load(deps.storage)?;
let supplied = cw_utils::must_pay(&info, &fee.denom)?;

ensure!(
supplied.u128() >= fee.amount.u128(),
// TODO: improve error
StdError::generic_err("insufficient funds")
);

Ok(Response::new().add_event(
new_event("post_dispatch")
.add_attribute("paid_denom", fee.denom)
.add_attribute("paid_amount", supplied),
))
}
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
match msg {
QueryMsg::Ownable(msg) => Ok(hpl_ownable::handle_query(deps, env, msg)?),
QueryMsg::Hook(msg) => match msg {
HookQueryMsg::Mailbox {} => to_binary(get_mailbox(deps)),
HookQueryMsg::QuoteDispatch(_) => to_binary(quote_dispatch(deps)),
},
QueryMsg::FeeHook(FeeHookQueryMsg::Fee {}) => to_binary(get_fee(deps)),
}
}

fn get_mailbox(_deps: Deps) -> Result<MailboxResponse, ContractError> {
Ok(MailboxResponse {
mailbox: "unrestricted".to_string(),
})
}

fn quote_dispatch(deps: Deps) -> Result<QuoteDispatchResponse, ContractError> {
let fee = COIN_FEE.load(deps.storage)?;
Ok(QuoteDispatchResponse { fees: vec![fee] })
}

#[cfg(test)]
mod test {
use cosmwasm_schema::serde::{de::DeserializeOwned, Serialize};
use cosmwasm_std::{
coin, from_json,
testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage},
to_json_binary, Addr, HexBinary, OwnedDeps,
};
use hpl_interface::hook::{PostDispatchMsg, QuoteDispatchMsg};
use hpl_ownable::get_owner;
use ibcx_test_utils::{addr, gen_bz};
use rstest::{fixture, rstest};

use super::*;

type TestDeps = OwnedDeps<MockStorage, MockApi, MockQuerier>;

fn query<S: Serialize, T: DeserializeOwned>(deps: Deps, msg: S) -> T {
let req: QueryMsg = from_json(to_json_binary(&msg).unwrap()).unwrap();
let res = crate::query(deps, mock_env(), req).unwrap();
from_json(res).unwrap()
}

#[fixture]
fn deps(
#[default(addr("deployer"))] sender: Addr,
#[default(addr("owner"))] owner: Addr,
#[default(coin(100, "uusd"))] fee: Coin,
) -> TestDeps {
let mut deps = mock_dependencies();

instantiate(
deps.as_mut(),
mock_env(),
mock_info(sender.as_str(), &[]),
InstantiateMsg {
owner: owner.to_string(),
fee,
},
)
.unwrap();

deps
}

#[rstest]
fn test_init(deps: TestDeps) {
assert_eq!("uusd", get_fee(deps.as_ref()).unwrap().fee.denom.as_str());
assert_eq!("owner", get_owner(deps.as_ref().storage).unwrap().as_str());
}

#[rstest]
#[case(&[coin(100, "uusd")])]
#[should_panic(expected = "Generic error: insufficient funds")]
#[case(&[coin(99, "uusd")])]
fn test_post_dispatch(mut deps: TestDeps, #[case] funds: &[Coin]) {
execute(
deps.as_mut(),
mock_env(),
mock_info("owner", funds),
ExecuteMsg::PostDispatch(PostDispatchMsg {
metadata: HexBinary::default(),
message: gen_bz(100),
}),
)
.map_err(|e| e.to_string())
.unwrap();
}

#[rstest]
fn test_query(deps: TestDeps) {
let res: MailboxResponse = query(deps.as_ref(), QueryMsg::Hook(HookQueryMsg::Mailbox {}));
assert_eq!("unrestricted", res.mailbox.as_str());

let res: QuoteDispatchResponse = query(
deps.as_ref(),
QueryMsg::Hook(HookQueryMsg::QuoteDispatch(QuoteDispatchMsg::default())),
);
assert_eq!(res.fees, vec![coin(100, "uusd")]);
}

#[rstest]
#[case(addr("owner"), coin(200, "uusd"))]
#[should_panic(expected = "unauthorized")]
#[case(addr("deployer"), coin(200, "uusd"))]
fn test_set_fee(mut deps: TestDeps, #[case] sender: Addr, #[case] fee: Coin) {
execute(
deps.as_mut(),
mock_env(),
mock_info(sender.as_str(), &[]),
ExecuteMsg::FeeHook(FeeHookMsg::SetFee { fee: fee.clone() }),
)
.map_err(|e| e.to_string())
.unwrap();

assert_eq!(fee, get_fee(deps.as_ref()).unwrap().fee);
}

#[rstest]
#[case(addr("owner"), Some(addr("deployer")))]
#[case(addr("owner"), None)]
#[should_panic(expected = "unauthorized")]
#[case(addr("deployer"), None)]
fn test_claim(mut deps: TestDeps, #[case] sender: Addr, #[case] recipient: Option<Addr>) {
let res = execute(
deps.as_mut(),
mock_env(),
mock_info(sender.as_str(), &[]),
ExecuteMsg::FeeHook(FeeHookMsg::Claim { recipient: recipient.clone() }),
)
.map_err(|e| e.to_string())
.unwrap();

assert_eq!(
CosmosMsg::Bank(BankMsg::Send {
to_address: recipient.unwrap_or_else(|| addr("owner")).into_string(),
amount: vec![],
}),
res.messages[0].msg
);
println!("{:?}", res);
}
}
Loading
Loading