Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b02e307

Browse files
committed
add exploit for cosmwasm ctf 06
1 parent 3b0f601 commit b02e307

File tree

12 files changed

+713
-1
lines changed

12 files changed

+713
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[alias]
2+
wasm = "build --release --lib --target wasm32-unknown-unknown"
3+
unit-test = "test --lib"
4+
schema = "run --bin schema"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[package]
2+
name = "oaksecurity-cosmwasm-ctf-06"
3+
version = "0.1.0"
4+
authors = ["Oak Security <[email protected]>"]
5+
edition = "2021"
6+
7+
exclude = [
8+
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
9+
"contract.wasm",
10+
"hash.txt",
11+
]
12+
13+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14+
15+
[lib]
16+
crate-type = ["cdylib", "rlib"]
17+
18+
[profile.release]
19+
opt-level = 3
20+
debug = false
21+
rpath = false
22+
lto = true
23+
debug-assertions = false
24+
codegen-units = 1
25+
panic = 'abort'
26+
incremental = false
27+
overflow-checks = true
28+
29+
[features]
30+
# for more explicit tests, cargo test --features=backtraces
31+
backtraces = ["cosmwasm-std/backtraces"]
32+
# use library feature to disable all instantiate/execute/query exports
33+
library = []
34+
35+
[package.metadata.scripts]
36+
optimize = """docker run --rm -v "$(pwd)":/code \
37+
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
38+
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
39+
cosmwasm/rust-optimizer:0.12.10
40+
"""
41+
42+
[dependencies]
43+
cosmwasm-schema = "1.1.3"
44+
cosmwasm-std = "1.1.3"
45+
cosmwasm-storage = "1.1.3"
46+
cw-storage-plus = "1.0.1"
47+
cw2 = "1.0.1"
48+
cw20 = "1.0.1"
49+
cw20-base = "1.0.1"
50+
schemars = "0.8.10"
51+
serde = { version = "1.0.145", default-features = false, features = ["derive"] }
52+
thiserror = { version = "1.0.31" }
53+
54+
[dev-dependencies]
55+
cw-multi-test = "0.16.2"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Awesomwasm 2023 CTF
2+
3+
## Challenge 06: *Hofund*
4+
5+
The contract allow anyone to propose themselves for the `owner` role of the contract, the rest of the users can vote in favor by sending a governance token.
6+
If a proposal was voted for with more than a third of the current supply, the user gets the `owner` role.
7+
8+
### Execute entry points:
9+
```rust
10+
pub enum ExecuteMsg {
11+
Propose {},
12+
ResolveProposal {},
13+
OwnerAction {
14+
action: CosmosMsg,
15+
},
16+
Receive(Cw20ReceiveMsg),
17+
}
18+
```
19+
20+
Please check the challenge's [integration_tests](./src/integration_tests.rs) for expected usage examples.
21+
You can use these tests as a base to create your exploit Proof of Concept.
22+
23+
**:house: Base scenario:**
24+
- The contract is newly instantiated
25+
26+
**:star: Goal for the challenge:**
27+
- Demonstrate how a proposer can obtain the owner role without controlling 1/3 of the total supply.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() {}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#[cfg(not(feature = "library"))]
2+
use cosmwasm_std::{
3+
entry_point, from_binary, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo,
4+
QueryRequest, Response, StdResult, Uint128, WasmQuery,
5+
};
6+
7+
use crate::error::ContractError;
8+
use crate::msg::{Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg};
9+
use crate::state::{Config, Proposal, CONFIG, PROPOSAL};
10+
use cw20::{BalanceResponse, Cw20QueryMsg, Cw20ReceiveMsg, TokenInfoResponse};
11+
12+
pub const DENOM: &str = "uawesome";
13+
14+
#[cfg_attr(not(feature = "library"), entry_point)]
15+
pub fn instantiate(
16+
deps: DepsMut,
17+
_env: Env,
18+
_info: MessageInfo,
19+
msg: InstantiateMsg,
20+
) -> Result<Response, ContractError> {
21+
let config = Config {
22+
voting_window: msg.window,
23+
voting_token: deps.api.addr_validate(&msg.token)?,
24+
owner: deps.api.addr_validate(&msg.owner)?,
25+
};
26+
CONFIG.save(deps.storage, &config)?;
27+
28+
Ok(Response::new()
29+
.add_attribute("action", "instantiate")
30+
.add_attribute("voting_window", msg.window.to_string())
31+
.add_attribute("voting_token", msg.token)
32+
.add_attribute("owner", msg.owner))
33+
}
34+
35+
#[cfg_attr(not(feature = "library"), entry_point)]
36+
pub fn execute(
37+
deps: DepsMut,
38+
env: Env,
39+
info: MessageInfo,
40+
msg: ExecuteMsg,
41+
) -> Result<Response, ContractError> {
42+
match msg {
43+
ExecuteMsg::Propose {} => propose(deps, env, info),
44+
ExecuteMsg::ResolveProposal {} => resolve_proposal(deps, env, info),
45+
ExecuteMsg::OwnerAction { action } => owner_action(deps, info, action),
46+
ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg),
47+
}
48+
}
49+
50+
/// Entry point when receiving CW20 tokens
51+
pub fn receive_cw20(
52+
deps: DepsMut,
53+
env: Env,
54+
info: MessageInfo,
55+
cw20_msg: Cw20ReceiveMsg,
56+
) -> Result<Response, ContractError> {
57+
let config = CONFIG.load(deps.storage)?;
58+
let current_proposal = PROPOSAL.load(deps.storage)?;
59+
60+
match from_binary(&cw20_msg.msg) {
61+
Ok(Cw20HookMsg::CastVote {}) => {
62+
if config.voting_token != info.sender {
63+
return Err(ContractError::Unauthorized {});
64+
}
65+
66+
if current_proposal
67+
.timestamp
68+
.plus_seconds(config.voting_window)
69+
< env.block.time
70+
{
71+
return Err(ContractError::VotingWindowClosed {});
72+
}
73+
74+
Ok(Response::default()
75+
.add_attribute("action", "Vote casting")
76+
.add_attribute("voter", cw20_msg.sender)
77+
.add_attribute("power", cw20_msg.amount))
78+
}
79+
_ => Ok(Response::default()),
80+
}
81+
}
82+
83+
/// Propose a new proposal
84+
pub fn propose(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
85+
let current_proposal = PROPOSAL.load(deps.storage);
86+
87+
if current_proposal.is_ok() {
88+
return Err(ContractError::ProposalAlreadyExists {});
89+
}
90+
91+
PROPOSAL.save(
92+
deps.storage,
93+
&Proposal {
94+
proposer: info.sender.clone(),
95+
timestamp: env.block.time,
96+
},
97+
)?;
98+
99+
Ok(Response::new()
100+
.add_attribute("action", "New proposal")
101+
.add_attribute("proposer", info.sender))
102+
}
103+
104+
/// Resolve an existing proposal
105+
pub fn resolve_proposal(
106+
deps: DepsMut,
107+
env: Env,
108+
_info: MessageInfo,
109+
) -> Result<Response, ContractError> {
110+
let config = CONFIG.load(deps.storage)?;
111+
let current_proposal = PROPOSAL.load(deps.storage)?;
112+
113+
if current_proposal
114+
.timestamp
115+
.plus_seconds(config.voting_window)
116+
< env.block.time
117+
{
118+
return Err(ContractError::ProposalNotReady {});
119+
}
120+
121+
let vtoken_info: TokenInfoResponse =
122+
deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
123+
contract_addr: config.voting_token.to_string(),
124+
msg: to_binary(&Cw20QueryMsg::TokenInfo {})?,
125+
}))?;
126+
127+
let balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
128+
contract_addr: config.voting_token.to_string(),
129+
msg: to_binary(&Cw20QueryMsg::Balance {
130+
address: env.contract.address.to_string(),
131+
})?,
132+
}))?;
133+
134+
let mut response = Response::new().add_attribute("action", "resolve_proposal");
135+
136+
if balance.balance >= (vtoken_info.total_supply / Uint128::from(3u32)) {
137+
CONFIG.update(deps.storage, |mut config| -> StdResult<_> {
138+
config.owner = current_proposal.proposer;
139+
Ok(config)
140+
})?;
141+
response = response.add_attribute("result", "Passed");
142+
} else {
143+
PROPOSAL.remove(deps.storage);
144+
response = response.add_attribute("result", "Failed");
145+
}
146+
147+
Ok(response)
148+
}
149+
150+
/// Entry point for owner to execute arbitrary Cosmos messages
151+
pub fn owner_action(
152+
deps: DepsMut,
153+
info: MessageInfo,
154+
msg: CosmosMsg,
155+
) -> Result<Response, ContractError> {
156+
let config = CONFIG.load(deps.storage)?;
157+
if config.owner != info.sender {
158+
return Err(ContractError::Unauthorized {});
159+
}
160+
161+
Ok(Response::new()
162+
.add_attribute("action", "owner_action")
163+
.add_message(msg))
164+
}
165+
166+
#[cfg_attr(not(feature = "library"), entry_point)]
167+
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
168+
match msg {
169+
QueryMsg::Config {} => to_binary(&query_config(deps)?),
170+
QueryMsg::Proposal {} => to_binary(&query_proposal(deps)?),
171+
QueryMsg::Balance {} => to_binary(&query_balance(deps, env)?),
172+
}
173+
}
174+
175+
/// Returns contract configuration
176+
pub fn query_config(deps: Deps) -> StdResult<Config> {
177+
CONFIG.load(deps.storage)
178+
}
179+
180+
/// Returns proposal information
181+
pub fn query_proposal(deps: Deps) -> StdResult<Proposal> {
182+
PROPOSAL.load(deps.storage)
183+
}
184+
185+
/// Returns balance of voting token in this contract
186+
pub fn query_balance(deps: Deps, env: Env) -> StdResult<Uint128> {
187+
let config = CONFIG.load(deps.storage)?;
188+
let balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
189+
contract_addr: config.voting_token.to_string(),
190+
msg: to_binary(&Cw20QueryMsg::Balance {
191+
address: env.contract.address.to_string(),
192+
})?,
193+
}))?;
194+
195+
Ok(balance.balance)
196+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use cosmwasm_std::StdError;
2+
use thiserror::Error;
3+
4+
#[derive(Error, Debug)]
5+
pub enum ContractError {
6+
#[error("{0}")]
7+
Std(#[from] StdError),
8+
9+
#[error("Unauthorized")]
10+
Unauthorized {},
11+
12+
#[error("The voting window is closed")]
13+
VotingWindowClosed {},
14+
15+
#[error("A proposal already exists")]
16+
ProposalAlreadyExists {},
17+
18+
#[error("No proposal exists")]
19+
ProposalDoesNotExists {},
20+
21+
#[error("The proposal is not ready yet")]
22+
ProposalNotReady {},
23+
}

0 commit comments

Comments
 (0)