Skip to content

Commit

Permalink
First draft of cw-bounties contract
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeHartnell committed Jul 9, 2023
1 parent 0367c84 commit 1e2ae79
Show file tree
Hide file tree
Showing 11 changed files with 487 additions and 0 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ wynd-utils = "0.4.1"
cw-ownable = "0.5.0"

cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.2.0" }
cw-bounties = { path = "contracts/external/cw-bounties", version = "*" }
cw-denom = { path = "./packages/cw-denom", version = "2.2.0" }
cw-hooks = { path = "./packages/cw-hooks", version = "2.2.0" }
cw-wormhole = { path = "./packages/cw-wormhole", version = "2.2.0" }
Expand Down
33 changes: 33 additions & 0 deletions contracts/external/cw-bounties/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "cw-bounties"
authors = ["Jake Hartnell"]
description = "A CosmWasm contract for creating and managing on-chain bounties."
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version = { 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-schema = { workspace = true }
cw-denom = { workspace = true }
cw-ownable = { workspace = true }
cw-paginate-storage = { workspace = true }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
cw2 = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
cosmwasm-schema = { workspace = true }
cw-multi-test = { workspace = true }
7 changes: 7 additions & 0 deletions contracts/external/cw-bounties/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# cw-bounties

A simple bounties smart contract. The contract is instantiated with an owner who controls when bounties are payed out (usually a DAO).

On bounty creation the funds are taken, on update funds are added or removed and bounty details can be updated, on removal funds are returned to the bounties contract owner.

Typical usage would involve a SubDAO with open proposal submission. Bounty hunters would be able to see a list of bounties, work on one and make a proposal to claim it.
11 changes: 11 additions & 0 deletions contracts/external/cw-bounties/examples/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use cosmwasm_schema::write_api;
use cw_bounties::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};

fn main() {
write_api! {
instantiate: InstantiateMsg,
query: QueryMsg,
execute: ExecuteMsg,
migrate: MigrateMsg,
}
}
272 changes: 272 additions & 0 deletions contracts/external/cw-bounties/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
use std::cmp::Ordering;

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult,
};
use cw2::set_contract_version;
use cw_paginate_storage::paginate_map_values;
use cw_utils::must_pay;

use crate::{
error::ContractError,
msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
state::{Bounty, BountyStatus, BOUNTIES, ID},
};

pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-bounties";
pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

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

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

// Set the contract owner
cw_ownable::initialize_owner(deps.storage, deps.api, Some(owner.as_str()))?;

// Initialize the next ID
ID.save(deps.storage, &1)?;

Ok(Response::default())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
// Only the owner can execute messages on this contract
cw_ownable::assert_owner(deps.storage, &info.sender)?;

match msg {
ExecuteMsg::Close { id } => close(deps, info, id),
ExecuteMsg::Create {
amount,
title,
description,
} => create(deps, env, info, amount, title, description),
ExecuteMsg::PayOut { id, recipient } => pay_out(deps, env, id, recipient),
ExecuteMsg::Update {
id,
amount,
title,
description,
} => update(deps, env, info, id, amount, title, description),
ExecuteMsg::UpdateOwnership(action) => update_owner(deps, info, env, action),
}
}

pub fn create(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Coin,
title: String,
description: Option<String>,
) -> Result<Response, ContractError> {
// Check funds sent match the bounty amount specified
let sent_amount = must_pay(&info, &amount.denom)?;
if sent_amount != amount.amount {
return Err(ContractError::InvalidAmount {
expected: amount.amount,
actual: sent_amount,
});
};

// Check bounty title is not empty string
if title.is_empty() {
return Err(ContractError::EmptyTitle {});
}

// Increment and get the next bounty ID
let id = ID.update(deps.storage, |mut id| -> StdResult<u64> {
id += 1;
Ok(id)
})?;

// Save the bounty
BOUNTIES.save(
deps.storage,
id,
&Bounty {
id,
amount,
title,
description,
status: BountyStatus::Open,
created_at: env.block.time.seconds(),
updated_at: None,
},
)?;

Ok(Response::default()
.add_attribute("action", "create_bounty")
.add_attribute("id", id.to_string()))
}

pub fn close(deps: DepsMut, info: MessageInfo, id: u64) -> Result<Response, ContractError> {
// Check bounty exists
let bounty = BOUNTIES.load(deps.storage, id)?;

// Check bounty is open
if bounty.status != BountyStatus::Open {
return Err(ContractError::NotOpen {});
};

// Pay out remaining funds to owner
// Only owner can call this, so sender is owner
let msg = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![bounty.amount],
};

Ok(Response::default()
.add_message(msg)
.add_attribute("action", "close_bounty"))
}

pub fn pay_out(
deps: DepsMut,
env: Env,
id: u64,
recipient: String,
) -> Result<Response, ContractError> {
// Check bounty exists
let mut bounty = BOUNTIES.load(deps.storage, id)?;

// Check bounty is open
if bounty.status != BountyStatus::Open {
return Err(ContractError::NotOpen {});
}

// Validate recipient address
deps.api.addr_validate(&recipient)?;

// Set bounty status to claimed
bounty.status = BountyStatus::Claimed {
claimed_by: recipient.clone(),
claimed_at: env.block.time.seconds(),
};
BOUNTIES.save(deps.storage, id, &bounty)?;

// Message to pay out remaining funds to recipient
let msg = BankMsg::Send {
to_address: recipient.clone(),
amount: vec![bounty.clone().amount],
};

Ok(Response::new()
.add_message(msg)
.add_attribute("action", "pay_out_bounty")
.add_attribute("bounty_id", id.to_string())
.add_attribute("amount", bounty.amount.to_string())
.add_attribute("recipient", recipient))
}

pub fn update(
deps: DepsMut,
env: Env,
info: MessageInfo,
id: u64,
new_amount: Coin,
title: String,
description: Option<String>,
) -> Result<Response, ContractError> {
// Check bounty exists
let bounty = BOUNTIES.load(deps.storage, id)?;

// Check bounty is open
if bounty.status != BountyStatus::Open {
return Err(ContractError::NotOpen {});
}

// Update bounty
BOUNTIES.save(
deps.storage,
bounty.id,
&Bounty {
id: bounty.id,
amount: new_amount.clone(),
title,
description,
status: bounty.status,
created_at: bounty.created_at,
updated_at: Some(env.block.time.seconds()),
},
)?;

// Check if amount is greater or less than original amount
let old_amount = bounty.amount;
let res = Response::new()
.add_attribute("action", "update_bounty")
.add_attribute("bounty_id", id.to_string())
.add_attribute("amount", new_amount.amount.to_string());

match new_amount.amount.cmp(&old_amount.amount) {
Ordering::Greater => {
// If new amount is greater, check funds sent plus
// original amount match new amount
let sent_amount = must_pay(&info, &new_amount.denom)?;
if sent_amount + old_amount.amount != new_amount.amount {
return Err(ContractError::InvalidAmount {
expected: new_amount.amount - old_amount.amount,
actual: sent_amount + old_amount.amount,
});
}
Ok(res)
}
Ordering::Less => {
// If new amount is less, pay out difference to owner
let diff = old_amount.amount - new_amount.amount;
let msg = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: old_amount.denom,
amount: diff,
}],
};

Ok(res.add_message(msg))
}
Ordering::Equal => {
// If the new amount hasn't changed we return the response
Ok(res)
}
}
}

pub fn update_owner(
deps: DepsMut,
info: MessageInfo,
env: Env,
action: cw_ownable::Action,
) -> Result<Response, ContractError> {
let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?;
Ok(Response::default().add_attributes(ownership.into_attributes()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Bounty { id } => to_binary(&BOUNTIES.load(deps.storage, id)?),
QueryMsg::Bounties { start_after, limit } => to_binary(&paginate_map_values(
deps,
&BOUNTIES,
start_after,
limit,
Order::Descending,
)?),
QueryMsg::Count {} => to_binary(&ID.load(deps.storage)?),
QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?),
}
}
26 changes: 26 additions & 0 deletions contracts/external/cw-bounties/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use cosmwasm_std::{StdError, Uint128};
use cw_ownable::OwnershipError;
use cw_utils::PaymentError;
use thiserror::Error;

#[derive(Error, Debug)]
#[cfg_attr(test, derive(PartialEq))] // Only neeed while testing.
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),

#[error(transparent)]
Ownable(#[from] OwnershipError),

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

#[error("Title cannot be an empty string")]
EmptyTitle {},

#[error("Bounty is not open")]
NotOpen {},

#[error("Invalid amount. Expected ({expected}), got ({actual})")]
InvalidAmount { expected: Uint128, actual: Uint128 },
}
11 changes: 11 additions & 0 deletions contracts/external/cw-bounties/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]

pub mod contract;
mod error;
pub mod msg;
pub mod state;

#[cfg(test)]
mod tests;

pub use crate::error::ContractError;
Loading

0 comments on commit 1e2ae79

Please sign in to comment.