├── .gitignore
├── README.md
├── SAMPLE_REPORT_TEMPLATE.md
├── awesomwasm-2023
├── POINTS.md
└── README.md
├── ctf-01
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
├── ctf-02
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
├── schema
│ ├── ctf-2.json
│ └── raw
│ │ ├── execute.json
│ │ ├── instantiate.json
│ │ ├── query.json
│ │ ├── response_to_get_user.json
│ │ └── response_to_get_voting_power.json
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
├── ctf-03
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
├── contracts
│ ├── .DS_Store
│ ├── flash_loan
│ │ ├── .cargo
│ │ │ └── config
│ │ ├── .editorconfig
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── README.md
│ │ └── src
│ │ │ ├── bin
│ │ │ └── schema.rs
│ │ │ ├── contract.rs
│ │ │ ├── error.rs
│ │ │ ├── integration_tests.rs
│ │ │ ├── lib.rs
│ │ │ └── state.rs
│ ├── mock_arb
│ │ ├── .cargo
│ │ │ └── config
│ │ ├── .editorconfig
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── README.md
│ │ └── src
│ │ │ ├── bin
│ │ │ └── schema.rs
│ │ │ ├── contract.rs
│ │ │ ├── error.rs
│ │ │ ├── integration_tests.rs
│ │ │ ├── lib.rs
│ │ │ └── state.rs
│ └── proxy
│ │ ├── .DS_Store
│ │ ├── .cargo
│ │ └── config
│ │ ├── .editorconfig
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── README.md
│ │ └── src
│ │ ├── bin
│ │ └── schema.rs
│ │ ├── contract.rs
│ │ ├── error.rs
│ │ ├── integration_tests.rs
│ │ ├── lib.rs
│ │ └── state.rs
└── packages
│ └── common
│ ├── Cargo.toml
│ └── src
│ ├── flash_loan.rs
│ ├── lib.rs
│ ├── mock_arb.rs
│ └── proxy.rs
├── ctf-04
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
├── ctf-05
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
├── ctf-06
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
├── ctf-07
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
├── ctf-08
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
├── ctf-09
├── .cargo
│ └── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
│ ├── bin
│ └── schema.rs
│ ├── contract.rs
│ ├── error.rs
│ ├── integration_tests.rs
│ ├── lib.rs
│ ├── msg.rs
│ └── state.rs
└── ctf-10
├── .cargo
└── config
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── NOTICE
├── README.md
└── src
├── bin
└── schema.rs
├── contract.rs
├── error.rs
├── integration_tests.rs
├── lib.rs
├── msg.rs
└── state.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | */target/
2 | .vscode/
3 | */contracts/*/target
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Oak Security CosmWasm CTF ⛳️
2 |
3 | Crack all our challenges and show the community that you know your way in security, either as an auditor or a security-minded developer! This CTF was run as a live event during AwesomWasm 2023, for info related to the event check [this other file](./awesomwasm-2023/README.md).
4 |
5 | Follow us on Twitter at [@SecurityOak](https://twitter.com/SecurityOak) to receive the latest news on Cosmos security and fresh audit reports.
6 |
7 | ## Getting started
8 |
9 | To get started with the challenges, please go to the [main](https://github.com/oak-security/cosmwasm-ctf/tree/main) branch. The 10 challenges follow no particular difficulty order, number 1 may not be easier than number 10 and the other way around. Each of them showcase a different security issue or exploitation techniques that we find during our security audits.
10 |
11 |
33 |
34 | After you have given your best to solve each of the challenges, we encourage you to create an "audit-like" report. You can follow [this template](./SAMPLE_REPORT_TEMPLATE.md) or any other that you consider suitable.
35 |
36 | Your results are ready now! we have published our own writeups so you can compare and check if your solutions are correct. Please visit:
37 | 1. [Capture The Flag ️Writeups — part 1](https://medium.com/oak-security/capture-the-flag-%EF%B8%8Fwriteups-awesomwasm-2023-pt-1-a40c6e506b49)
38 | 2. [Capture The Flag ️Writeups — part 2](https://medium.com/oak-security/capture-the-flag-%EF%B8%8Fwriteups-awesomwasm-2023-pt-2-cb3e9b297c0)
39 |
40 | In addition:
41 | 1. To view the proof of concept for the challenges, please visit the [poc-exploit](https://github.com/oak-security/cosmwasm-ctf/tree/poc-exploit) branch. The proof of concept is written as an `exploit()` test case and can be found in the `exploit.rs` file.
42 | 2. To view the fixed versions of the challenges, please visit the [fixed](https://github.com/oak-security/cosmwasm-ctf/tree/fixed) branch. All proof of concept test cases are prefixed with `#[ignore="bug is patched"]`, so they will not be automatically executed when running `cargo test`.
43 |
44 | ### Running test cases
45 |
46 | 1. Navigate into challenge folder.
47 |
48 | ```bash
49 | cd ctf-01/
50 | ```
51 |
52 | 2. Run tests
53 |
54 | ```bash
55 | cargo test
56 | ```
57 |
58 | ## Questions?
59 |
60 | Just open an issue in this repository to get an answer from our team.
61 |
--------------------------------------------------------------------------------
/SAMPLE_REPORT_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Sample Report Template
2 |
3 | ## Challenge 01: *Mjolnir*
4 |
5 | ### Description
6 |
7 | _The main description of your finding goes here! Please try to provide the following details, you don't have actually to list questions and answers but include the content within your paragraphs_:
8 | - _How this kind of vulnerability works at a high level?_
9 | - _What is incorrect in the code?_
10 | - _Where is it located? add the relative path to the file and the line number/s_
11 | - _What could an attacker achieve by successfully exploiting this issue? Who's affected?_
12 | - _What does an attacker have to do to exploit the issue?_
13 |
14 | ### Recommendation
15 |
16 | _Your recommendation to fix the issue goes here. It should solve the described finding and not introduce any new vulnerability.
17 | Try to be specific about what you would change; you are free to add code here as long as you indicate the file and lines._
18 |
19 | ### Proof of concept
20 |
21 | ```rust
22 | // code goes here
23 | ```
24 |
25 | ---
26 |
27 | ## Challenge 02: *Gungnir*
28 |
29 | ### Description
30 |
31 | The bug occurs in ...
32 |
33 | ### Recommendation
34 |
35 | The fix should be ...
36 |
37 | ### Proof of concept
38 |
39 | ```rust
40 | // code goes here
41 | ```
42 |
43 | ---
44 |
45 | ## Challenge 03: *Laevateinn*
46 |
47 | ### Description
48 |
49 | The bug occurs in ...
50 |
51 | ### Recommendation
52 |
53 | The fix should be ...
54 |
55 | ### Proof of concept
56 |
57 | ```rust
58 | // code goes here
59 | ```
60 |
61 | ---
62 |
63 | ## Challenge 04: *Gram*
64 |
65 | ### Description
66 |
67 | The bug occurs in ...
68 |
69 | ### Recommendation
70 |
71 | The fix should be ...
72 |
73 | ### Proof of concept
74 |
75 | ```rust
76 | // code goes here
77 | ```
78 |
79 | ---
80 |
81 | ## Challenge 05: *Draupnir*
82 |
83 | ### Description
84 |
85 | The bug occurs in ...
86 |
87 | ### Recommendation
88 |
89 | The fix should be ...
90 |
91 | ### Proof of concept
92 |
93 | ```rust
94 | // code goes here
95 | ```
96 |
97 | ---
98 |
99 | ## Challenge 06: *Hofund*
100 |
101 | ### Description
102 |
103 | The bug occurs in ...
104 |
105 | ### Recommendation
106 |
107 | The fix should be ...
108 |
109 | ### Proof of concept
110 |
111 | ```rust
112 | // code goes here
113 | ```
114 |
115 | ---
116 |
117 | ## Challenge 07: *Tyrfing*
118 |
119 | ### Description
120 |
121 | The bug occurs in ...
122 |
123 | ### Recommendation
124 |
125 | The fix should be ...
126 |
127 | ### Proof of concept
128 |
129 | ```rust
130 | // code goes here
131 | ```
132 |
133 | ---
134 |
135 | ## Challenge 08: *Gjallarhorn*
136 |
137 | ### Description
138 |
139 | The bug occurs in ...
140 |
141 | ### Recommendation
142 |
143 | The fix should be ...
144 |
145 | ### Proof of concept
146 |
147 | ```rust
148 | // code goes here
149 | ```
150 |
151 | ---
152 |
153 | ## Challenge 09: *Brisingamen*
154 |
155 | ### Description
156 |
157 | The bug occurs in ...
158 |
159 | ### Recommendation
160 |
161 | The fix should be ...
162 |
163 | ### Proof of concept
164 |
165 | ```rust
166 | // code goes here
167 | ```
168 |
169 | ---
170 |
171 | ## Challenge 10: *Mistilteinn*
172 |
173 | ### Description
174 |
175 | The bug occurs in ...
176 |
177 | ### Recommendation
178 |
179 | The fix should be ...
180 |
181 | ### Proof of concept
182 |
183 | ```rust
184 | // code goes here
185 | ```
186 |
--------------------------------------------------------------------------------
/awesomwasm-2023/POINTS.md:
--------------------------------------------------------------------------------
1 | # Points
2 |
3 | | Challenge number | Points per description of finding | Points per recommendation | Points per proof of concept test case | Total |
4 | |------------------ |:---------------------------------: |:-------------------------: |:-------------------------------------: |:-----: |
5 | | [01: Mjolnir](./ctf-01/README.md) | 20 | 25 | 45 | 90 |
6 | | [02: Gungnir](./ctf-02/README.md) | 40 | 35 | 75 | 150 |
7 | | [03: Laevateinn](./ctf-03/README.md) | 40 | 35 | 75 | 150 |
8 | | [04: Gram](./ctf-04/README.md) | 40 | 35 | 75 | 150 |
9 | | [05: Draupnir](./ctf-05/README.md) | 40 | 35 | 75 | 150 |
10 | | [06: Hofund](./ctf-06/README.md) | 40 | 35 | 75 | 150 |
11 | | [07: Tyrfing](./ctf-07/README.md) | 40 | 35 | 75 | 150 |
12 | | [08: Gjallarhorn](./ctf-08/README.md) | 40 | 35 | 75 | 150 |
13 | | [09: Brisingamen](./ctf-09/README.md) | 40 | 35 | 75 | 150 |
14 | | [10: Mistilteinn](./ctf-10/README.md) | 20 | 25 | 45 | 90 |
15 | | Total | | | | 1380 |
--------------------------------------------------------------------------------
/awesomwasm-2023/README.md:
--------------------------------------------------------------------------------
1 | > [!NOTE]
2 | > This file contains the information related to the Awesomwasm 2023 live event. If you want to work on the CTF, please check the [root README](https://github.com/oak-security/cosmwasm-ctf/tree/main)
3 |
4 | ---
5 |
6 | # Oak Security Capture The Flag ⛳️ - AwesomWasm Online Event
7 |
8 | Follow us on Twitter at [@SecurityOak](https://twitter.com/SecurityOak) to receive the latest information on the event. All the details will be first published there and then compiled in this `README`.
9 |
10 | The CTF ended on July 17th. Thanks to everyone for participating in it!
11 |
12 | ## Writeups
13 |
14 | 1. [Capture The Flag ️Writeups — AwesomWasm 2023 Pt. 1](https://medium.com/oak-security/capture-the-flag-%EF%B8%8Fwriteups-awesomwasm-2023-pt-1-a40c6e506b49)
15 | 2. [Capture The Flag ️Writeups — AwesomWasm 2023 Pt. 2](https://medium.com/oak-security/capture-the-flag-%EF%B8%8Fwriteups-awesomwasm-2023-pt-2-cb3e9b297c0)
16 |
17 | ## Navigation
18 |
19 | 1. To get started with the challenges, please visit the [main](https://github.com/oak-security/cosmwasm-ctf/tree/main) branch.
20 | 2. To view the proof of concept for the challenges, please visit the [poc-exploit](https://github.com/oak-security/cosmwasm-ctf/tree/poc-exploit) branch. The proof of concept is written as an `exploit()` test case and can be found in the `exploit.rs` file.
21 | 3. To view the fixed versions of the challenges, please visit the [fixed](https://github.com/oak-security/cosmwasm-ctf/tree/fixed) branch. All proof of concept test cases are prefixed with `#[ignore="bug is patched"]`, so they will not be automatically executed when running `cargo test`.
22 |
23 | ## Winners
24 |
25 | 1. [@forbiddnstars](https://twitter.com/forbiddnstars)
26 | 2. [@CruncherDefi](https://twitter.com/CruncherDefi)
27 | 3. [@jc0f0116](https://twitter.com/jc0f0116)
28 | 4. [@LeTurt_](https://twitter.com/LeTurt_)
29 | 5. [@i_be_jc](https://twitter.com/i_be_jc)
30 |
31 | Note that the three best submissions had all identified all security vulnerabilities. We used the quality and readability of the report as a tie-breaker to determine the winner. 🎉
32 |
33 | Here is the [official announcement](https://twitter.com/SecurityOak/status/1684462534244327424) in Twitter.
34 |
35 | ## Key information
36 |
37 | Our Capture The Flag event will be exclusively centered around CosmWasm smart contract security. As a participant, you will review the code of several smart contracts. To crack the challenges you will need to spot a wide variety of dangerous security vulnerabilities.
38 |
39 | The top 5 winners will be announced on [Twitter](https://twitter.com/securityoak) and receive shoutouts there.
40 |
41 | - **Number of challenges:** 10
42 | - **Start date:** 10th of July, 2023
43 | - **End Date:** 17th of July, 2023 (23:59)
44 | - **Location:** Worldwide! our event will be held online and asynchronously so anyone can participate.
45 | - **Recorded live stream** : https://www.youtube.com/live/YIb3UsLxlbQ?feature=share
46 | - **Results submission**: [Google Forms link](https://docs.google.com/forms/d/e/1FAIpQLSfc5Pr7sNCOIUP4aORM9JV4MTJi0Kl7QhPLHQSHX8Bgb9BUCw/viewform)
47 |
48 | Crack all our challenges and show the community that you know your way in security, either as an auditor or a security-minded developer!
49 |
50 | Join the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) to get in touch with us!
51 |
52 | Follow us on [Twitter](https://twitter.com/SecurityOak) to stay up to date.
53 |
54 | ## Getting started
55 |
56 | ### Quick links
57 |
58 | - [POINTS.md](POINTS.md) contains the overall points of each challenges.
59 | - [SAMPLE_REPORT_TEMPLATE.md](../SAMPLE_REPORT_TEMPLATE.md) contains a sample report template in Markdown format.
60 |
61 | ### Running test case
62 |
63 | 1. Navigate into challenge folder.
64 |
65 | ```bash
66 | cd ctf-01/
67 | ```
68 |
69 | 2. Run tests
70 |
71 | ```bash
72 | cargo test
73 | ```
74 |
75 | ## Questions?
76 |
77 | Just open an issue in this repository to get an answer from our team.
78 |
--------------------------------------------------------------------------------
/ctf-01/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-01/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-01/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-01"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 | cw-utils = "1.0.1"
48 | cw2 = "1.0.1"
49 | schemars = "0.8.10"
50 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
51 | thiserror = { version = "1.0.31" }
52 |
53 | [dev-dependencies]
54 | cw-multi-test = "0.16.2"
55 |
--------------------------------------------------------------------------------
/ctf-01/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-01/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 01: *Mjolnir*
4 |
5 | Smart contract that allows user deposit for a locked period before unlocking them.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | Deposit {},
11 | Withdraw { ids: Vec },
12 | }
13 | ```
14 |
15 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
16 |
17 | **:house: Base scenario:**
18 | - The contract contains initial funds.
19 | - `USER` deposits funds into the contract.
20 |
21 | **:star: Goal for the challenge:**
22 | - Demonstrate how an unprivileged user can drain all funds inside the contract.
23 |
24 | ## Scoring
25 |
26 | This challenge has been assigned a total of **90** points:
27 | - **20** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
28 | - **25** points will be awarded for a proper recommendation that fixes the issue.
29 | - If the report is deemed valid, the remaining **45** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
30 |
31 |
32 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
33 |
34 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
35 |
36 | ## Any questions?
37 |
38 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
39 |
40 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
41 |
--------------------------------------------------------------------------------
/ctf-01/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-01/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{
4 | to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
5 | };
6 |
7 | use crate::error::ContractError;
8 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
9 | use crate::state::{Lockup, LAST_ID, LOCKUPS};
10 | use cw_utils::must_pay;
11 |
12 | pub const DENOM: &str = "uawesome";
13 | pub const MINIMUM_DEPOSIT_AMOUNT: Uint128 = Uint128::new(10_000);
14 | pub const LOCK_PERIOD: u64 = 60 * 60 * 24;
15 |
16 | #[cfg_attr(not(feature = "library"), entry_point)]
17 | pub fn instantiate(
18 | _deps: DepsMut,
19 | _env: Env,
20 | _info: MessageInfo,
21 | _msg: InstantiateMsg,
22 | ) -> Result {
23 | Ok(Response::new().add_attribute("action", "instantiate"))
24 | }
25 |
26 | #[cfg_attr(not(feature = "library"), entry_point)]
27 | pub fn execute(
28 | deps: DepsMut,
29 | env: Env,
30 | info: MessageInfo,
31 | msg: ExecuteMsg,
32 | ) -> Result {
33 | match msg {
34 | ExecuteMsg::Deposit {} => deposit(deps, env, info),
35 | ExecuteMsg::Withdraw { ids } => withdraw(deps, env, info, ids),
36 | }
37 | }
38 |
39 | /// Deposit entry point for users
40 | pub fn deposit(deps: DepsMut, env: Env, info: MessageInfo) -> Result {
41 | // check minimum amount and denom
42 | let amount = must_pay(&info, DENOM).unwrap();
43 |
44 | if amount < MINIMUM_DEPOSIT_AMOUNT {
45 | return Err(ContractError::Unauthorized {});
46 | }
47 |
48 | // increment lock id
49 | let id = LAST_ID.load(deps.storage).unwrap_or(1);
50 | LAST_ID.save(deps.storage, &(id + 1)).unwrap();
51 |
52 | // create lockup
53 | let lock = Lockup {
54 | id,
55 | owner: info.sender,
56 | amount,
57 | release_timestamp: env.block.time.plus_seconds(LOCK_PERIOD),
58 | };
59 |
60 | // save lockup
61 | LOCKUPS.save(deps.storage, id, &lock).unwrap();
62 |
63 | Ok(Response::new()
64 | .add_attribute("action", "deposit")
65 | .add_attribute("id", lock.id.to_string())
66 | .add_attribute("owner", lock.owner)
67 | .add_attribute("amount", lock.amount)
68 | .add_attribute("release_timestamp", lock.release_timestamp.to_string()))
69 | }
70 |
71 | /// Withdrawal entry point for users
72 | pub fn withdraw(
73 | deps: DepsMut,
74 | env: Env,
75 | info: MessageInfo,
76 | ids: Vec,
77 | ) -> Result {
78 | let mut lockups: Vec = vec![];
79 | let mut total_amount = Uint128::zero();
80 |
81 | // fetch vaults to process
82 | for lockup_id in ids.clone() {
83 | let lockup = LOCKUPS.load(deps.storage, lockup_id).unwrap();
84 | lockups.push(lockup);
85 | }
86 |
87 | for lockup in lockups {
88 | // validate owner and time
89 | if lockup.owner != info.sender || env.block.time < lockup.release_timestamp {
90 | return Err(ContractError::Unauthorized {});
91 | }
92 |
93 | // increase total amount
94 | total_amount += lockup.amount;
95 |
96 | // remove from storage
97 | LOCKUPS.remove(deps.storage, lockup.id);
98 | }
99 |
100 | let msg = BankMsg::Send {
101 | to_address: info.sender.to_string(),
102 | amount: vec![Coin {
103 | denom: DENOM.to_string(),
104 | amount: total_amount,
105 | }],
106 | };
107 |
108 | Ok(Response::new()
109 | .add_attribute("action", "withdraw")
110 | .add_attribute("ids", format!("{:?}", ids))
111 | .add_attribute("total_amount", total_amount)
112 | .add_message(msg))
113 | }
114 |
115 | #[cfg_attr(not(feature = "library"), entry_point)]
116 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
117 | match msg {
118 | QueryMsg::GetLockup { id } => to_binary(&get_lockup(deps, id)?),
119 | }
120 | }
121 |
122 | /// Returns lockup information for a specified id
123 | pub fn get_lockup(deps: Deps, id: u64) -> StdResult {
124 | Ok(LOCKUPS.load(deps.storage, id).unwrap())
125 | }
126 |
--------------------------------------------------------------------------------
/ctf-01/src/error.rs:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/ctf-01/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::{
4 | contract::{DENOM, LOCK_PERIOD, MINIMUM_DEPOSIT_AMOUNT},
5 | msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
6 | state::Lockup,
7 | };
8 | use cosmwasm_std::{coin, Addr, Empty, Uint128};
9 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
10 |
11 | pub fn challenge_contract() -> Box> {
12 | let contract = ContractWrapper::new(
13 | crate::contract::execute,
14 | crate::contract::instantiate,
15 | crate::contract::query,
16 | );
17 | Box::new(contract)
18 | }
19 |
20 | pub const USER: &str = "user";
21 | pub const ADMIN: &str = "admin";
22 |
23 | pub fn proper_instantiate() -> (App, Addr) {
24 | let mut app = App::default();
25 | let cw_template_id = app.store_code(challenge_contract());
26 |
27 | // init contract
28 | let msg = InstantiateMsg { count: 1i32 };
29 | let contract_addr = app
30 | .instantiate_contract(
31 | cw_template_id,
32 | Addr::unchecked(ADMIN),
33 | &msg,
34 | &[],
35 | "test",
36 | None,
37 | )
38 | .unwrap();
39 |
40 | // mint funds to contract
41 | app = mint_tokens(
42 | app,
43 | contract_addr.to_string(),
44 | MINIMUM_DEPOSIT_AMOUNT * Uint128::new(10),
45 | );
46 |
47 | // mint funds to user
48 | app = mint_tokens(app, USER.to_string(), MINIMUM_DEPOSIT_AMOUNT);
49 |
50 | // deposit
51 | let msg = ExecuteMsg::Deposit {};
52 | let sender = Addr::unchecked(USER);
53 | app.execute_contract(
54 | sender.clone(),
55 | contract_addr.clone(),
56 | &msg,
57 | &[coin(MINIMUM_DEPOSIT_AMOUNT.u128(), DENOM)],
58 | )
59 | .unwrap();
60 |
61 | // verify no funds
62 | let balance = app.wrap().query_balance(USER, DENOM).unwrap().amount;
63 | assert_eq!(balance, Uint128::zero());
64 |
65 | (app, contract_addr)
66 | }
67 |
68 | pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App {
69 | app.sudo(cw_multi_test::SudoMsg::Bank(
70 | cw_multi_test::BankSudo::Mint {
71 | to_address: recipient.to_owned(),
72 | amount: vec![coin(amount.u128(), DENOM)],
73 | },
74 | ))
75 | .unwrap();
76 | app
77 | }
78 |
79 | #[test]
80 | fn basic_flow() {
81 | let (mut app, contract_addr) = proper_instantiate();
82 |
83 | let sender = Addr::unchecked(USER);
84 |
85 | // test query
86 | let msg = QueryMsg::GetLockup { id: 1 };
87 | let lockup: Lockup = app
88 | .wrap()
89 | .query_wasm_smart(contract_addr.clone(), &msg)
90 | .unwrap();
91 | assert_eq!(lockup.amount, MINIMUM_DEPOSIT_AMOUNT);
92 | assert_eq!(lockup.owner, sender);
93 |
94 | // fast forward 24 hrs
95 | app.update_block(|block| {
96 | block.time = block.time.plus_seconds(LOCK_PERIOD);
97 | });
98 |
99 | // test withdraw
100 | let msg = ExecuteMsg::Withdraw { ids: vec![1] };
101 | app.execute_contract(sender, contract_addr, &msg, &[])
102 | .unwrap();
103 |
104 | // verify funds received
105 | let balance = app.wrap().query_balance(USER, DENOM).unwrap().amount;
106 | assert_eq!(balance, MINIMUM_DEPOSIT_AMOUNT);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/ctf-01/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-01/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 |
3 | use crate::state::Lockup;
4 |
5 | #[cw_serde]
6 | pub struct InstantiateMsg {
7 | pub count: i32,
8 | }
9 |
10 | #[cw_serde]
11 | pub enum ExecuteMsg {
12 | Deposit {},
13 | Withdraw { ids: Vec },
14 | }
15 |
16 | #[cw_serde]
17 | #[derive(QueryResponses)]
18 | pub enum QueryMsg {
19 | #[returns(Lockup)]
20 | GetLockup { id: u64 },
21 | }
22 |
--------------------------------------------------------------------------------
/ctf-01/src/state.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 | use cosmwasm_std::{Addr, Timestamp, Uint128};
3 | use cw_storage_plus::{Item, Map};
4 |
5 | #[cw_serde]
6 | pub struct Lockup {
7 | /// Unique lockup identifier
8 | pub id: u64,
9 | /// Owner address
10 | pub owner: Addr,
11 | /// Locked amount
12 | pub amount: Uint128,
13 | /// Timestamp when the lockup can be withdrawn
14 | pub release_timestamp: Timestamp,
15 | }
16 |
17 | pub const LAST_ID: Item = Item::new("lock_id");
18 | pub const LOCKUPS: Map = Map::new("lockups");
19 |
--------------------------------------------------------------------------------
/ctf-02/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-02/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-02/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-02"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 = false
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 | cw-utils = "1.0.1"
48 | cw2 = "1.0.1"
49 | schemars = "0.8.10"
50 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
51 | thiserror = { version = "1.0.31" }
52 |
53 | [dev-dependencies]
54 | cw-multi-test = "0.16.2"
55 |
--------------------------------------------------------------------------------
/ctf-02/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-02/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 02: *Gungnir*
4 |
5 | Staking contract for users to lock their deposits for a fixed amount of time to generate voting power.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | Deposit {},
11 | Withdraw { amount: Uint128 },
12 | Stake { lock_amount: u128 },
13 | Unstake { unlock_amount: u128 },
14 | }
15 | ```
16 |
17 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
18 |
19 | **:house: Base scenario:**
20 | - The contract is newly instantiated with zero funds.
21 |
22 | **:star: Goal for the challenge:**
23 | - Demonstrate how an unprivileged user can achieve an unfair amount of voting power.
24 |
25 | ## Scoring
26 |
27 | This challenge has been assigned a total of **150** points:
28 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
29 | - **35** points will be awarded for a proper recommendation that fixes the issue.
30 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
31 |
32 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
33 |
34 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
35 |
36 | ## Any questions?
37 |
38 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
39 |
40 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
41 |
--------------------------------------------------------------------------------
/ctf-02/schema/ctf-2.json:
--------------------------------------------------------------------------------
1 | {
2 | "contract_name": "ctf-2",
3 | "contract_version": "0.1.0",
4 | "idl_version": "1.0.0",
5 | "instantiate": {
6 | "$schema": "http://json-schema.org/draft-07/schema#",
7 | "title": "InstantiateMsg",
8 | "type": "object",
9 | "additionalProperties": false
10 | },
11 | "execute": {
12 | "$schema": "http://json-schema.org/draft-07/schema#",
13 | "title": "ExecuteMsg",
14 | "oneOf": [
15 | {
16 | "type": "object",
17 | "required": [
18 | "deposit"
19 | ],
20 | "properties": {
21 | "deposit": {
22 | "type": "object",
23 | "additionalProperties": false
24 | }
25 | },
26 | "additionalProperties": false
27 | },
28 | {
29 | "type": "object",
30 | "required": [
31 | "withdraw"
32 | ],
33 | "properties": {
34 | "withdraw": {
35 | "type": "object",
36 | "required": [
37 | "amount"
38 | ],
39 | "properties": {
40 | "amount": {
41 | "$ref": "#/definitions/Uint128"
42 | }
43 | },
44 | "additionalProperties": false
45 | }
46 | },
47 | "additionalProperties": false
48 | },
49 | {
50 | "type": "object",
51 | "required": [
52 | "stake"
53 | ],
54 | "properties": {
55 | "stake": {
56 | "type": "object",
57 | "required": [
58 | "lock_amount"
59 | ],
60 | "properties": {
61 | "lock_amount": {
62 | "type": "integer",
63 | "format": "uint128",
64 | "minimum": 0.0
65 | }
66 | },
67 | "additionalProperties": false
68 | }
69 | },
70 | "additionalProperties": false
71 | },
72 | {
73 | "type": "object",
74 | "required": [
75 | "unstake"
76 | ],
77 | "properties": {
78 | "unstake": {
79 | "type": "object",
80 | "required": [
81 | "unlock_amount"
82 | ],
83 | "properties": {
84 | "unlock_amount": {
85 | "type": "integer",
86 | "format": "uint128",
87 | "minimum": 0.0
88 | }
89 | },
90 | "additionalProperties": false
91 | }
92 | },
93 | "additionalProperties": false
94 | }
95 | ],
96 | "definitions": {
97 | "Uint128": {
98 | "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
99 | "type": "string"
100 | }
101 | }
102 | },
103 | "query": {
104 | "$schema": "http://json-schema.org/draft-07/schema#",
105 | "title": "QueryMsg",
106 | "oneOf": [
107 | {
108 | "type": "object",
109 | "required": [
110 | "get_user"
111 | ],
112 | "properties": {
113 | "get_user": {
114 | "type": "object",
115 | "required": [
116 | "user"
117 | ],
118 | "properties": {
119 | "user": {
120 | "type": "string"
121 | }
122 | },
123 | "additionalProperties": false
124 | }
125 | },
126 | "additionalProperties": false
127 | },
128 | {
129 | "type": "object",
130 | "required": [
131 | "get_voting_power"
132 | ],
133 | "properties": {
134 | "get_voting_power": {
135 | "type": "object",
136 | "required": [
137 | "user"
138 | ],
139 | "properties": {
140 | "user": {
141 | "type": "string"
142 | }
143 | },
144 | "additionalProperties": false
145 | }
146 | },
147 | "additionalProperties": false
148 | }
149 | ]
150 | },
151 | "migrate": null,
152 | "sudo": null,
153 | "responses": {
154 | "get_user": {
155 | "$schema": "http://json-schema.org/draft-07/schema#",
156 | "title": "UserInfo",
157 | "type": "object",
158 | "required": [
159 | "released_time",
160 | "total_tokens",
161 | "voting_power"
162 | ],
163 | "properties": {
164 | "released_time": {
165 | "$ref": "#/definitions/Timestamp"
166 | },
167 | "total_tokens": {
168 | "$ref": "#/definitions/Uint128"
169 | },
170 | "voting_power": {
171 | "type": "integer",
172 | "format": "uint128",
173 | "minimum": 0.0
174 | }
175 | },
176 | "definitions": {
177 | "Timestamp": {
178 | "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
179 | "allOf": [
180 | {
181 | "$ref": "#/definitions/Uint64"
182 | }
183 | ]
184 | },
185 | "Uint128": {
186 | "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
187 | "type": "string"
188 | },
189 | "Uint64": {
190 | "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
191 | "type": "string"
192 | }
193 | }
194 | },
195 | "get_voting_power": {
196 | "$schema": "http://json-schema.org/draft-07/schema#",
197 | "title": "uint128",
198 | "type": "integer",
199 | "format": "uint128",
200 | "minimum": 0.0
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/ctf-02/schema/raw/execute.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "ExecuteMsg",
4 | "oneOf": [
5 | {
6 | "type": "object",
7 | "required": [
8 | "deposit"
9 | ],
10 | "properties": {
11 | "deposit": {
12 | "type": "object",
13 | "additionalProperties": false
14 | }
15 | },
16 | "additionalProperties": false
17 | },
18 | {
19 | "type": "object",
20 | "required": [
21 | "withdraw"
22 | ],
23 | "properties": {
24 | "withdraw": {
25 | "type": "object",
26 | "required": [
27 | "amount"
28 | ],
29 | "properties": {
30 | "amount": {
31 | "$ref": "#/definitions/Uint128"
32 | }
33 | },
34 | "additionalProperties": false
35 | }
36 | },
37 | "additionalProperties": false
38 | },
39 | {
40 | "type": "object",
41 | "required": [
42 | "stake"
43 | ],
44 | "properties": {
45 | "stake": {
46 | "type": "object",
47 | "required": [
48 | "lock_amount"
49 | ],
50 | "properties": {
51 | "lock_amount": {
52 | "type": "integer",
53 | "format": "uint128",
54 | "minimum": 0.0
55 | }
56 | },
57 | "additionalProperties": false
58 | }
59 | },
60 | "additionalProperties": false
61 | },
62 | {
63 | "type": "object",
64 | "required": [
65 | "unstake"
66 | ],
67 | "properties": {
68 | "unstake": {
69 | "type": "object",
70 | "required": [
71 | "unlock_amount"
72 | ],
73 | "properties": {
74 | "unlock_amount": {
75 | "type": "integer",
76 | "format": "uint128",
77 | "minimum": 0.0
78 | }
79 | },
80 | "additionalProperties": false
81 | }
82 | },
83 | "additionalProperties": false
84 | }
85 | ],
86 | "definitions": {
87 | "Uint128": {
88 | "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
89 | "type": "string"
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/ctf-02/schema/raw/instantiate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "InstantiateMsg",
4 | "type": "object",
5 | "additionalProperties": false
6 | }
7 |
--------------------------------------------------------------------------------
/ctf-02/schema/raw/query.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "QueryMsg",
4 | "oneOf": [
5 | {
6 | "type": "object",
7 | "required": [
8 | "get_user"
9 | ],
10 | "properties": {
11 | "get_user": {
12 | "type": "object",
13 | "required": [
14 | "user"
15 | ],
16 | "properties": {
17 | "user": {
18 | "type": "string"
19 | }
20 | },
21 | "additionalProperties": false
22 | }
23 | },
24 | "additionalProperties": false
25 | },
26 | {
27 | "type": "object",
28 | "required": [
29 | "get_voting_power"
30 | ],
31 | "properties": {
32 | "get_voting_power": {
33 | "type": "object",
34 | "required": [
35 | "user"
36 | ],
37 | "properties": {
38 | "user": {
39 | "type": "string"
40 | }
41 | },
42 | "additionalProperties": false
43 | }
44 | },
45 | "additionalProperties": false
46 | }
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/ctf-02/schema/raw/response_to_get_user.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "UserInfo",
4 | "type": "object",
5 | "required": [
6 | "released_time",
7 | "total_tokens",
8 | "voting_power"
9 | ],
10 | "properties": {
11 | "released_time": {
12 | "$ref": "#/definitions/Timestamp"
13 | },
14 | "total_tokens": {
15 | "$ref": "#/definitions/Uint128"
16 | },
17 | "voting_power": {
18 | "type": "integer",
19 | "format": "uint128",
20 | "minimum": 0.0
21 | }
22 | },
23 | "definitions": {
24 | "Timestamp": {
25 | "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
26 | "allOf": [
27 | {
28 | "$ref": "#/definitions/Uint64"
29 | }
30 | ]
31 | },
32 | "Uint128": {
33 | "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
34 | "type": "string"
35 | },
36 | "Uint64": {
37 | "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
38 | "type": "string"
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ctf-02/schema/raw/response_to_get_voting_power.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "uint128",
4 | "type": "integer",
5 | "format": "uint128",
6 | "minimum": 0.0
7 | }
8 |
--------------------------------------------------------------------------------
/ctf-02/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-02/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{
4 | coin, to_binary, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
5 | };
6 | use cw_utils::must_pay;
7 |
8 | use crate::error::ContractError;
9 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
10 | use crate::state::{UserInfo, VOTING_POWER};
11 |
12 | pub const DENOM: &str = "uawesome";
13 | pub const LOCK_PERIOD: u64 = 60 * 60 * 24; // One day
14 |
15 | #[cfg_attr(not(feature = "library"), entry_point)]
16 | pub fn instantiate(
17 | _deps: DepsMut,
18 | _env: Env,
19 | _info: MessageInfo,
20 | _msg: InstantiateMsg,
21 | ) -> Result {
22 | Ok(Response::new().add_attribute("action", "instantiate"))
23 | }
24 |
25 | #[cfg_attr(not(feature = "library"), entry_point)]
26 | pub fn execute(
27 | deps: DepsMut,
28 | env: Env,
29 | info: MessageInfo,
30 | msg: ExecuteMsg,
31 | ) -> Result {
32 | match msg {
33 | ExecuteMsg::Deposit {} => deposit(deps, info),
34 | ExecuteMsg::Withdraw { amount } => withdraw(deps, info, amount),
35 | ExecuteMsg::Stake { lock_amount } => stake(deps, env, info, lock_amount),
36 | ExecuteMsg::Unstake { unlock_amount } => unstake(deps, env, info, unlock_amount),
37 | }
38 | }
39 |
40 | /// Entry point for user to stake tokens
41 | pub fn deposit(deps: DepsMut, info: MessageInfo) -> Result {
42 | // validate denom
43 | let amount = must_pay(&info, DENOM).unwrap();
44 |
45 | // increase total stake
46 | let mut user = VOTING_POWER
47 | .load(deps.storage, &info.sender)
48 | .unwrap_or_default();
49 | user.total_tokens += amount;
50 |
51 | VOTING_POWER
52 | .save(deps.storage, &info.sender, &user)
53 | .unwrap();
54 |
55 | Ok(Response::new()
56 | .add_attribute("action", "deposit")
57 | .add_attribute("user", info.sender)
58 | .add_attribute("amount", amount))
59 | }
60 |
61 | /// Entry point for users to withdraw staked tokens
62 | pub fn withdraw(
63 | deps: DepsMut,
64 | info: MessageInfo,
65 | amount: Uint128,
66 | ) -> Result {
67 | // decrease total stake
68 | let mut user = VOTING_POWER.load(deps.storage, &info.sender).unwrap();
69 |
70 | user.total_tokens -= amount;
71 |
72 | // cannot withdraw staked tokens
73 | if user.total_tokens.u128() < user.voting_power {
74 | return Err(ContractError::Unauthorized {});
75 | }
76 |
77 | VOTING_POWER
78 | .save(deps.storage, &info.sender, &user)
79 | .unwrap();
80 |
81 | let msg = BankMsg::Send {
82 | to_address: info.sender.to_string(),
83 | amount: vec![coin(amount.u128(), DENOM)],
84 | };
85 |
86 | Ok(Response::new()
87 | .add_attribute("action", "withdraw")
88 | .add_attribute("user", info.sender)
89 | .add_attribute("amount", amount)
90 | .add_message(msg))
91 | }
92 |
93 | /// Entry point for user to stake tokens for voting power
94 | pub fn stake(
95 | deps: DepsMut,
96 | env: Env,
97 | info: MessageInfo,
98 | lock_amount: u128,
99 | ) -> Result {
100 | // increase voting power
101 | let mut user = VOTING_POWER.load(deps.storage, &info.sender).unwrap();
102 |
103 | user.voting_power += lock_amount;
104 |
105 | // cannot stake more than total tokens
106 | if user.voting_power > user.total_tokens.u128() {
107 | return Err(ContractError::Unauthorized {});
108 | }
109 |
110 | user.released_time = env.block.time.plus_seconds(LOCK_PERIOD);
111 |
112 | VOTING_POWER
113 | .save(deps.storage, &info.sender, &user)
114 | .unwrap();
115 |
116 | Ok(Response::new()
117 | .add_attribute("action", "stake")
118 | .add_attribute("lock_amount", lock_amount.to_string())
119 | .add_attribute("user.voting_power", user.voting_power.to_string()))
120 | }
121 |
122 | /// Entry point for users to decrease voting power
123 | pub fn unstake(
124 | deps: DepsMut,
125 | env: Env,
126 | info: MessageInfo,
127 | unlock_amount: u128,
128 | ) -> Result {
129 | // decrease voting power
130 | let mut user = VOTING_POWER.load(deps.storage, &info.sender).unwrap();
131 |
132 | // check release time
133 | if env.block.time < user.released_time {
134 | return Err(ContractError::Unauthorized {});
135 | }
136 |
137 | user.voting_power -= unlock_amount;
138 |
139 | VOTING_POWER
140 | .save(deps.storage, &info.sender, &user)
141 | .unwrap();
142 |
143 | Ok(Response::new()
144 | .add_attribute("action", "unstake")
145 | .add_attribute("unlock_amount", unlock_amount.to_string())
146 | .add_attribute("user.voting_power", user.voting_power.to_string()))
147 | }
148 |
149 | #[cfg_attr(not(feature = "library"), entry_point)]
150 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
151 | match msg {
152 | QueryMsg::GetUser { user } => to_binary(&get_user(deps, user)?),
153 | QueryMsg::GetVotingPower { user } => to_binary(&get_voting_power(deps, user)?),
154 | }
155 | }
156 |
157 | /// Returns user information from a specified user address
158 | pub fn get_user(deps: Deps, user: String) -> StdResult {
159 | let user_addr = deps.api.addr_validate(&user).unwrap();
160 | Ok(VOTING_POWER.load(deps.storage, &user_addr).unwrap())
161 | }
162 |
163 | /// Returns voting power for a specified user address
164 | pub fn get_voting_power(deps: Deps, user: String) -> StdResult {
165 | let user_addr = deps.api.addr_validate(&user).unwrap();
166 | Ok(VOTING_POWER
167 | .load(deps.storage, &user_addr)
168 | .unwrap()
169 | .voting_power)
170 | }
171 |
--------------------------------------------------------------------------------
/ctf-02/src/error.rs:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/ctf-02/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::{
4 | contract::{DENOM, LOCK_PERIOD},
5 | msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
6 | state::UserInfo,
7 | };
8 | use cosmwasm_std::{coin, Addr, Empty, Uint128};
9 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
10 |
11 | pub fn challenge_contract() -> Box> {
12 | let contract = ContractWrapper::new(
13 | crate::contract::execute,
14 | crate::contract::instantiate,
15 | crate::contract::query,
16 | );
17 | Box::new(contract)
18 | }
19 |
20 | pub const USER: &str = "user";
21 | pub const ADMIN: &str = "admin";
22 |
23 | pub fn proper_instantiate() -> (App, Addr) {
24 | let mut app = App::default();
25 | let cw_template_id = app.store_code(challenge_contract());
26 |
27 | // init contract
28 | let msg = InstantiateMsg {};
29 | let contract_addr = app
30 | .instantiate_contract(
31 | cw_template_id,
32 | Addr::unchecked(ADMIN),
33 | &msg,
34 | &[],
35 | "test",
36 | None,
37 | )
38 | .unwrap();
39 |
40 | (app, contract_addr)
41 | }
42 |
43 | pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App {
44 | app.sudo(cw_multi_test::SudoMsg::Bank(
45 | cw_multi_test::BankSudo::Mint {
46 | to_address: recipient,
47 | amount: vec![coin(amount.u128(), DENOM)],
48 | },
49 | ))
50 | .unwrap();
51 | app
52 | }
53 |
54 | #[test]
55 | fn basic_flow() {
56 | let (mut app, contract_addr) = proper_instantiate();
57 |
58 | let amount = Uint128::new(1_000);
59 |
60 | app = mint_tokens(app, USER.to_string(), amount);
61 | let sender = Addr::unchecked(USER);
62 |
63 | // deposit funds
64 | let msg = ExecuteMsg::Deposit {};
65 | app.execute_contract(
66 | sender.clone(),
67 | contract_addr.clone(),
68 | &msg,
69 | &[coin(amount.u128(), DENOM)],
70 | )
71 | .unwrap();
72 |
73 | // no funds left
74 | let balance = app.wrap().query_balance(USER, DENOM).unwrap().amount;
75 | assert_eq!(balance, Uint128::zero());
76 |
77 | // query user
78 | let msg = QueryMsg::GetUser {
79 | user: (&USER).to_string(),
80 | };
81 | let user: UserInfo = app
82 | .wrap()
83 | .query_wasm_smart(contract_addr.clone(), &msg)
84 | .unwrap();
85 | assert_eq!(user.total_tokens, amount);
86 |
87 | // cannot stake more than deposited
88 | let msg = ExecuteMsg::Stake {
89 | lock_amount: amount.u128() + 1,
90 | };
91 | app.execute_contract(sender.clone(), contract_addr.clone(), &msg, &[])
92 | .unwrap_err();
93 |
94 | // normal stake
95 | let msg = ExecuteMsg::Stake {
96 | lock_amount: amount.u128(),
97 | };
98 | app.execute_contract(sender.clone(), contract_addr.clone(), &msg, &[])
99 | .unwrap();
100 |
101 | // query voting power
102 | let msg = QueryMsg::GetVotingPower {
103 | user: (&USER).to_string(),
104 | };
105 | let voting_power: u128 = app
106 | .wrap()
107 | .query_wasm_smart(contract_addr.clone(), &msg)
108 | .unwrap();
109 | assert_eq!(voting_power, amount.u128());
110 |
111 | // cannot unstake before maturity
112 | let msg = ExecuteMsg::Unstake {
113 | unlock_amount: amount.u128(),
114 | };
115 | app.execute_contract(sender.clone(), contract_addr.clone(), &msg, &[])
116 | .unwrap_err();
117 |
118 | // cannot withdraw while staked
119 | let msg = ExecuteMsg::Withdraw { amount };
120 | app.execute_contract(sender.clone(), contract_addr.clone(), &msg, &[])
121 | .unwrap_err();
122 |
123 | // fast forward time
124 | app.update_block(|block| {
125 | block.time = block.time.plus_seconds(LOCK_PERIOD);
126 | });
127 |
128 | // normal unstake
129 | let msg = ExecuteMsg::Unstake {
130 | unlock_amount: amount.u128(),
131 | };
132 | app.execute_contract(sender.clone(), contract_addr.clone(), &msg, &[])
133 | .unwrap();
134 |
135 | // no more voting power
136 | let msg = QueryMsg::GetVotingPower {
137 | user: (&USER).to_string(),
138 | };
139 | let voting_power: u128 = app
140 | .wrap()
141 | .query_wasm_smart(contract_addr.clone(), &msg)
142 | .unwrap();
143 | assert_eq!(voting_power, 0_u128);
144 |
145 | // normal withdraw
146 | let msg = ExecuteMsg::Withdraw { amount };
147 | app.execute_contract(sender, contract_addr, &msg, &[])
148 | .unwrap();
149 |
150 | // funds are received
151 | let balance = app.wrap().query_balance(USER, DENOM).unwrap().amount;
152 | assert_eq!(balance, amount);
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/ctf-02/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-02/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 | use cosmwasm_std::Uint128;
3 |
4 | use crate::state::UserInfo;
5 |
6 | #[cw_serde]
7 | pub struct InstantiateMsg {}
8 |
9 | #[cw_serde]
10 | pub enum ExecuteMsg {
11 | Deposit {},
12 | Withdraw { amount: Uint128 },
13 | Stake { lock_amount: u128 },
14 | Unstake { unlock_amount: u128 },
15 | }
16 |
17 | #[cw_serde]
18 | #[derive(QueryResponses)]
19 | pub enum QueryMsg {
20 | #[returns(UserInfo)]
21 | GetUser { user: String },
22 |
23 | #[returns(u128)]
24 | GetVotingPower { user: String },
25 | }
26 |
--------------------------------------------------------------------------------
/ctf-02/src/state.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 | use cosmwasm_std::{Addr, Timestamp, Uint128};
3 | use cw_storage_plus::Map;
4 |
5 | #[cw_serde]
6 | #[derive(Default)]
7 | pub struct UserInfo {
8 | /// Total tokens staked
9 | pub total_tokens: Uint128,
10 | /// User voting power
11 | pub voting_power: u128,
12 | /// Release time to withdraw staked tokens
13 | pub released_time: Timestamp,
14 | }
15 |
16 | pub const VOTING_POWER: Map<&Addr, UserInfo> = Map::new("voting_power");
17 |
--------------------------------------------------------------------------------
/ctf-03/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "contracts/proxy",
4 | "contracts/flash_loan",
5 | "contracts/mock_arb",
6 | "packages/common",
7 | ]
8 |
9 | [workspace.dependencies]
10 | cw-multi-test = { git = "https://github.com/oak-security/cw-multi-test.git", branch = "main" }
11 | cosmwasm-schema = { version = "=1.1.9" }
12 | cosmwasm-std = { version = "1.1.9" }
13 | cw-storage-plus = "1.0.1"
14 | cw2 = "1.0.1"
15 | schemars = "0.8.10"
16 | serde = { version = "1.0" }
17 | cw-utils = "1.0.1"
18 | thiserror = { version = "1.0.31" }
--------------------------------------------------------------------------------
/ctf-03/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-03/README.md:
--------------------------------------------------------------------------------
1 | # Oak Security CosmWasm CTF
2 |
3 | ## Challenge 03: *Laevateinn*
4 |
5 | Flash loan protocol which allows users to execute a [flash loan](https://chain.link/education-hub/flash-loans) using the proxy contract.
6 |
7 | ### Flash loan contract entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | SetProxyAddr { proxy_addr: String },
11 | FlashLoan {},
12 | SettleLoan {},
13 | WithdrawFunds { recipient: Addr },
14 | TransferOwner { new_owner: Addr },
15 | }
16 | ```
17 |
18 | ### Proxy contract entry points:
19 | ```rust
20 | pub enum ExecuteMsg {
21 | RequestFlashLoan { recipient: Addr, msg: Binary },
22 | }
23 | ```
24 |
25 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
26 |
27 | **:house: Base scenario:**
28 | - The flash loan contract will have initial funds deposited.
29 | - Proxy contract is configured to flash loan contract.
30 |
31 | **:star: Goal for the challenge:**
32 | - Demonstrate how an unprivileged user can drain all funds from the flash loan contract.
33 |
34 | ## Scoring
35 |
36 | This challenge has been assigned a total of **150** points:
37 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
38 | - **35** points will be awarded for a proper recommendation that fixes the issue.
39 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
40 |
41 |
42 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
43 |
44 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
45 |
46 | ## Any questions?
47 |
48 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
49 |
50 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
51 |
--------------------------------------------------------------------------------
/ctf-03/contracts/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oak-security/cosmwasm-ctf/c32fa94947f4199596d26696afeb3de0ab3d9cca/ctf-03/contracts/.DS_Store
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "flash_loan"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 = { workspace = true }
44 | cosmwasm-std = { workspace = true }
45 | cw-storage-plus = { workspace = true }
46 | cw2 = { workspace = true }
47 | schemars = { workspace = true }
48 | serde = { workspace = true }
49 | thiserror = { workspace = true }
50 | cw-multi-test = { workspace = true }
51 | common = { path = "../../packages/common" }
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/README.md:
--------------------------------------------------------------------------------
1 | # Oak Security CosmWasm CTF
2 |
3 | ## Challenge 03: *Laevateinn*
4 |
5 | The flash loan contract is the core logic for the flash loan implementation.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | SetProxyAddr { proxy_addr: String },
11 | FlashLoan {},
12 | SettleLoan {},
13 | WithdrawFunds { recipient: Addr },
14 | TransferOwner { new_owner: Addr },
15 | }
16 | ```
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{
4 | coin, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
5 | };
6 |
7 | use crate::error::ContractError;
8 | use crate::state::{CONFIG, FLASH_LOAN};
9 | use common::flash_loan::{Config, ExecuteMsg, FlashLoanState, InstantiateMsg, QueryMsg};
10 |
11 | pub const DENOM: &str = "uawesome";
12 |
13 | #[cfg_attr(not(feature = "library"), entry_point)]
14 | pub fn instantiate(
15 | deps: DepsMut,
16 | _env: Env,
17 | info: MessageInfo,
18 | _msg: InstantiateMsg,
19 | ) -> Result {
20 | let state = Config {
21 | proxy_addr: None,
22 | owner: info.sender.clone(),
23 | };
24 | CONFIG.save(deps.storage, &state)?;
25 |
26 | let state = FlashLoanState {
27 | requested_amount: None,
28 | };
29 |
30 | FLASH_LOAN.save(deps.storage, &state)?;
31 |
32 | Ok(Response::new()
33 | .add_attribute("action", "instantiate")
34 | .add_attribute("owner", info.sender))
35 | }
36 |
37 | #[cfg_attr(not(feature = "library"), entry_point)]
38 | pub fn execute(
39 | deps: DepsMut,
40 | env: Env,
41 | info: MessageInfo,
42 | msg: ExecuteMsg,
43 | ) -> Result {
44 | match msg {
45 | ExecuteMsg::FlashLoan {} => flash_loan(deps, env, info),
46 | ExecuteMsg::SettleLoan {} => settle_loan(deps, env, info),
47 | ExecuteMsg::SetProxyAddr { proxy_addr } => set_proxy_addr(deps, info, proxy_addr),
48 | ExecuteMsg::WithdrawFunds { recipient } => withdraw_funds(deps, env, info, recipient),
49 | ExecuteMsg::TransferOwner { new_owner } => transfer_owner(deps, info, new_owner),
50 | }
51 | }
52 |
53 | /// Entry point to start a flash loan by the proxy contract
54 | pub fn flash_loan(deps: DepsMut, env: Env, info: MessageInfo) -> Result {
55 | let config = CONFIG.load(deps.storage)?;
56 |
57 | let mut state = FLASH_LOAN.load(deps.storage)?;
58 |
59 | if state.requested_amount.is_some() {
60 | return Err(ContractError::OngoingFlashLoan {});
61 | }
62 |
63 | if config.proxy_addr.is_none() {
64 | return Err(ContractError::ProxyAddressNotSet {});
65 | }
66 |
67 | if info.sender != config.proxy_addr.unwrap() {
68 | return Err(ContractError::Unauthorized {});
69 | }
70 |
71 | let balance = deps.querier.query_balance(env.contract.address, DENOM)?;
72 |
73 | if balance.amount.is_zero() {
74 | return Err(ContractError::ZeroBalance {});
75 | }
76 |
77 | let msg = BankMsg::Send {
78 | to_address: info.sender.to_string(),
79 | amount: vec![coin(balance.amount.u128(), DENOM)],
80 | };
81 |
82 | state.requested_amount = Some(balance.amount);
83 |
84 | FLASH_LOAN.save(deps.storage, &state)?;
85 |
86 | Ok(Response::new()
87 | .add_attribute("action", "flash_loan")
88 | .add_attribute("amount", state.requested_amount.unwrap().to_string())
89 | .add_message(msg))
90 | }
91 |
92 | /// Entry point to settle a flash loan from the proxy contract
93 | pub fn settle_loan(deps: DepsMut, env: Env, info: MessageInfo) -> Result {
94 | let config = CONFIG.load(deps.storage)?;
95 |
96 | let mut state = FLASH_LOAN.load(deps.storage)?;
97 |
98 | if state.requested_amount.is_none() {
99 | return Err(ContractError::NoFlashLoan {});
100 | }
101 |
102 | if config.proxy_addr.is_none() {
103 | return Err(ContractError::ProxyAddressNotSet {});
104 | }
105 |
106 | if info.sender != config.proxy_addr.unwrap() {
107 | return Err(ContractError::Unauthorized {});
108 | }
109 |
110 | let balance = deps.querier.query_balance(env.contract.address, DENOM)?;
111 |
112 | if balance.amount < state.requested_amount.unwrap() {
113 | return Err(ContractError::RequestedTooHighAmount {});
114 | }
115 |
116 | state.requested_amount = None;
117 |
118 | FLASH_LOAN.save(deps.storage, &state)?;
119 |
120 | Ok(Response::new().add_attribute("action", "settle_loan"))
121 | }
122 |
123 | /// Entry point for owner to set proxy address
124 | pub fn set_proxy_addr(
125 | deps: DepsMut,
126 | info: MessageInfo,
127 | proxy_addr: String,
128 | ) -> Result {
129 | let mut config = CONFIG.load(deps.storage)?;
130 |
131 | if info.sender != config.owner {
132 | return Err(ContractError::Unauthorized {});
133 | }
134 |
135 | if config.proxy_addr.is_none() {
136 | config.proxy_addr = Some(deps.api.addr_validate(&proxy_addr).unwrap());
137 | } else {
138 | return Err(ContractError::ProxyAddressAlreadySet {});
139 | }
140 |
141 | CONFIG.save(deps.storage, &config)?;
142 |
143 | Ok(Response::new().add_attribute("action", "set_proxy_addr"))
144 | }
145 |
146 | /// Entry point for owner to withdraw funds
147 | pub fn withdraw_funds(
148 | deps: DepsMut,
149 | env: Env,
150 | info: MessageInfo,
151 | recipient: Addr,
152 | ) -> Result {
153 | let config = CONFIG.load(deps.storage)?;
154 |
155 | if info.sender != config.owner {
156 | return Err(ContractError::Unauthorized {});
157 | }
158 |
159 | let balance = deps.querier.query_balance(env.contract.address, DENOM)?;
160 |
161 | if balance.amount.is_zero() {
162 | return Err(ContractError::ZeroBalance {});
163 | }
164 |
165 | let msg = BankMsg::Send {
166 | to_address: recipient.to_string(),
167 | amount: vec![coin(balance.amount.u128(), DENOM)],
168 | };
169 |
170 | Ok(Response::new()
171 | .add_attribute("action", "withdraw_funds")
172 | .add_attribute("amount", balance.amount)
173 | .add_message(msg))
174 | }
175 |
176 | /// Entry point to transfer ownership
177 | pub fn transfer_owner(
178 | deps: DepsMut,
179 | info: MessageInfo,
180 | new_owner: Addr,
181 | ) -> Result {
182 | let mut config = CONFIG.load(deps.storage)?;
183 |
184 | if !is_trusted(&info.sender, &config) {
185 | return Err(ContractError::Unauthorized {});
186 | }
187 |
188 | config.owner = new_owner;
189 |
190 | CONFIG.save(deps.storage, &config)?;
191 |
192 | Ok(Response::new().add_attribute("action", "transfer_owner"))
193 | }
194 |
195 | pub fn is_trusted(sender: &Addr, config: &Config) -> bool {
196 | let mut trusted = false;
197 |
198 | if sender == config.owner {
199 | trusted = true;
200 | }
201 |
202 | if config.proxy_addr.is_some() && sender == config.proxy_addr.as_ref().unwrap() {
203 | trusted = true;
204 | }
205 |
206 | trusted
207 | }
208 |
209 | #[cfg_attr(not(feature = "library"), entry_point)]
210 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
211 | match msg {
212 | QueryMsg::Config {} => to_binary(&query_config(deps)?),
213 | QueryMsg::FlashLoanState {} => to_binary(&query_flash_loan_state(deps)?),
214 | }
215 | }
216 |
217 | /// Returns contract configuration
218 | pub fn query_config(deps: Deps) -> StdResult {
219 | let config = CONFIG.load(deps.storage)?;
220 | Ok(config)
221 | }
222 |
223 | /// Returns current flash loan state
224 | pub fn query_flash_loan_state(deps: Deps) -> StdResult {
225 | let state = FLASH_LOAN.load(deps.storage)?;
226 | Ok(state)
227 | }
228 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/src/error.rs:
--------------------------------------------------------------------------------
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("Proxy address is not set")]
13 | ProxyAddressNotSet {},
14 |
15 | #[error("Proxy address is already set")]
16 | ProxyAddressAlreadySet {},
17 |
18 | #[error("Contract have zero balance")]
19 | ZeroBalance {},
20 |
21 | #[error("Flash loan is already executed")]
22 | OngoingFlashLoan {},
23 |
24 | #[error("No flash loan is executed")]
25 | NoFlashLoan {},
26 |
27 | #[error("Requested amount is too large")]
28 | RequestedTooHighAmount {},
29 | }
30 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::contract::DENOM;
4 | use common::flash_loan::{Config, ExecuteMsg, FlashLoanState, InstantiateMsg, QueryMsg};
5 | use cosmwasm_std::{coin, Addr, Empty, Uint128};
6 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
7 |
8 | pub fn challenge_contract() -> Box> {
9 | let contract = ContractWrapper::new(
10 | crate::contract::execute,
11 | crate::contract::instantiate,
12 | crate::contract::query,
13 | );
14 | Box::new(contract)
15 | }
16 |
17 | pub const USER: &str = "user";
18 | pub const ADMIN: &str = "admin";
19 |
20 | pub fn proper_instantiate() -> (App, Addr) {
21 | let mut app = App::default();
22 | let cw_template_id = app.store_code(challenge_contract());
23 |
24 | // init contract
25 | let msg = InstantiateMsg {};
26 | let contract_addr = app
27 | .instantiate_contract(
28 | cw_template_id,
29 | Addr::unchecked(ADMIN),
30 | &msg,
31 | &[],
32 | "test",
33 | None,
34 | )
35 | .unwrap();
36 |
37 | // mint funds to contract
38 | app = mint_tokens(app, contract_addr.to_string(), Uint128::new(10_000));
39 |
40 | (app, contract_addr)
41 | }
42 |
43 | pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App {
44 | app.sudo(cw_multi_test::SudoMsg::Bank(
45 | cw_multi_test::BankSudo::Mint {
46 | to_address: recipient,
47 | amount: vec![coin(amount.u128(), DENOM)],
48 | },
49 | ))
50 | .unwrap();
51 | app
52 | }
53 |
54 | #[test]
55 | fn basic_flow() {
56 | let (mut app, contract_addr) = proper_instantiate();
57 |
58 | // check query
59 | let config: Config = app
60 | .wrap()
61 | .query_wasm_smart(contract_addr.clone(), &QueryMsg::Config {})
62 | .unwrap();
63 | assert_eq!(config.owner.to_string(), ADMIN);
64 | assert_eq!(config.proxy_addr, None);
65 |
66 | let state: FlashLoanState = app
67 | .wrap()
68 | .query_wasm_smart(contract_addr.clone(), &QueryMsg::FlashLoanState {})
69 | .unwrap();
70 | assert_eq!(state.requested_amount, None);
71 |
72 | // try set proxy addr
73 | app.execute_contract(
74 | Addr::unchecked(USER),
75 | contract_addr.clone(),
76 | &ExecuteMsg::SetProxyAddr {
77 | proxy_addr: USER.to_owned(),
78 | },
79 | &[],
80 | )
81 | .unwrap_err();
82 |
83 | app.execute_contract(
84 | Addr::unchecked(ADMIN),
85 | contract_addr.clone(),
86 | &ExecuteMsg::SetProxyAddr {
87 | proxy_addr: ADMIN.to_owned(),
88 | },
89 | &[],
90 | )
91 | .unwrap();
92 |
93 | // execute a flash loan
94 | app.execute_contract(
95 | Addr::unchecked(ADMIN),
96 | contract_addr.clone(),
97 | &ExecuteMsg::FlashLoan {},
98 | &[],
99 | )
100 | .unwrap();
101 |
102 | // cannot execute twice
103 | app.execute_contract(
104 | Addr::unchecked(ADMIN),
105 | contract_addr.clone(),
106 | &ExecuteMsg::FlashLoan {},
107 | &[],
108 | )
109 | .unwrap_err();
110 |
111 | // ensure funds received
112 | let balance = app.wrap().query_balance(ADMIN, DENOM).unwrap();
113 | assert_eq!(balance.amount, Uint128::new(10_000));
114 |
115 | // only proxy address can settle loan
116 | app.execute_contract(
117 | Addr::unchecked(USER),
118 | contract_addr.clone(),
119 | &ExecuteMsg::SettleLoan {},
120 | &[],
121 | )
122 | .unwrap_err();
123 |
124 | // cannot settle when amount not returned
125 | app.execute_contract(
126 | Addr::unchecked(ADMIN),
127 | contract_addr.clone(),
128 | &ExecuteMsg::SettleLoan {},
129 | &[],
130 | )
131 | .unwrap_err();
132 |
133 | // can only settle after fully paid
134 | app.send_tokens(
135 | Addr::unchecked(ADMIN),
136 | contract_addr.clone(),
137 | &[coin(9999_u128, DENOM)],
138 | )
139 | .unwrap();
140 |
141 | app.execute_contract(
142 | Addr::unchecked(ADMIN),
143 | contract_addr.clone(),
144 | &ExecuteMsg::SettleLoan {},
145 | &[],
146 | )
147 | .unwrap_err();
148 |
149 | // send the rest and pay off loan
150 | app.send_tokens(
151 | Addr::unchecked(ADMIN),
152 | contract_addr.clone(),
153 | &[coin(1_u128, DENOM)],
154 | )
155 | .unwrap();
156 |
157 | app.execute_contract(
158 | Addr::unchecked(ADMIN),
159 | contract_addr.clone(),
160 | &ExecuteMsg::SettleLoan {},
161 | &[],
162 | )
163 | .unwrap();
164 |
165 | // cannot settle twice
166 | app.execute_contract(
167 | Addr::unchecked(ADMIN),
168 | contract_addr.clone(),
169 | &ExecuteMsg::SettleLoan {},
170 | &[],
171 | )
172 | .unwrap_err();
173 |
174 | // contract have funds
175 | let balance = app
176 | .wrap()
177 | .query_balance(contract_addr.to_string(), DENOM)
178 | .unwrap();
179 | assert_eq!(balance.amount, Uint128::new(10_000));
180 |
181 | // withdraw funds
182 | app.execute_contract(
183 | Addr::unchecked(ADMIN),
184 | contract_addr.clone(),
185 | &ExecuteMsg::WithdrawFunds {
186 | recipient: Addr::unchecked(ADMIN),
187 | },
188 | &[],
189 | )
190 | .unwrap();
191 |
192 | let balance = app.wrap().query_balance(ADMIN, DENOM).unwrap();
193 | assert_eq!(balance.amount, Uint128::new(10_000));
194 |
195 | let balance = app
196 | .wrap()
197 | .query_balance(contract_addr.to_string(), DENOM)
198 | .unwrap();
199 | assert_eq!(balance.amount, Uint128::zero());
200 |
201 | // change admin works
202 | app.execute_contract(
203 | Addr::unchecked(ADMIN),
204 | contract_addr,
205 | &ExecuteMsg::TransferOwner {
206 | new_owner: Addr::unchecked(USER),
207 | },
208 | &[],
209 | )
210 | .unwrap();
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod state;
5 |
6 | pub use crate::error::ContractError;
7 |
--------------------------------------------------------------------------------
/ctf-03/contracts/flash_loan/src/state.rs:
--------------------------------------------------------------------------------
1 | use common::flash_loan::{Config, FlashLoanState};
2 | use cw_storage_plus::Item;
3 |
4 | pub const CONFIG: Item = Item::new("config");
5 | pub const FLASH_LOAN: Item = Item::new("flash_loan");
6 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mock_arb"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 = { workspace = true }
44 | cosmwasm-std = { workspace = true }
45 | cw-storage-plus = { workspace = true }
46 | cw2 = { workspace = true }
47 | schemars = { workspace = true }
48 | serde = { workspace = true }
49 | thiserror = { workspace = true }
50 | cw-utils = { workspace = true }
51 | cw-multi-test = { workspace = true }
52 | flash_loan = { path = "../flash_loan" }
53 | common = { path = "../../packages/common" }
54 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/README.md:
--------------------------------------------------------------------------------
1 | # Oak Security CosmWasm CTF
2 |
3 | ## Challenge 03: *Laevateinn*
4 |
5 | Mock contract for integration tests.
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
4 | use cw_utils::must_pay;
5 |
6 | use crate::error::ContractError;
7 |
8 | use common::mock_arb::{ExecuteMsg, InstantiateMsg, QueryMsg};
9 |
10 | pub const DENOM: &str = "uawesome";
11 |
12 | #[cfg_attr(not(feature = "library"), entry_point)]
13 | pub fn instantiate(
14 | _deps: DepsMut,
15 | _env: Env,
16 | _info: MessageInfo,
17 | _msg: InstantiateMsg,
18 | ) -> Result {
19 | Ok(Response::new().add_attribute("action", "instantiate"))
20 | }
21 |
22 | #[cfg_attr(not(feature = "library"), entry_point)]
23 | pub fn execute(
24 | deps: DepsMut,
25 | env: Env,
26 | info: MessageInfo,
27 | msg: ExecuteMsg,
28 | ) -> Result {
29 | match msg {
30 | ExecuteMsg::Arbitrage { recipient } => arbitrage(deps, env, info, recipient),
31 | }
32 | }
33 |
34 | pub fn arbitrage(
35 | deps: DepsMut,
36 | env: Env,
37 | info: MessageInfo,
38 | recipient: Addr,
39 | ) -> Result {
40 | let received_amount = must_pay(&info, DENOM).unwrap();
41 |
42 | // Some arbitrage stuffs..
43 |
44 | let contract_balance = deps
45 | .querier
46 | .query_balance(env.contract.address.to_string(), DENOM)
47 | .unwrap();
48 |
49 | // This contract does not intend to hold any funds
50 | let msg = BankMsg::Send {
51 | to_address: recipient.to_string(),
52 | amount: vec![contract_balance.clone()],
53 | };
54 |
55 | Ok(Response::new()
56 | .add_attribute("action", "arbitrage")
57 | .add_attribute("sent_amount", received_amount)
58 | .add_attribute("contract_balance", contract_balance.amount)
59 | .add_message(msg))
60 | }
61 |
62 | #[cfg_attr(not(feature = "library"), entry_point)]
63 | pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
64 | match msg {}
65 | }
66 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/src/error.rs:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {}
3 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod state;
5 |
6 | pub use crate::error::ContractError;
7 |
--------------------------------------------------------------------------------
/ctf-03/contracts/mock_arb/src/state.rs:
--------------------------------------------------------------------------------
1 | use common::mock_arb::Config;
2 | use cw_storage_plus::Item;
3 |
4 | pub const CONFIG: Item = Item::new("config");
5 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oak-security/cosmwasm-ctf/c32fa94947f4199596d26696afeb3de0ab3d9cca/ctf-03/contracts/proxy/.DS_Store
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "proxy"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 = { workspace = true }
44 | cosmwasm-std = { workspace = true }
45 | cw-storage-plus = { workspace = true }
46 | cw2 = { workspace = true }
47 | schemars = { workspace = true }
48 | serde = { workspace = true }
49 | thiserror = { workspace = true }
50 | cw-multi-test = { workspace = true }
51 | flash_loan = { path = "../flash_loan" }
52 | mock_arb = { path = "../mock_arb" }
53 | common = { path = "../../packages/common" }
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/README.md:
--------------------------------------------------------------------------------
1 | # Oak Security CosmWasm CTF
2 |
3 | ## Challenge 03: *Laevateinn*
4 |
5 | The proxy contract is the entry point for user to execute flash loan into the flash loan contract.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | RequestFlashLoan { recipient: Addr, msg: Binary },
11 | }
12 | ```
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{
4 | to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
5 | WasmMsg,
6 | };
7 |
8 | use crate::error::ContractError;
9 | use crate::state::CONFIG;
10 | use common::flash_loan::{
11 | Config as FlashLoanConfig, ExecuteMsg as FlashLoanExecuteMsg, QueryMsg as FlashLoanQueryMsg,
12 | };
13 | use common::proxy::{Config, ExecuteMsg, InstantiateMsg, QueryMsg};
14 |
15 | pub const DENOM: &str = "uawesome";
16 |
17 | #[cfg_attr(not(feature = "library"), entry_point)]
18 | pub fn instantiate(
19 | deps: DepsMut,
20 | _env: Env,
21 | _info: MessageInfo,
22 | msg: InstantiateMsg,
23 | ) -> Result {
24 | let flash_loan_addr = deps.api.addr_validate(&msg.flash_loan_addr).unwrap();
25 |
26 | let state = Config { flash_loan_addr };
27 | CONFIG.save(deps.storage, &state)?;
28 |
29 | Ok(Response::new()
30 | .add_attribute("action", "instantiate")
31 | .add_attribute("flash_loan_addr", msg.flash_loan_addr))
32 | }
33 |
34 | #[cfg_attr(not(feature = "library"), entry_point)]
35 | pub fn execute(
36 | deps: DepsMut,
37 | env: Env,
38 | info: MessageInfo,
39 | msg: ExecuteMsg,
40 | ) -> Result {
41 | match msg {
42 | ExecuteMsg::RequestFlashLoan { recipient, msg } => {
43 | request_flash_loan(deps, env, info, recipient, msg)
44 | }
45 | }
46 | }
47 |
48 | /// Entry point for user to request flash loan
49 | pub fn request_flash_loan(
50 | deps: DepsMut,
51 | env: Env,
52 | _info: MessageInfo,
53 | recipient: Addr,
54 | msg: Binary,
55 | ) -> Result {
56 | let config = CONFIG.load(deps.storage)?;
57 |
58 | // Disallow calling flash loan addr
59 | if recipient == config.flash_loan_addr {
60 | return Err(ContractError::CallToFlashLoan {});
61 | }
62 |
63 | let flash_loan_config: FlashLoanConfig = deps.querier.query_wasm_smart(
64 | config.flash_loan_addr.to_string(),
65 | &FlashLoanQueryMsg::Config {},
66 | )?;
67 |
68 | // Ensure we have calling permissions
69 | if flash_loan_config.proxy_addr.is_none()
70 | || flash_loan_config.proxy_addr.unwrap() != env.contract.address
71 | {
72 | return Err(ContractError::ContractNoSetProxyAddr {});
73 | }
74 |
75 | let mut msgs: Vec = vec![];
76 |
77 | // 1. Request flash loan
78 | msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
79 | contract_addr: config.flash_loan_addr.to_string(),
80 | msg: to_binary(&FlashLoanExecuteMsg::FlashLoan {})?,
81 | funds: vec![],
82 | }));
83 |
84 | // 2. Callback
85 | let flash_loan_balance = deps
86 | .querier
87 | .query_balance(config.flash_loan_addr.to_string(), DENOM)
88 | .unwrap();
89 |
90 | msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
91 | contract_addr: recipient.to_string(),
92 | msg,
93 | funds: vec![flash_loan_balance],
94 | }));
95 |
96 | // 3. Settle loan
97 | msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
98 | contract_addr: config.flash_loan_addr.to_string(),
99 | msg: to_binary(&FlashLoanExecuteMsg::SettleLoan {})?,
100 | funds: vec![],
101 | }));
102 |
103 | Ok(Response::new()
104 | .add_attribute("action", "request_flash_loan")
105 | .add_messages(msgs))
106 | }
107 |
108 | #[cfg_attr(not(feature = "library"), entry_point)]
109 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
110 | match msg {
111 | QueryMsg::GetFlashLoanAddress {} => to_binary(&get_flash_loan_addr(deps)?),
112 | }
113 | }
114 |
115 | /// Returns flash loan contract address
116 | pub fn get_flash_loan_addr(deps: Deps) -> StdResult {
117 | let config = CONFIG.load(deps.storage)?;
118 | Ok(config.flash_loan_addr)
119 | }
120 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/src/error.rs:
--------------------------------------------------------------------------------
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("Calling to flash loan contract is disallowed")]
13 | CallToFlashLoan {},
14 |
15 | #[error("Flash loan contract did not set proxy address properly")]
16 | ContractNoSetProxyAddr {},
17 | }
18 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::contract::DENOM;
4 | use common::flash_loan::{
5 | ExecuteMsg as FlashLoanExecuteMsg, InstantiateMsg as FlashLoanInstantiateMsg,
6 | };
7 | use common::mock_arb::{
8 | ExecuteMsg as MockArbExecuteMsg, InstantiateMsg as MockArbInstantiateMsg,
9 | };
10 | use common::proxy::{ExecuteMsg, InstantiateMsg};
11 | use cosmwasm_std::{coin, to_binary, Addr, Empty, Uint128};
12 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
13 |
14 | pub fn proxy_contract() -> Box> {
15 | let contract = ContractWrapper::new(
16 | crate::contract::execute,
17 | crate::contract::instantiate,
18 | crate::contract::query,
19 | );
20 | Box::new(contract)
21 | }
22 |
23 | pub fn flash_loan_contract() -> Box> {
24 | let contract = ContractWrapper::new(
25 | flash_loan::contract::execute,
26 | flash_loan::contract::instantiate,
27 | flash_loan::contract::query,
28 | );
29 | Box::new(contract)
30 | }
31 |
32 | pub fn mock_arb_contract() -> Box> {
33 | let contract = ContractWrapper::new(
34 | mock_arb::contract::execute,
35 | mock_arb::contract::instantiate,
36 | mock_arb::contract::query,
37 | );
38 | Box::new(contract)
39 | }
40 |
41 | pub const USER: &str = "user";
42 | pub const ADMIN: &str = "admin";
43 |
44 | pub fn proper_instantiate() -> (App, Addr, Addr, Addr) {
45 | let mut app = App::default();
46 |
47 | let cw_template_id = app.store_code(proxy_contract());
48 | let flash_loan_id = app.store_code(flash_loan_contract());
49 | let mock_arb_id = app.store_code(mock_arb_contract());
50 |
51 | // init flash loan contract
52 | let msg = FlashLoanInstantiateMsg {};
53 | let flash_loan_contract = app
54 | .instantiate_contract(
55 | flash_loan_id,
56 | Addr::unchecked(ADMIN),
57 | &msg,
58 | &[],
59 | "flash_loan",
60 | None,
61 | )
62 | .unwrap();
63 |
64 | // init proxy contract
65 | let msg = InstantiateMsg {
66 | flash_loan_addr: flash_loan_contract.to_string(),
67 | };
68 | let proxy_contract = app
69 | .instantiate_contract(
70 | cw_template_id,
71 | Addr::unchecked(ADMIN),
72 | &msg,
73 | &[],
74 | "proxy",
75 | None,
76 | )
77 | .unwrap();
78 |
79 | // init mock arb contract
80 | let msg = MockArbInstantiateMsg {};
81 | let mock_arb_contract = app
82 | .instantiate_contract(
83 | mock_arb_id,
84 | Addr::unchecked(ADMIN),
85 | &msg,
86 | &[],
87 | "mock_arb",
88 | None,
89 | )
90 | .unwrap();
91 |
92 | // mint funds to flash loan contract
93 | app = mint_tokens(app, flash_loan_contract.to_string(), Uint128::new(10_000));
94 |
95 | // set proxy contract
96 | app.execute_contract(
97 | Addr::unchecked(ADMIN),
98 | flash_loan_contract.clone(),
99 | &FlashLoanExecuteMsg::SetProxyAddr {
100 | proxy_addr: proxy_contract.to_string(),
101 | },
102 | &[],
103 | )
104 | .unwrap();
105 |
106 | (app, proxy_contract, flash_loan_contract, mock_arb_contract)
107 | }
108 |
109 | pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App {
110 | app.sudo(cw_multi_test::SudoMsg::Bank(
111 | cw_multi_test::BankSudo::Mint {
112 | to_address: recipient,
113 | amount: vec![coin(amount.u128(), DENOM)],
114 | },
115 | ))
116 | .unwrap();
117 | app
118 | }
119 |
120 | #[test]
121 | fn basic_flow() {
122 | let (mut app, proxy_contract, flash_loan_contract, mock_arb_contract) =
123 | proper_instantiate();
124 |
125 | // prepare arb msg
126 | let arb_msg = to_binary(&MockArbExecuteMsg::Arbitrage {
127 | recipient: flash_loan_contract.clone(),
128 | })
129 | .unwrap();
130 |
131 | // cannot call flash loan address from proxy
132 | app.execute_contract(
133 | Addr::unchecked(ADMIN),
134 | proxy_contract.clone(),
135 | &ExecuteMsg::RequestFlashLoan {
136 | recipient: flash_loan_contract.clone(),
137 | msg: arb_msg.clone(),
138 | },
139 | &[],
140 | )
141 | .unwrap_err();
142 |
143 | // try perform flash loan
144 | app.execute_contract(
145 | Addr::unchecked(ADMIN),
146 | proxy_contract,
147 | &ExecuteMsg::RequestFlashLoan {
148 | recipient: mock_arb_contract,
149 | msg: arb_msg,
150 | },
151 | &[],
152 | )
153 | .unwrap();
154 |
155 | // funds are sent back to flash loan contract
156 | let balance = app
157 | .wrap()
158 | .query_balance(flash_loan_contract.to_string(), DENOM)
159 | .unwrap();
160 | assert_eq!(balance.amount, Uint128::new(10_000));
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod state;
5 |
6 | pub use crate::error::ContractError;
7 |
--------------------------------------------------------------------------------
/ctf-03/contracts/proxy/src/state.rs:
--------------------------------------------------------------------------------
1 | use common::proxy::Config;
2 | use cw_storage_plus::Item;
3 |
4 | pub const CONFIG: Item = Item::new("config");
5 |
--------------------------------------------------------------------------------
/ctf-03/packages/common/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "common"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | cosmwasm-schema = { workspace = true }
10 | cosmwasm-std = { workspace = true }
11 | cw-storage-plus = { workspace = true }
12 | cw2 = { workspace = true }
13 | schemars = { workspace = true }
14 | serde = { workspace = true }
15 | thiserror = { workspace = true }
--------------------------------------------------------------------------------
/ctf-03/packages/common/src/flash_loan.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 | use cosmwasm_std::{Addr, Uint128};
3 |
4 | #[cw_serde]
5 | pub struct InstantiateMsg {}
6 |
7 | #[cw_serde]
8 | pub enum ExecuteMsg {
9 | SetProxyAddr { proxy_addr: String },
10 | FlashLoan {},
11 | SettleLoan {},
12 | WithdrawFunds { recipient: Addr },
13 | TransferOwner { new_owner: Addr },
14 | }
15 |
16 | #[cw_serde]
17 | #[derive(QueryResponses)]
18 | pub enum QueryMsg {
19 | #[returns(Config)]
20 | Config {},
21 |
22 | #[returns(FlashLoanState)]
23 | FlashLoanState {},
24 | }
25 |
26 | #[cw_serde]
27 | pub struct GetCountResponse {
28 | pub count: i32,
29 | }
30 |
31 | #[cw_serde]
32 | pub struct Config {
33 | pub owner: Addr,
34 | pub proxy_addr: Option,
35 | }
36 |
37 | #[cw_serde]
38 | pub struct FlashLoanState {
39 | pub requested_amount: Option,
40 | }
41 |
--------------------------------------------------------------------------------
/ctf-03/packages/common/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod flash_loan;
2 | pub mod mock_arb;
3 | pub mod proxy;
4 |
--------------------------------------------------------------------------------
/ctf-03/packages/common/src/mock_arb.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 | use cosmwasm_std::Addr;
3 |
4 | #[cw_serde]
5 | pub struct InstantiateMsg {}
6 |
7 | #[cw_serde]
8 | pub enum ExecuteMsg {
9 | Arbitrage { recipient: Addr },
10 | }
11 |
12 | #[cw_serde]
13 | #[derive(QueryResponses)]
14 | pub enum QueryMsg {}
15 |
16 | #[cw_serde]
17 | pub struct Config {}
18 |
--------------------------------------------------------------------------------
/ctf-03/packages/common/src/proxy.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 | use cosmwasm_std::{Addr, Binary};
3 |
4 | #[cw_serde]
5 | pub struct InstantiateMsg {
6 | pub flash_loan_addr: String,
7 | }
8 |
9 | #[cw_serde]
10 | pub enum ExecuteMsg {
11 | RequestFlashLoan { recipient: Addr, msg: Binary },
12 | }
13 |
14 | #[cw_serde]
15 | #[derive(QueryResponses)]
16 | pub enum QueryMsg {
17 | #[returns(Addr)]
18 | GetFlashLoanAddress {},
19 | }
20 |
21 | #[cw_serde]
22 | pub struct Config {
23 | /// Flash loan address
24 | pub flash_loan_addr: Addr,
25 | }
26 |
--------------------------------------------------------------------------------
/ctf-04/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-04/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-04/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-04"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 | cw-utils = "1.0.1"
48 | cw2 = "1.0.1"
49 | schemars = "0.8.10"
50 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
51 | thiserror = { version = "1.0.31" }
52 |
53 | [dev-dependencies]
54 | cw-multi-test = "0.16.2"
55 |
--------------------------------------------------------------------------------
/ctf-04/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-04/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 04: *Gram*
4 |
5 | Simplified vault for minting shares proportional to the current balance of the contract, it allows to redeem back for funds afterwards.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | /// Mint shares
11 | Mint {},
12 | /// Burn shares
13 | Burn { shares: Uint128 },
14 | }
15 | ```
16 |
17 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
18 |
19 | **:house: Base scenario:**
20 | - The contract is newly instantiated with zero funds.
21 |
22 | **:star: Goal for the challenge:**
23 | - Demonstrate how an unprivileged user can withdraw more funds than deposited.
24 |
25 | ## Scoring
26 |
27 | This challenge has been assigned a total of **150** points:
28 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
29 | - **35** points will be awarded for a proper recommendation that fixes the issue.
30 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
31 |
32 |
33 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
34 |
35 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
36 |
37 | ## Any questions?
38 |
39 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
40 |
41 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
42 |
--------------------------------------------------------------------------------
/ctf-04/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-04/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{
4 | coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
5 | Uint128,
6 | };
7 | use cw_utils::must_pay;
8 |
9 | use crate::error::ContractError;
10 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
11 | use crate::state::{Balance, Config, BALANCES, CONFIG};
12 |
13 | pub const DENOM: &str = "uawesome";
14 |
15 | #[cfg_attr(not(feature = "library"), entry_point)]
16 | pub fn instantiate(
17 | deps: DepsMut,
18 | _env: Env,
19 | _info: MessageInfo,
20 | _msg: InstantiateMsg,
21 | ) -> Result {
22 | let config = Config {
23 | total_supply: Uint128::zero(),
24 | };
25 |
26 | CONFIG.save(deps.storage, &config)?;
27 | Ok(Response::new().add_attribute("action", "instantiate"))
28 | }
29 |
30 | #[cfg_attr(not(feature = "library"), entry_point)]
31 | pub fn execute(
32 | deps: DepsMut,
33 | env: Env,
34 | info: MessageInfo,
35 | msg: ExecuteMsg,
36 | ) -> Result {
37 | match msg {
38 | ExecuteMsg::Mint {} => mint(deps, env, info),
39 | ExecuteMsg::Burn { shares } => burn(deps, env, info, shares),
40 | }
41 | }
42 |
43 | /// Entry point for users to mint shares
44 | pub fn mint(deps: DepsMut, env: Env, info: MessageInfo) -> Result {
45 | let amount = must_pay(&info, DENOM).unwrap();
46 |
47 | let mut config = CONFIG.load(deps.storage).unwrap();
48 |
49 | let contract_balance = deps
50 | .querier
51 | .query_balance(env.contract.address.to_string(), DENOM)
52 | .unwrap();
53 |
54 | let total_assets = contract_balance.amount - amount;
55 | let total_supply = config.total_supply;
56 |
57 | // share = asset * total supply / total assets
58 | let mint_amount = if total_supply.is_zero() {
59 | amount
60 | } else {
61 | amount.multiply_ratio(total_supply, total_assets)
62 | };
63 |
64 | if mint_amount.is_zero() {
65 | return Err(ContractError::ZeroAmountNotAllowed {});
66 | }
67 |
68 | // increase total supply
69 | config.total_supply += mint_amount;
70 | CONFIG.save(deps.storage, &config)?;
71 |
72 | // increase user balance
73 | let mut user = BALANCES
74 | .load(deps.storage, &info.sender)
75 | .unwrap_or_default();
76 | user.amount += mint_amount;
77 | BALANCES.save(deps.storage, &info.sender, &user)?;
78 |
79 | Ok(Response::new()
80 | .add_attribute("action", "mint")
81 | .add_attribute("user", info.sender.to_string())
82 | .add_attribute("asset", amount.to_string())
83 | .add_attribute("shares", mint_amount.to_string()))
84 | }
85 |
86 | /// Entry point for users to burn shares
87 | pub fn burn(
88 | deps: DepsMut,
89 | env: Env,
90 | info: MessageInfo,
91 | shares: Uint128,
92 | ) -> Result {
93 | let mut config = CONFIG.load(deps.storage).unwrap();
94 |
95 | let contract_balance = deps
96 | .querier
97 | .query_balance(env.contract.address.to_string(), DENOM)
98 | .unwrap();
99 |
100 | let total_assets = contract_balance.amount;
101 | let total_supply = config.total_supply;
102 |
103 | // asset = share * total assets / total supply
104 | let asset_to_return = shares.multiply_ratio(total_assets, total_supply);
105 |
106 | if asset_to_return.is_zero() {
107 | return Err(ContractError::ZeroAmountNotAllowed {});
108 | }
109 |
110 | // decrease total supply
111 | config.total_supply -= shares;
112 | CONFIG.save(deps.storage, &config)?;
113 |
114 | // decrease user balance
115 | let mut user = BALANCES.load(deps.storage, &info.sender)?;
116 | user.amount -= shares;
117 | BALANCES.save(deps.storage, &info.sender, &user)?;
118 |
119 | let msg = BankMsg::Send {
120 | to_address: info.sender.to_string(),
121 | amount: coins(asset_to_return.u128(), DENOM),
122 | };
123 |
124 | Ok(Response::new()
125 | .add_attribute("action", "burn")
126 | .add_attribute("user", info.sender.to_string())
127 | .add_attribute("asset", asset_to_return.to_string())
128 | .add_attribute("shares", shares.to_string())
129 | .add_message(msg))
130 | }
131 |
132 | #[cfg_attr(not(feature = "library"), entry_point)]
133 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
134 | match msg {
135 | QueryMsg::GetConfig {} => to_binary(&query_config(deps)?),
136 | QueryMsg::UserBalance { address } => to_binary(&query_user(deps, address)?),
137 | }
138 | }
139 |
140 | pub fn query_config(deps: Deps) -> StdResult {
141 | let config = CONFIG.load(deps.storage).unwrap();
142 | Ok(config)
143 | }
144 |
145 | pub fn query_user(deps: Deps, address: String) -> StdResult {
146 | let user = deps.api.addr_validate(&address).unwrap();
147 | let balance = BALANCES.load(deps.storage, &user).unwrap_or_default();
148 | Ok(balance)
149 | }
150 |
--------------------------------------------------------------------------------
/ctf-04/src/error.rs:
--------------------------------------------------------------------------------
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("Zero amount is not allowed")]
13 | ZeroAmountNotAllowed {},
14 | }
15 |
--------------------------------------------------------------------------------
/ctf-04/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::{
4 | contract::DENOM,
5 | msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
6 | state::Balance,
7 | };
8 | use cosmwasm_std::{coin, Addr, Empty, Uint128};
9 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
10 |
11 | pub fn challenge_contract() -> Box> {
12 | let contract = ContractWrapper::new(
13 | crate::contract::execute,
14 | crate::contract::instantiate,
15 | crate::contract::query,
16 | );
17 | Box::new(contract)
18 | }
19 |
20 | pub const USER: &str = "user";
21 | pub const USER2: &str = "user2";
22 | pub const ADMIN: &str = "admin";
23 |
24 | pub fn proper_instantiate() -> (App, Addr) {
25 | let mut app = App::default();
26 | let cw_template_id = app.store_code(challenge_contract());
27 |
28 | // init contract
29 | let msg = InstantiateMsg { offset: 10 };
30 | let contract_addr = app
31 | .instantiate_contract(
32 | cw_template_id,
33 | Addr::unchecked(ADMIN),
34 | &msg,
35 | &[],
36 | "test",
37 | None,
38 | )
39 | .unwrap();
40 |
41 | (app, contract_addr)
42 | }
43 |
44 | pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App {
45 | app.sudo(cw_multi_test::SudoMsg::Bank(
46 | cw_multi_test::BankSudo::Mint {
47 | to_address: recipient,
48 | amount: vec![coin(amount.u128(), DENOM)],
49 | },
50 | ))
51 | .unwrap();
52 | app
53 | }
54 |
55 | #[test]
56 | fn basic_flow() {
57 | let (mut app, contract_addr) = proper_instantiate();
58 |
59 | // mint funds to user
60 | app = mint_tokens(app, USER.to_owned(), Uint128::new(10_000));
61 |
62 | // mint shares for user
63 | app.execute_contract(
64 | Addr::unchecked(USER),
65 | contract_addr.clone(),
66 | &ExecuteMsg::Mint {},
67 | &[coin(10_000, DENOM)],
68 | )
69 | .unwrap();
70 |
71 | // mint funds to user2
72 | app = mint_tokens(app, USER2.to_owned(), Uint128::new(10_000));
73 |
74 | // mint shares for user2
75 | app.execute_contract(
76 | Addr::unchecked(USER2),
77 | contract_addr.clone(),
78 | &ExecuteMsg::Mint {},
79 | &[coin(10_000, DENOM)],
80 | )
81 | .unwrap();
82 |
83 | // query user
84 | let balance: Balance = app
85 | .wrap()
86 | .query_wasm_smart(
87 | contract_addr.clone(),
88 | &QueryMsg::UserBalance {
89 | address: USER.to_string(),
90 | },
91 | )
92 | .unwrap();
93 |
94 | // burn shares for user
95 | app.execute_contract(
96 | Addr::unchecked(USER),
97 | contract_addr.clone(),
98 | &ExecuteMsg::Burn {
99 | shares: balance.amount,
100 | },
101 | &[],
102 | )
103 | .unwrap();
104 |
105 | // burn shares for user2
106 | app.execute_contract(
107 | Addr::unchecked(USER2),
108 | contract_addr.clone(),
109 | &ExecuteMsg::Burn {
110 | shares: balance.amount,
111 | },
112 | &[],
113 | )
114 | .unwrap();
115 |
116 | let bal = app.wrap().query_balance(USER, DENOM).unwrap();
117 | assert_eq!(bal.amount, Uint128::new(10_000));
118 |
119 | let bal = app.wrap().query_balance(USER2, DENOM).unwrap();
120 | assert_eq!(bal.amount, Uint128::new(10_000));
121 |
122 | let bal = app
123 | .wrap()
124 | .query_balance(contract_addr.to_string(), DENOM)
125 | .unwrap();
126 | assert_eq!(bal.amount, Uint128::zero());
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/ctf-04/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-04/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 | use cosmwasm_std::Uint128;
3 |
4 | use crate::state::{Balance, Config};
5 |
6 | #[cw_serde]
7 | pub struct InstantiateMsg {
8 | pub offset: u64,
9 | }
10 |
11 | #[cw_serde]
12 | pub enum ExecuteMsg {
13 | /// Mint shares
14 | Mint {},
15 | /// Burn shares
16 | Burn { shares: Uint128 },
17 | }
18 |
19 | #[cw_serde]
20 | #[derive(QueryResponses)]
21 | pub enum QueryMsg {
22 | #[returns(Config)]
23 | GetConfig {},
24 |
25 | #[returns(Balance)]
26 | UserBalance { address: String },
27 | }
28 |
--------------------------------------------------------------------------------
/ctf-04/src/state.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 | use cosmwasm_std::{Addr, Uint128};
3 | use cw_storage_plus::{Item, Map};
4 |
5 | #[cw_serde]
6 | pub struct Config {
7 | pub total_supply: Uint128,
8 | }
9 |
10 | #[cw_serde]
11 | #[derive(Default)]
12 | pub struct Balance {
13 | pub amount: Uint128,
14 | }
15 |
16 | pub const CONFIG: Item = Item::new("config");
17 | pub const BALANCES: Map<&Addr, Balance> = Map::new("balances");
18 |
--------------------------------------------------------------------------------
/ctf-05/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-05/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-05/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-05"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 | cw-utils = "1.0.1"
49 | schemars = "0.8.10"
50 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
51 | thiserror = { version = "1.0.31" }
52 |
53 | [dev-dependencies]
54 | cw-multi-test = "0.16.2"
55 |
--------------------------------------------------------------------------------
/ctf-05/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-05/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 05: *Draupnir*
4 |
5 | Simplified vault where users can deposit and withdraw their tokens which will be internally accounted. The vault's `owner` can perform arbitrary actions through the `OwnerAction` entry point. In addition, a two step address transfer is implemented for the `owner` role.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | Deposit {},
11 | Withdraw { amount: Uint128 },
12 | OwnerAction { msg: CosmosMsg },
13 | ProposeNewOwner { new_owner: String },
14 | AcceptOwnership {},
15 | DropOwnershipProposal {},
16 | }
17 | ```
18 |
19 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
20 |
21 | **:house: Base scenario:**
22 | - The contract has been instantiated with zero funds.
23 | - `USER1` and `USER2` deposit `10_000` tokens each.
24 | - The owner role is assigned to the `ADMIN` address.
25 |
26 | **:star: Goal for the challenge:**
27 | - Demonstrate how an unprivileged user can drain all the funds inside the contract.
28 |
29 | ## Scoring
30 |
31 | This challenge has been assigned a total of **150** points:
32 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
33 | - **35** points will be awarded for a proper recommendation that fixes the issue.
34 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
35 |
36 |
37 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
38 |
39 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
40 |
41 | ## Any questions?
42 |
43 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
44 |
45 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
46 |
--------------------------------------------------------------------------------
/ctf-05/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-05/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::{
3 | coin, entry_point, to_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo,
4 | Response, StdResult, Uint128,
5 | };
6 |
7 | use crate::error::ContractError;
8 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
9 | use crate::state::{assert_owner, State, BALANCES, STATE};
10 | use cw_utils::must_pay;
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 {
21 | let state = State {
22 | current_owner: deps.api.addr_validate(&msg.owner)?,
23 | proposed_owner: None,
24 | };
25 | STATE.save(deps.storage, &state)?;
26 |
27 | Ok(Response::new()
28 | .add_attribute("action", "instantiate")
29 | .add_attribute("owner", msg.owner))
30 | }
31 |
32 | #[cfg_attr(not(feature = "library"), entry_point)]
33 | pub fn execute(
34 | deps: DepsMut,
35 | _env: Env,
36 | info: MessageInfo,
37 | msg: ExecuteMsg,
38 | ) -> Result {
39 | match msg {
40 | ExecuteMsg::Deposit {} => deposit(deps, info),
41 | ExecuteMsg::Withdraw { amount } => withdraw(deps, info, amount),
42 | ExecuteMsg::OwnerAction { msg } => owner_action(deps, info, msg),
43 | ExecuteMsg::ProposeNewOwner { new_owner } => propose_owner(deps, info, new_owner),
44 | ExecuteMsg::AcceptOwnership {} => accept_owner(deps, info),
45 | ExecuteMsg::DropOwnershipProposal {} => drop_owner(deps, info),
46 | }
47 | }
48 |
49 | /// Deposit entry point for user
50 | pub fn deposit(deps: DepsMut, info: MessageInfo) -> Result {
51 | // validate denom
52 | let amount = must_pay(&info, DENOM).unwrap();
53 |
54 | // increase total stake
55 | let mut user_balance = BALANCES
56 | .load(deps.storage, &info.sender)
57 | .unwrap_or_default();
58 | user_balance += amount;
59 |
60 | BALANCES.save(deps.storage, &info.sender, &user_balance)?;
61 |
62 | Ok(Response::new()
63 | .add_attribute("action", "deposit")
64 | .add_attribute("user", info.sender)
65 | .add_attribute("amount", amount))
66 | }
67 |
68 | /// Withdrawal entry point for user
69 | pub fn withdraw(
70 | deps: DepsMut,
71 | info: MessageInfo,
72 | amount: Uint128,
73 | ) -> Result {
74 | // decrease total stake
75 | let mut user_balance = BALANCES.load(deps.storage, &info.sender)?;
76 |
77 | // Cosmwasm's Uint128 checks math operations
78 | user_balance -= amount;
79 |
80 | BALANCES.save(deps.storage, &info.sender, &user_balance)?;
81 |
82 | let msg = BankMsg::Send {
83 | to_address: info.sender.to_string(),
84 | amount: vec![coin(amount.u128(), DENOM)],
85 | };
86 |
87 | Ok(Response::new()
88 | .add_attribute("action", "withdraw")
89 | .add_attribute("user", info.sender)
90 | .add_attribute("amount", amount)
91 | .add_message(msg))
92 | }
93 |
94 | /// Entry point for owner to execute arbitrary Cosmos messages
95 | pub fn owner_action(
96 | deps: DepsMut,
97 | info: MessageInfo,
98 | msg: CosmosMsg,
99 | ) -> Result {
100 | assert_owner(deps.storage, info.sender)?;
101 |
102 | Ok(Response::new()
103 | .add_attribute("action", "owner_action")
104 | .add_message(msg))
105 | }
106 |
107 | /// Entry point for current owner to propose a new owner
108 | pub fn propose_owner(
109 | deps: DepsMut,
110 | info: MessageInfo,
111 | new_owner: String,
112 | ) -> Result {
113 | assert_owner(deps.storage, info.sender)?;
114 |
115 | STATE.update(deps.storage, |mut state| -> StdResult<_> {
116 | state.proposed_owner = Some(deps.api.addr_validate(&new_owner)?);
117 | Ok(state)
118 | })?;
119 |
120 | Ok(Response::new()
121 | .add_attribute("action", "propose_owner")
122 | .add_attribute("new proposal", new_owner))
123 | }
124 |
125 | /// Entry point for new owner to accept a pending ownership transfer
126 | pub fn accept_owner(deps: DepsMut, info: MessageInfo) -> Result {
127 | let state = STATE.load(deps.storage)?;
128 |
129 | if state.proposed_owner != Some(info.sender.clone()) {
130 | ContractError::Unauthorized {};
131 | }
132 |
133 | STATE.update(deps.storage, |mut state| -> StdResult<_> {
134 | state.current_owner = info.sender.clone();
135 | state.proposed_owner = None;
136 | Ok(state)
137 | })?;
138 |
139 | Ok(Response::new()
140 | .add_attribute("action", "accept_owner")
141 | .add_attribute("new owner", info.sender))
142 | }
143 |
144 | /// Entry point for current owner to drop pending ownership
145 | pub fn drop_owner(deps: DepsMut, info: MessageInfo) -> Result {
146 | assert_owner(deps.storage, info.sender)?;
147 |
148 | STATE.update(deps.storage, |mut state| -> StdResult<_> {
149 | state.proposed_owner = None;
150 | Ok(state)
151 | })?;
152 |
153 | Ok(Response::new().add_attribute("action", "drop_owner"))
154 | }
155 |
156 | #[cfg_attr(not(feature = "library"), entry_point)]
157 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
158 | match msg {
159 | QueryMsg::State {} => to_binary(&query_state(deps)?),
160 | QueryMsg::UserBalance { address } => to_binary(&query_balance(deps, address)?),
161 | }
162 | }
163 |
164 | /// Returns user balance
165 | pub fn query_balance(deps: Deps, address: String) -> StdResult {
166 | let address = deps.api.addr_validate(&address)?;
167 | BALANCES.load(deps.storage, &address)
168 | }
169 |
170 | /// Returns contract state
171 | pub fn query_state(deps: Deps) -> StdResult {
172 | STATE.load(deps.storage)
173 | }
174 |
--------------------------------------------------------------------------------
/ctf-05/src/error.rs:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/ctf-05/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::contract::DENOM;
4 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
5 | use crate::state::State;
6 | use cosmwasm_std::{coin, Addr, Empty, Uint128};
7 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
8 |
9 | pub fn challenge_contract() -> Box> {
10 | let contract = ContractWrapper::new(
11 | crate::contract::execute,
12 | crate::contract::instantiate,
13 | crate::contract::query,
14 | );
15 | Box::new(contract)
16 | }
17 |
18 | pub const USER1: &str = "user1";
19 | pub const USER2: &str = "user2";
20 | pub const ADMIN: &str = "admin";
21 |
22 | pub fn base_scenario() -> (App, Addr) {
23 | let mut app = App::default();
24 | let cw_template_id = app.store_code(challenge_contract());
25 |
26 | // init contract
27 | let msg = InstantiateMsg {
28 | owner: ADMIN.to_string(),
29 | };
30 | let contract_addr = app
31 | .instantiate_contract(
32 | cw_template_id,
33 | Addr::unchecked(ADMIN),
34 | &msg,
35 | &[],
36 | "test",
37 | None,
38 | )
39 | .unwrap();
40 |
41 | // User 1 deposit
42 | app = mint_tokens(app, USER1.to_owned(), Uint128::new(10_000));
43 | app.execute_contract(
44 | Addr::unchecked(USER1),
45 | contract_addr.clone(),
46 | &ExecuteMsg::Deposit {},
47 | &[coin(10_000, DENOM)],
48 | )
49 | .unwrap();
50 |
51 | // User 2 deposit
52 | app = mint_tokens(app, USER2.to_owned(), Uint128::new(10_000));
53 | app.execute_contract(
54 | Addr::unchecked(USER2),
55 | contract_addr.clone(),
56 | &ExecuteMsg::Deposit {},
57 | &[coin(10_000, DENOM)],
58 | )
59 | .unwrap();
60 |
61 | (app, contract_addr)
62 | }
63 |
64 | pub fn proper_instantiate() -> (App, Addr) {
65 | let mut app = App::default();
66 | let cw_template_id = app.store_code(challenge_contract());
67 |
68 | // init contract
69 | let msg = InstantiateMsg {
70 | owner: ADMIN.to_string(),
71 | };
72 | let contract_addr = app
73 | .instantiate_contract(
74 | cw_template_id,
75 | Addr::unchecked(ADMIN),
76 | &msg,
77 | &[],
78 | "test",
79 | None,
80 | )
81 | .unwrap();
82 |
83 | // mint funds to user
84 | app = mint_tokens(app, USER1.to_owned(), Uint128::new(10_000));
85 | app = mint_tokens(app, USER2.to_owned(), Uint128::new(8_000));
86 |
87 | (app, contract_addr)
88 | }
89 |
90 | pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App {
91 | app.sudo(cw_multi_test::SudoMsg::Bank(
92 | cw_multi_test::BankSudo::Mint {
93 | to_address: recipient,
94 | amount: vec![coin(amount.u128(), DENOM)],
95 | },
96 | ))
97 | .unwrap();
98 | app
99 | }
100 |
101 | #[test]
102 | fn basic_flow() {
103 | let (mut app, contract_addr) = proper_instantiate();
104 |
105 | // User 1 deposit
106 | app.execute_contract(
107 | Addr::unchecked(USER1),
108 | contract_addr.clone(),
109 | &ExecuteMsg::Deposit {},
110 | &[coin(10_000, DENOM)],
111 | )
112 | .unwrap();
113 |
114 | // User 2 deposit
115 | app.execute_contract(
116 | Addr::unchecked(USER2),
117 | contract_addr.clone(),
118 | &ExecuteMsg::Deposit {},
119 | &[coin(8_000, DENOM)],
120 | )
121 | .unwrap();
122 |
123 | // Query balances
124 | let balance: Uint128 = app
125 | .wrap()
126 | .query_wasm_smart(
127 | contract_addr.clone(),
128 | &QueryMsg::UserBalance {
129 | address: USER1.to_string(),
130 | },
131 | )
132 | .unwrap();
133 | assert_eq!(balance, Uint128::new(10_000));
134 |
135 | let balance: Uint128 = app
136 | .wrap()
137 | .query_wasm_smart(
138 | contract_addr.clone(),
139 | &QueryMsg::UserBalance {
140 | address: USER2.to_string(),
141 | },
142 | )
143 | .unwrap();
144 | assert_eq!(balance, Uint128::new(8_000));
145 |
146 | let bal = app
147 | .wrap()
148 | .query_balance(contract_addr.to_string(), DENOM)
149 | .unwrap();
150 | assert_eq!(bal.amount, Uint128::new(18_000));
151 |
152 | // Withdraw user 1
153 | app.execute_contract(
154 | Addr::unchecked(USER1),
155 | contract_addr.clone(),
156 | &ExecuteMsg::Withdraw {
157 | amount: Uint128::new(5_000),
158 | },
159 | &[],
160 | )
161 | .unwrap();
162 |
163 | // Query balances
164 | let balance: Uint128 = app
165 | .wrap()
166 | .query_wasm_smart(
167 | contract_addr.clone(),
168 | &QueryMsg::UserBalance {
169 | address: USER1.to_string(),
170 | },
171 | )
172 | .unwrap();
173 | assert_eq!(balance, Uint128::new(5_000));
174 |
175 | let bal = app
176 | .wrap()
177 | .query_balance(contract_addr.to_string(), DENOM)
178 | .unwrap();
179 | assert_eq!(bal.amount, Uint128::new(13_000));
180 | }
181 |
182 | #[test]
183 | fn ownership_flow() {
184 | let (mut app, contract_addr) = proper_instantiate();
185 |
186 | // Initial state
187 | let state: State = app
188 | .wrap()
189 | .query_wasm_smart(contract_addr.clone(), &QueryMsg::State {})
190 | .unwrap();
191 |
192 | assert_eq!(
193 | state,
194 | State {
195 | current_owner: Addr::unchecked(ADMIN),
196 | proposed_owner: None,
197 | }
198 | );
199 |
200 | // Ownership transfer
201 | app.execute_contract(
202 | Addr::unchecked(ADMIN),
203 | contract_addr.clone(),
204 | &ExecuteMsg::ProposeNewOwner {
205 | new_owner: "new_owner".to_string(),
206 | },
207 | &[],
208 | )
209 | .unwrap();
210 |
211 | app.execute_contract(
212 | Addr::unchecked("new_owner"),
213 | contract_addr.clone(),
214 | &ExecuteMsg::AcceptOwnership {},
215 | &[],
216 | )
217 | .unwrap();
218 |
219 | // Final state
220 | let state: State = app
221 | .wrap()
222 | .query_wasm_smart(contract_addr, &QueryMsg::State {})
223 | .unwrap();
224 |
225 | assert_eq!(
226 | state,
227 | State {
228 | current_owner: Addr::unchecked("new_owner"),
229 | proposed_owner: None,
230 | }
231 | );
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/ctf-05/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-05/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 | use cosmwasm_std::{CosmosMsg, Uint128};
3 |
4 | use crate::state::State;
5 |
6 | #[cw_serde]
7 | pub struct InstantiateMsg {
8 | pub owner: String,
9 | }
10 |
11 | #[cw_serde]
12 | pub enum ExecuteMsg {
13 | Deposit {},
14 | Withdraw { amount: Uint128 },
15 | OwnerAction { msg: CosmosMsg },
16 | ProposeNewOwner { new_owner: String },
17 | AcceptOwnership {},
18 | DropOwnershipProposal {},
19 | }
20 |
21 | #[cw_serde]
22 | #[derive(QueryResponses)]
23 | pub enum QueryMsg {
24 | #[returns(State)]
25 | State {},
26 |
27 | #[returns(Uint128)]
28 | UserBalance { address: String },
29 | }
30 |
--------------------------------------------------------------------------------
/ctf-05/src/state.rs:
--------------------------------------------------------------------------------
1 | use schemars::JsonSchema;
2 | use serde::{Deserialize, Serialize};
3 |
4 | use crate::error::ContractError;
5 | use cosmwasm_std::{Addr, Storage, Uint128};
6 | use cw_storage_plus::{Item, Map};
7 |
8 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
9 | pub struct State {
10 | /// Current owner
11 | pub current_owner: Addr,
12 | /// Proposed owner
13 | pub proposed_owner: Option,
14 | }
15 |
16 | pub const STATE: Item = Item::new("state");
17 | pub const BALANCES: Map<&Addr, Uint128> = Map::new("user_balances");
18 |
19 | pub fn assert_owner(store: &dyn Storage, sender: Addr) -> Result<(), ContractError> {
20 | let state = STATE.load(store)?;
21 |
22 | if state.current_owner != sender {
23 | return Err(ContractError::Unauthorized {});
24 | }
25 | Ok(())
26 | }
27 |
--------------------------------------------------------------------------------
/ctf-06/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-06/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-06/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-06"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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"
56 |
--------------------------------------------------------------------------------
/ctf-06/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-06/README.md:
--------------------------------------------------------------------------------
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. If a proposal was voted for with more than a third of the current supply, the user gets the `owner` role.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | Propose {},
11 | ResolveProposal {},
12 | OwnerAction {
13 | action: CosmosMsg,
14 | },
15 | Receive(Cw20ReceiveMsg),
16 | }
17 | ```
18 |
19 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
20 |
21 | **:house: Base scenario:**
22 | - The contract is newly instantiated
23 |
24 | **:star: Goal for the challenge:**
25 | - Demonstrate how a proposer can obtain the owner role without controlling 1/3 of the total supply.
26 |
27 | ## Scoring
28 |
29 | This challenge has been assigned a total of **150** points:
30 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
31 | - **35** points will be awarded for a proper recommendation that fixes the issue.
32 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
33 |
34 |
35 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
36 |
37 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
38 |
39 | ## Any questions?
40 |
41 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
42 |
43 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
44 |
--------------------------------------------------------------------------------
/ctf-06/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-06/src/contract.rs:
--------------------------------------------------------------------------------
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
177 | CONFIG.load(deps.storage)
178 | }
179 |
180 | /// Returns proposal information
181 | pub fn query_proposal(deps: Deps) -> StdResult {
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 {
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 | }
197 |
--------------------------------------------------------------------------------
/ctf-06/src/error.rs:
--------------------------------------------------------------------------------
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 | }
24 |
--------------------------------------------------------------------------------
/ctf-06/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-06/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 | use cosmwasm_std::CosmosMsg;
3 | use cw20::Cw20ReceiveMsg;
4 |
5 | #[cw_serde]
6 | pub struct InstantiateMsg {
7 | pub token: String,
8 | pub owner: String,
9 | pub window: u64,
10 | }
11 |
12 | #[cw_serde]
13 | pub enum ExecuteMsg {
14 | Propose {},
15 | ResolveProposal {},
16 | OwnerAction { action: CosmosMsg },
17 | Receive(Cw20ReceiveMsg),
18 | }
19 |
20 | #[cw_serde]
21 | pub enum Cw20HookMsg {
22 | CastVote {},
23 | }
24 |
25 | #[cw_serde]
26 | pub enum QueryMsg {
27 | Config {},
28 | Proposal {},
29 | Balance {},
30 | }
31 |
--------------------------------------------------------------------------------
/ctf-06/src/state.rs:
--------------------------------------------------------------------------------
1 | use schemars::JsonSchema;
2 | use serde::{Deserialize, Serialize};
3 |
4 | use cosmwasm_std::{Addr, Timestamp};
5 | use cw_storage_plus::Item;
6 |
7 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
8 | pub struct Config {
9 | /// Voting window period
10 | pub voting_window: u64,
11 | /// Voting token contract address
12 | pub voting_token: Addr,
13 | /// Owner address
14 | pub owner: Addr,
15 | }
16 |
17 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
18 | pub struct Proposal {
19 | /// Proposer address
20 | pub proposer: Addr,
21 | /// Timestamp of proposal
22 | pub timestamp: Timestamp,
23 | }
24 |
25 | pub const CONFIG: Item = Item::new("config");
26 | pub const PROPOSAL: Item = Item::new("proposal");
27 |
--------------------------------------------------------------------------------
/ctf-07/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-07/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-07/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-07"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 | cw-utils = "1.0.1"
49 | schemars = "0.8.10"
50 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
51 | thiserror = { version = "1.0.31" }
52 |
53 | [dev-dependencies]
54 | cw-multi-test = "0.16.2"
55 |
--------------------------------------------------------------------------------
/ctf-07/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-07/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 07: *Tyrfing*
4 |
5 | Simplified vault that accounts for the top depositor! The `owner` can set the threshold to become top depositor.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | Deposit {},
11 | Withdraw { amount: Uint128 },
12 | OwnerAction { msg: CosmosMsg },
13 | UpdateConfig { new_threshold: Uint128 },
14 | }
15 | ```
16 |
17 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
18 |
19 | **:house: Base scenario:**
20 | - The contract is newly instantiated.
21 | - `USER1` and `USER2` deposit 10_000 tokens each
22 | - The owner role is assigned to the `ADMIN` address
23 |
24 | **:star: Goal for the challenge:**
25 | - Demonstrate how an unprivileged user can drain all the contract's funds.
26 |
27 | ## Scoring
28 |
29 | This challenge has been assigned a total of **150** points:
30 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
31 | - **35** points will be awarded for a proper recommendation that fixes the issue.
32 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
33 |
34 |
35 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
36 |
37 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
38 |
39 | ## Any questions?
40 |
41 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
42 |
43 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
44 |
--------------------------------------------------------------------------------
/ctf-07/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-07/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::{
3 | coin, entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env,
4 | MessageInfo, Response, StdResult, Uint128,
5 | };
6 | use cw_storage_plus::Item;
7 |
8 | use crate::error::ContractError;
9 | use crate::msg::{ConfigQueryResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
10 | use crate::state::{BALANCES, OWNER, THRESHOLD};
11 | use cw_utils::must_pay;
12 |
13 | pub const DENOM: &str = "uawesome";
14 | pub const TOP_DEPOSITOR: Item = Item::new("address");
15 |
16 | #[cfg_attr(not(feature = "library"), entry_point)]
17 | pub fn instantiate(
18 | deps: DepsMut,
19 | _env: Env,
20 | _info: MessageInfo,
21 | msg: InstantiateMsg,
22 | ) -> Result {
23 | OWNER.save(deps.storage, &deps.api.addr_validate(&msg.owner)?)?;
24 |
25 | THRESHOLD.save(deps.storage, &msg.threshold)?;
26 |
27 | Ok(Response::new()
28 | .add_attribute("action", "instantiate")
29 | .add_attribute("owner", msg.owner))
30 | }
31 |
32 | #[cfg_attr(not(feature = "library"), entry_point)]
33 | pub fn execute(
34 | deps: DepsMut,
35 | _env: Env,
36 | info: MessageInfo,
37 | msg: ExecuteMsg,
38 | ) -> Result {
39 | match msg {
40 | ExecuteMsg::Deposit {} => deposit(deps, info),
41 | ExecuteMsg::Withdraw { amount } => withdraw(deps, info, amount),
42 | ExecuteMsg::OwnerAction { msg } => owner_action(deps, info, msg),
43 | ExecuteMsg::UpdateConfig { new_threshold } => update_config(deps, info, new_threshold),
44 | }
45 | }
46 |
47 | /// Deposit entry point for user
48 | pub fn deposit(deps: DepsMut, info: MessageInfo) -> Result {
49 | // validate denom
50 | let amount = must_pay(&info, DENOM).unwrap();
51 |
52 | // increase total stake
53 | let mut user_balance = BALANCES
54 | .load(deps.storage, &info.sender)
55 | .unwrap_or_default();
56 | user_balance += amount;
57 |
58 | BALANCES.save(deps.storage, &info.sender, &user_balance)?;
59 |
60 | let current_threshold = THRESHOLD.load(deps.storage)?;
61 |
62 | if user_balance > current_threshold {
63 | THRESHOLD.save(deps.storage, &user_balance)?;
64 | TOP_DEPOSITOR.save(deps.storage, &info.sender)?;
65 | }
66 |
67 | Ok(Response::new()
68 | .add_attribute("action", "deposit")
69 | .add_attribute("user", info.sender)
70 | .add_attribute("amount", amount))
71 | }
72 |
73 | /// Withdrawal entry point for user
74 | pub fn withdraw(
75 | deps: DepsMut,
76 | info: MessageInfo,
77 | amount: Uint128,
78 | ) -> Result {
79 | // decrease total stake
80 | let mut user_balance = BALANCES.load(deps.storage, &info.sender)?;
81 |
82 | // Cosmwasm's Uint128 checks math operations
83 | user_balance -= amount;
84 |
85 | BALANCES.save(deps.storage, &info.sender, &user_balance)?;
86 |
87 | let msg = BankMsg::Send {
88 | to_address: info.sender.to_string(),
89 | amount: vec![coin(amount.u128(), DENOM)],
90 | };
91 |
92 | Ok(Response::new()
93 | .add_attribute("action", "withdraw")
94 | .add_attribute("user", info.sender)
95 | .add_attribute("amount", amount)
96 | .add_message(msg))
97 | }
98 |
99 | /// Entry point for owner to update threshold
100 | pub fn update_config(
101 | deps: DepsMut,
102 | info: MessageInfo,
103 | new_threshold: Uint128,
104 | ) -> Result {
105 | let owner = OWNER.load(deps.storage)?;
106 |
107 | if owner != info.sender {
108 | return Err(ContractError::Unauthorized {});
109 | }
110 |
111 | THRESHOLD.save(deps.storage, &new_threshold)?;
112 |
113 | Ok(Response::new()
114 | .add_attribute("action", "Update config")
115 | .add_attribute("threshold", new_threshold))
116 | }
117 |
118 | /// Entry point for owner to execute arbitrary Cosmos messages
119 | pub fn owner_action(
120 | deps: DepsMut,
121 | info: MessageInfo,
122 | msg: CosmosMsg,
123 | ) -> Result {
124 | let owner = OWNER.load(deps.storage)?;
125 |
126 | if owner != info.sender {
127 | return Err(ContractError::Unauthorized {});
128 | }
129 |
130 | Ok(Response::new()
131 | .add_attribute("action", "owner_action")
132 | .add_message(msg))
133 | }
134 |
135 | #[cfg_attr(not(feature = "library"), entry_point)]
136 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
137 | match msg {
138 | QueryMsg::Config {} => to_binary(&query_config(deps)?),
139 | QueryMsg::UserBalance { address } => to_binary(&query_balance(deps, address)?),
140 | QueryMsg::Top {} => to_binary(&query_top_depositor(deps)?),
141 | }
142 | }
143 |
144 | /// Returns balance for specified address
145 | pub fn query_balance(deps: Deps, address: String) -> StdResult {
146 | let address = deps.api.addr_validate(&address)?;
147 | BALANCES.load(deps.storage, &address)
148 | }
149 |
150 | /// Returns contract configuration
151 | pub fn query_config(deps: Deps) -> StdResult {
152 | let owner = OWNER.load(deps.storage)?;
153 | let threshold = THRESHOLD.load(deps.storage)?;
154 |
155 | Ok(ConfigQueryResponse { owner, threshold })
156 | }
157 |
158 | /// Returns the top depositor
159 | pub fn query_top_depositor(deps: Deps) -> StdResult {
160 | TOP_DEPOSITOR.load(deps.storage)
161 | }
162 |
--------------------------------------------------------------------------------
/ctf-07/src/error.rs:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/ctf-07/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::{
4 | contract::DENOM,
5 | msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
6 | };
7 | use cosmwasm_std::{coin, Addr, Empty, Uint128};
8 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
9 |
10 | pub fn challenge_contract() -> Box> {
11 | let contract = ContractWrapper::new(
12 | crate::contract::execute,
13 | crate::contract::instantiate,
14 | crate::contract::query,
15 | );
16 | Box::new(contract)
17 | }
18 |
19 | pub const USER1: &str = "user1";
20 | pub const USER2: &str = "user2";
21 | pub const ADMIN: &str = "admin";
22 |
23 | pub fn proper_instantiate() -> (App, Addr) {
24 | let mut app = App::default();
25 | let cw_template_id = app.store_code(challenge_contract());
26 |
27 | // init contract
28 | let msg = InstantiateMsg {
29 | owner: ADMIN.to_string(),
30 | threshold: Uint128::from(99u128),
31 | };
32 |
33 | let contract_addr = app
34 | .instantiate_contract(
35 | cw_template_id,
36 | Addr::unchecked(ADMIN),
37 | &msg,
38 | &[],
39 | "test",
40 | None,
41 | )
42 | .unwrap();
43 |
44 | app = mint_tokens(app, USER1.to_string(), Uint128::from(100u128));
45 |
46 | (app, contract_addr)
47 | }
48 |
49 | pub fn base_scenario() -> (App, Addr) {
50 | let mut app = App::default();
51 | let cw_template_id = app.store_code(challenge_contract());
52 |
53 | // init contract
54 | let msg = InstantiateMsg {
55 | owner: ADMIN.to_string(),
56 | threshold: Uint128::from(99u128),
57 | };
58 |
59 | let contract_addr = app
60 | .instantiate_contract(
61 | cw_template_id,
62 | Addr::unchecked(ADMIN),
63 | &msg,
64 | &[],
65 | "test",
66 | None,
67 | )
68 | .unwrap();
69 |
70 | // User 1 deposit
71 | app = mint_tokens(app, USER1.to_string(), Uint128::from(100u128));
72 | app.execute_contract(
73 | Addr::unchecked(USER1),
74 | contract_addr.clone(),
75 | &ExecuteMsg::Deposit {},
76 | &[coin(100, DENOM)],
77 | )
78 | .unwrap();
79 |
80 | // User 2 deposit
81 | app = mint_tokens(app, USER2.to_string(), Uint128::from(110u128));
82 | app.execute_contract(
83 | Addr::unchecked(USER2),
84 | contract_addr.clone(),
85 | &ExecuteMsg::Deposit {},
86 | &[coin(110, DENOM)],
87 | )
88 | .unwrap();
89 |
90 | (app, contract_addr)
91 | }
92 |
93 | pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App {
94 | app.sudo(cw_multi_test::SudoMsg::Bank(
95 | cw_multi_test::BankSudo::Mint {
96 | to_address: recipient,
97 | amount: vec![coin(amount.u128(), DENOM)],
98 | },
99 | ))
100 | .unwrap();
101 | app
102 | }
103 |
104 | #[test]
105 | fn basic_flow() {
106 | let (mut app, contract_addr) = proper_instantiate();
107 |
108 | let bal = app.wrap().query_balance(USER1, DENOM).unwrap();
109 | assert_eq!(bal.amount, Uint128::new(100));
110 |
111 | // User 1 deposit
112 | app.execute_contract(
113 | Addr::unchecked(USER1),
114 | contract_addr.clone(),
115 | &ExecuteMsg::Deposit {},
116 | &[coin(100, DENOM)],
117 | )
118 | .unwrap();
119 |
120 | let bal = app.wrap().query_balance(USER1, DENOM).unwrap();
121 | assert_eq!(bal.amount, Uint128::zero());
122 |
123 | // Query top depositor
124 | let top: Addr = app
125 | .wrap()
126 | .query_wasm_smart(contract_addr.clone(), &QueryMsg::Top {})
127 | .unwrap();
128 | assert_eq!(top, Addr::unchecked(USER1));
129 |
130 | // User 1 withdraw
131 | app.execute_contract(
132 | Addr::unchecked(USER1),
133 | contract_addr,
134 | &ExecuteMsg::Withdraw {
135 | amount: Uint128::new(100),
136 | },
137 | &[],
138 | )
139 | .unwrap();
140 |
141 | let bal = app.wrap().query_balance(USER1, DENOM).unwrap();
142 | assert_eq!(bal.amount, Uint128::new(100));
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/ctf-07/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-07/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 | use cosmwasm_std::{Addr, CosmosMsg, Uint128};
3 |
4 | #[cw_serde]
5 | pub struct InstantiateMsg {
6 | pub owner: String,
7 | pub threshold: Uint128,
8 | }
9 |
10 | #[cw_serde]
11 | pub enum ExecuteMsg {
12 | Deposit {},
13 | Withdraw { amount: Uint128 },
14 | OwnerAction { msg: CosmosMsg },
15 | UpdateConfig { new_threshold: Uint128 },
16 | }
17 |
18 | #[cw_serde]
19 | pub enum QueryMsg {
20 | Config {},
21 | UserBalance { address: String },
22 | Top {},
23 | }
24 |
25 | // We define a custom struct for each query response
26 | #[cw_serde]
27 | pub struct ConfigQueryResponse {
28 | pub owner: Addr,
29 | pub threshold: Uint128,
30 | }
31 |
--------------------------------------------------------------------------------
/ctf-07/src/state.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_std::{Addr, Uint128};
2 | use cw_storage_plus::{Item, Map};
3 |
4 | pub const OWNER: Item = Item::new("address");
5 |
6 | pub const THRESHOLD: Item = Item::new("config");
7 |
8 | pub const BALANCES: Map<&Addr, Uint128> = Map::new("user_balances");
9 |
--------------------------------------------------------------------------------
/ctf-08/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-08/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-08/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-08"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 | schemars = "0.8.10"
49 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
50 | thiserror = { version = "1.0.31" }
51 | cw721 = "0.17.0"
52 | cw721-base = "0.17.0"
53 | cw-utils = "1.0.1"
54 |
55 | [dev-dependencies]
56 | cw-multi-test = "0.16.2"
57 |
--------------------------------------------------------------------------------
/ctf-08/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-08/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 08: *Gjallarhorn*
4 |
5 | Open marketplace for an NFT project. Users can sell their own NFTs at any price or allow others to offer different NFTs in exchange to trade.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | BuyNFT {
11 | id: String,
12 | },
13 | NewSale {
14 | id: String,
15 | price: Uint128,
16 | tradable: bool,
17 | },
18 | CancelSale {
19 | id: String,
20 | },
21 | NewTrade {
22 | target: String,
23 | offered: String,
24 | },
25 | AcceptTrade {
26 | id: String,
27 | trader: String,
28 | },
29 | CancelTrade {
30 | id: String,
31 | },
32 | }
33 | ```
34 |
35 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
36 |
37 | **:house: Base scenario:**
38 | - The contract is newly instantiated.
39 | - `USER1` and `USER2` placed new sales of their NFTs, one of them is open for trades and the other does not.
40 |
41 | **:star: Goal for the challenge:**
42 | - Demonstrate how a user can retrieve other users' NFT for free.
43 |
44 | ## Scoring
45 |
46 | This challenge has been assigned a total of **150** points:
47 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
48 | - **35** points will be awarded for a proper recommendation that fixes the issue.
49 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
50 |
51 |
52 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
53 |
54 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
55 |
56 | ## Any questions?
57 |
58 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
59 |
60 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
61 |
--------------------------------------------------------------------------------
/ctf-08/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-08/src/error.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_std::{StdError, Uint128};
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("Payment is not the same as the price {price}")]
13 | IncorrectPayment { price: Uint128 },
14 |
15 | #[error("Selected NFT's offer is not tradeable")]
16 | NonTradeable {},
17 |
18 | #[error("The reply ID is unrecognized")]
19 | UnrecognizedReply {},
20 | }
21 |
--------------------------------------------------------------------------------
/ctf-08/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-08/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 | use cosmwasm_std::Uint128;
3 |
4 | #[cw_serde]
5 | pub struct InstantiateMsg {
6 | pub nft_address: String,
7 | }
8 |
9 | #[cw_serde]
10 | pub enum ExecuteMsg {
11 | BuyNFT {
12 | id: String,
13 | },
14 | NewSale {
15 | id: String,
16 | price: Uint128,
17 | tradable: bool,
18 | },
19 | CancelSale {
20 | id: String,
21 | },
22 | NewTrade {
23 | target: String,
24 | offered: String,
25 | },
26 | AcceptTrade {
27 | id: String,
28 | trader: String,
29 | },
30 | CancelTrade {
31 | id: String,
32 | },
33 | }
34 |
35 | #[cw_serde]
36 | pub enum QueryMsg {
37 | GetSale {
38 | id: String,
39 | },
40 | GetSalesBySeller {
41 | seller: String,
42 | from_index: Option,
43 | limit: Option,
44 | },
45 | GetTrade {
46 | id: String,
47 | trader: String,
48 | },
49 | GetTradesByTrader {
50 | trader: String,
51 | from_index: Option,
52 | limit: Option,
53 | },
54 | }
55 |
56 | // We define a custom struct for each query response
57 | #[cw_serde]
58 | pub struct GetCountResponse {
59 | pub count: i32,
60 | }
61 |
--------------------------------------------------------------------------------
/ctf-08/src/state.rs:
--------------------------------------------------------------------------------
1 | use schemars::JsonSchema;
2 | use serde::{Deserialize, Serialize};
3 |
4 | use cosmwasm_std::{Addr, Uint128};
5 | use cw_storage_plus::{Item, Map};
6 |
7 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
8 | pub struct Config {
9 | pub nft_contract: Addr,
10 | }
11 |
12 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
13 | pub struct Sale {
14 | pub nft_id: String,
15 | pub price: Uint128,
16 | pub owner: Addr,
17 | pub tradable: bool,
18 | }
19 |
20 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
21 | pub struct Trade {
22 | pub asked_id: String,
23 | pub to_trade_id: String,
24 | pub trader: Addr,
25 | }
26 |
27 | #[derive(Serialize, Deserialize, Default, Clone)]
28 | pub struct Operations {
29 | pub n_trades: Uint128,
30 | pub n_sales: Uint128,
31 | }
32 |
33 | pub const CONFIG: Item = Item::new("config");
34 | pub const SALES: Map = Map::new("sales");
35 | pub const TRADES: Map<(String, String), Trade> = Map::new("trades");
36 | pub const OPERATIONS: Item = Item::new("operations");
37 |
--------------------------------------------------------------------------------
/ctf-09/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-09/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-09/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-09"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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 | cw0 = "0.10.3"
48 | cw2 = "1.0.1"
49 | schemars = "0.8.10"
50 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
51 | thiserror = { version = "1.0.31" }
52 |
53 | [dev-dependencies]
54 | cw-multi-test = "0.16.2"
55 |
--------------------------------------------------------------------------------
/ctf-09/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-09/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 09: *Brisingamen*
4 |
5 | Staking contract that allows the owner to distribute staking rewards for stakers.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | IncreaseReward {},
11 | Deposit {},
12 | Withdraw { amount: Uint128 },
13 | ClaimRewards {},
14 | }
15 | ```
16 |
17 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
18 |
19 | **:house: Base scenario:**
20 | - The contract is setup with a `USER` stake and owner has increased global index rewards.
21 |
22 | **:star: Goal for the challenge:**
23 | - Demonstrate how a user can earn an unfair amount of rewards in relation to other users.
24 |
25 |
26 | ## Scoring
27 |
28 | This challenge has been assigned a total of **150** points:
29 | - **40** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
30 | - **35** points will be awarded for a proper recommendation that fixes the issue.
31 | - If the report is deemed valid, the remaining **75** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
32 |
33 |
34 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
35 |
36 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
37 |
38 | ## Any questions?
39 |
40 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
41 |
42 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
43 |
--------------------------------------------------------------------------------
/ctf-09/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-09/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{
4 | coin, to_binary, BankMsg, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Response,
5 | StdResult, Uint128,
6 | };
7 | use cw0::must_pay;
8 |
9 | use crate::error::ContractError;
10 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
11 | use crate::state::{State, UserRewardInfo, STATE, USERS};
12 |
13 | pub const DENOM: &str = "uawesome";
14 | pub const REWARD_DENOM: &str = "uoak";
15 |
16 | #[cfg_attr(not(feature = "library"), entry_point)]
17 | pub fn instantiate(
18 | deps: DepsMut,
19 | _env: Env,
20 | info: MessageInfo,
21 | _msg: InstantiateMsg,
22 | ) -> Result {
23 | let state = State {
24 | owner: info.sender.clone(),
25 | global_index: Decimal::zero(),
26 | total_staked: Uint128::zero(),
27 | };
28 | STATE.save(deps.storage, &state)?;
29 |
30 | Ok(Response::new()
31 | .add_attribute("action", "instantiate")
32 | .add_attribute("owner", info.sender))
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 {
42 | match msg {
43 | ExecuteMsg::IncreaseReward {} => increase_reward(deps, env, info),
44 | ExecuteMsg::Deposit {} => deposit(deps, info),
45 | ExecuteMsg::Withdraw { amount } => withdraw(deps, info, amount),
46 | ExecuteMsg::ClaimRewards {} => claim_rewards(deps, info),
47 | }
48 | }
49 |
50 | /// Entry point for owner to increase reward
51 | pub fn increase_reward(
52 | deps: DepsMut,
53 | _env: Env,
54 | info: MessageInfo,
55 | ) -> Result {
56 | let mut state = STATE.load(deps.storage)?;
57 |
58 | let amount = must_pay(&info, REWARD_DENOM).map_err(|_| ContractError::NoDenomSent {})?;
59 |
60 | if info.sender != state.owner {
61 | return Err(ContractError::Unauthorized {});
62 | }
63 |
64 | let total_stake = state.total_staked;
65 |
66 | if total_stake.is_zero() {
67 | // No need to distribute rewards if no one staked
68 | return Err(ContractError::NoUserStake {});
69 | }
70 |
71 | state.global_index += Decimal::from_ratio(amount, total_stake);
72 |
73 | STATE.save(deps.storage, &state)?;
74 |
75 | Ok(Response::new().add_attribute("action", "increase_reward"))
76 | }
77 |
78 | /// Entry point for users to deposit funds
79 | pub fn deposit(deps: DepsMut, info: MessageInfo) -> Result {
80 | let amount = must_pay(&info, DENOM).map_err(|_| ContractError::NoDenomSent {})?;
81 |
82 | let mut state = STATE.load(deps.storage)?;
83 |
84 | let mut user = USERS
85 | .load(deps.storage, &info.sender)
86 | .unwrap_or(UserRewardInfo {
87 | staked_amount: Uint128::zero(),
88 | user_index: state.global_index,
89 | pending_rewards: Uint128::zero(),
90 | });
91 |
92 | // update rewards
93 | update_rewards(&mut user, &state);
94 |
95 | // increase user amount
96 | user.staked_amount += amount;
97 |
98 | // increase total staked amount
99 | state.total_staked += amount;
100 |
101 | USERS.save(deps.storage, &info.sender, &user)?;
102 | STATE.save(deps.storage, &state)?;
103 |
104 | Ok(Response::new().add_attribute("action", "deposit"))
105 | }
106 |
107 | /// Entry point for users to withdraw funds
108 | pub fn withdraw(
109 | deps: DepsMut,
110 | info: MessageInfo,
111 | amount: Uint128,
112 | ) -> Result {
113 | let mut state = STATE.load(deps.storage)?;
114 |
115 | let mut user = USERS.load(deps.storage, &info.sender)?;
116 |
117 | if amount.is_zero() {
118 | return Err(ContractError::ZeroAmountWithdrawal {});
119 | }
120 |
121 | if user.staked_amount < amount {
122 | return Err(ContractError::WithdrawTooMuch {});
123 | }
124 |
125 | // update rewards
126 | update_rewards(&mut user, &state);
127 |
128 | // decrease user amount
129 | user.staked_amount -= amount;
130 |
131 | // decrease total staked amount
132 | state.total_staked -= amount;
133 |
134 | USERS.save(deps.storage, &info.sender, &user)?;
135 | STATE.save(deps.storage, &state)?;
136 |
137 | let msg = BankMsg::Send {
138 | to_address: info.sender.to_string(),
139 | amount: vec![coin(amount.u128(), DENOM)],
140 | };
141 |
142 | Ok(Response::new()
143 | .add_attribute("action", "withdraw")
144 | .add_message(msg))
145 | }
146 |
147 | /// Entry point for user to claim rewards
148 | pub fn claim_rewards(deps: DepsMut, info: MessageInfo) -> Result {
149 | let mut user = USERS.load(deps.storage, &info.sender)?;
150 |
151 | let state = STATE.load(deps.storage)?;
152 |
153 | // update rewards
154 | update_rewards(&mut user, &state);
155 |
156 | let amount = user.pending_rewards;
157 |
158 | // disallow claiming zero rewards
159 | if amount.is_zero() {
160 | return Err(ContractError::ZeroRewardClaim {});
161 | }
162 |
163 | // set pending rewards to zero
164 | user.pending_rewards = Uint128::zero();
165 |
166 | USERS.save(deps.storage, &info.sender, &user)?;
167 |
168 | let msg = BankMsg::Send {
169 | to_address: info.sender.to_string(),
170 | amount: vec![coin(amount.u128(), REWARD_DENOM)],
171 | };
172 |
173 | Ok(Response::new()
174 | .add_attribute("action", "claim_reward")
175 | .add_message(msg))
176 | }
177 |
178 | pub fn update_rewards(user: &mut UserRewardInfo, state: &State) {
179 | // no need update amount if zero
180 | if user.staked_amount.is_zero() {
181 | return;
182 | }
183 |
184 | // calculate pending rewards
185 | let reward = (state.global_index - user.user_index) * user.staked_amount;
186 | user.pending_rewards += reward;
187 |
188 | user.user_index = state.global_index;
189 | }
190 |
191 | #[cfg_attr(not(feature = "library"), entry_point)]
192 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
193 | match msg {
194 | QueryMsg::State {} => to_binary(&query_state(deps)?),
195 | QueryMsg::User { user } => to_binary(&query_user(deps, user)?),
196 | }
197 | }
198 |
199 | /// Query contract state
200 | pub fn query_state(deps: Deps) -> StdResult {
201 | let state = STATE.load(deps.storage)?;
202 | Ok(state)
203 | }
204 |
205 | /// Query user information
206 | pub fn query_user(deps: Deps, user: String) -> StdResult {
207 | let user = deps.api.addr_validate(&user).unwrap();
208 | let state = STATE.load(deps.storage)?;
209 | let mut user_info = USERS.load(deps.storage, &user)?;
210 | update_rewards(&mut user_info, &state);
211 | Ok(user_info)
212 | }
213 |
--------------------------------------------------------------------------------
/ctf-09/src/error.rs:
--------------------------------------------------------------------------------
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("Zero amount withdrawal is disallowed")]
13 | ZeroAmountWithdrawal {},
14 |
15 | #[error("No rewards to claim")]
16 | ZeroRewardClaim {},
17 |
18 | #[error("Withdraw amount higher than available balance")]
19 | WithdrawTooMuch {},
20 |
21 | #[error("Caller did not provide requested funds")]
22 | NoDenomSent {},
23 |
24 | #[error("No user staked")]
25 | NoUserStake {},
26 | }
27 |
--------------------------------------------------------------------------------
/ctf-09/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-09/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::{cw_serde, QueryResponses};
2 | use cosmwasm_std::Uint128;
3 |
4 | use crate::state::{State, UserRewardInfo};
5 |
6 | #[cw_serde]
7 | pub struct InstantiateMsg {}
8 |
9 | #[cw_serde]
10 | pub enum ExecuteMsg {
11 | /// Owner increase global index reward
12 | IncreaseReward {},
13 | /// User deposits
14 | Deposit {},
15 | /// User withdraws
16 | Withdraw { amount: Uint128 },
17 | /// User claim rewards
18 | ClaimRewards {},
19 | }
20 |
21 | #[cw_serde]
22 | #[derive(QueryResponses)]
23 | pub enum QueryMsg {
24 | /// Query contract state
25 | #[returns(State)]
26 | State {},
27 |
28 | /// Query user reward information
29 | #[returns(UserRewardInfo)]
30 | User { user: String },
31 | }
32 |
--------------------------------------------------------------------------------
/ctf-09/src/state.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 |
3 | use cosmwasm_std::{Addr, Decimal, Uint128};
4 | use cw_storage_plus::{Item, Map};
5 |
6 | #[cw_serde]
7 | pub struct State {
8 | pub owner: Addr,
9 | pub total_staked: Uint128,
10 | pub global_index: Decimal,
11 | }
12 |
13 | #[cw_serde]
14 | pub struct UserRewardInfo {
15 | pub staked_amount: Uint128,
16 | pub user_index: Decimal,
17 | pub pending_rewards: Uint128,
18 | }
19 |
20 | pub const STATE: Item = Item::new("state");
21 |
22 | pub const USERS: Map<&Addr, UserRewardInfo> = Map::new("users");
23 |
--------------------------------------------------------------------------------
/ctf-10/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | wasm = "build --release --lib --target wasm32-unknown-unknown"
3 | unit-test = "test --lib"
4 | schema = "run --bin schema"
5 |
--------------------------------------------------------------------------------
/ctf-10/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.rs]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/ctf-10/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oaksecurity-cosmwasm-ctf-10"
3 | version = "0.1.0"
4 | authors = ["Oak Security "]
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.2.1"
44 | cosmwasm-std = { version="1.2.7", features = ["cosmwasm_1_2"]}
45 | cosmwasm-storage = "1.1.3"
46 | cw-storage-plus = "1.0.1"
47 | cw2 = "1.0.1"
48 | schemars = "0.8.10"
49 | serde = { version = "1.0.145", default-features = false, features = ["derive"] }
50 | thiserror = { version = "1.0.31" }
51 | cw721 = "0.17.0"
52 | cw721-base = "0.17.0"
53 | cw-utils = "1.0.1"
54 |
55 | [dev-dependencies]
56 | cw-multi-test = "0.16.2"
57 |
--------------------------------------------------------------------------------
/ctf-10/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Oak Security
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/ctf-10/README.md:
--------------------------------------------------------------------------------
1 | # Awesomwasm 2023 CTF
2 |
3 | ## Challenge 10: *Mistilteinn*
4 |
5 | Smart contract that allows whitelisted users to mint NFTs.
6 |
7 | ### Execute entry points:
8 | ```rust
9 | pub enum ExecuteMsg {
10 | Mint {},
11 | }
12 | ```
13 |
14 | Please check the challenge's [integration_tests](./src/integration_test.rs) for expected usage examples. You can use these tests as a base to create your exploit Proof of Concept.
15 |
16 | **:house: Base scenario:**
17 | - The contract is instantiated with whitelisted users as `USER1`, `USER2`, and `USER3`.
18 |
19 | **:star: Goal for the challenge:**
20 | - Demonstrate how whitelisted users can bypass the `mint_per_user` limitation.
21 |
22 | ## Scoring
23 |
24 | This challenge has been assigned a total of **90** points:
25 | - **20** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above.
26 | - **25** points will be awarded for a proper recommendation that fixes the issue.
27 | - If the report is deemed valid, the remaining **45** additional points will be awarded for a working Proof of Concept exploit of the vulnerability.
28 |
29 |
30 | :exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion.
31 |
32 | :exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF.
33 |
34 | ## Any questions?
35 |
36 | If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible.
37 |
38 | Please remember that only questions about the functionality from the point of view of a standard user will be answered. Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered.
39 |
--------------------------------------------------------------------------------
/ctf-10/src/bin/schema.rs:
--------------------------------------------------------------------------------
1 | fn main() {}
2 |
--------------------------------------------------------------------------------
/ctf-10/src/contract.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "library"))]
2 | use cosmwasm_std::entry_point;
3 | use cosmwasm_std::{
4 | to_binary, wasm_instantiate, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo,
5 | Reply, Response, StdResult, SubMsg, WasmMsg,
6 | };
7 | use cw721::TokensResponse;
8 | use cw721_base::{
9 | ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg, QueryMsg as Cw721QueryMsg,
10 | };
11 | use cw_utils::parse_reply_instantiate_data;
12 |
13 | use crate::error::ContractError;
14 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
15 | use crate::state::{Config, Whitelist, CONFIG, WHITELIST};
16 |
17 | pub const DENOM: &str = "uawesome";
18 |
19 | #[cfg_attr(not(feature = "library"), entry_point)]
20 | pub fn instantiate(
21 | deps: DepsMut,
22 | env: Env,
23 | _info: MessageInfo,
24 | msg: InstantiateMsg,
25 | ) -> Result {
26 | // nft contract init msg
27 | let cw721_init_msg = Cw721InstantiateMsg {
28 | name: "Awesome Wasm".to_owned(),
29 | symbol: "AWESOME".to_owned(),
30 | minter: env.contract.address.to_string(),
31 | };
32 |
33 | let submsg = SubMsg::reply_on_success(
34 | wasm_instantiate(
35 | msg.cw721_code_id,
36 | &cw721_init_msg,
37 | vec![],
38 | "awesome nft contract".to_owned(),
39 | )
40 | .unwrap(),
41 | 1,
42 | );
43 |
44 | // store config
45 | let config = Config {
46 | nft_contract: Addr::unchecked(""),
47 | mint_per_user: msg.mint_per_user,
48 | total_tokens: 0,
49 | };
50 |
51 | CONFIG.save(deps.storage, &config)?;
52 |
53 | // validate and store whitelisted users
54 | let _ = msg
55 | .whitelisted_users
56 | .iter()
57 | .map(|user| deps.api.addr_validate(user).unwrap());
58 |
59 | let whitelist = Whitelist {
60 | users: msg.whitelisted_users,
61 | };
62 |
63 | WHITELIST.save(deps.storage, &whitelist)?;
64 |
65 | Ok(Response::new()
66 | .add_attribute("action", "instantiate")
67 | .add_attribute("mint_per_user", msg.mint_per_user.to_string())
68 | .add_attribute("total_whitelisted_users", whitelist.users.len().to_string())
69 | .add_submessage(submsg))
70 | }
71 |
72 | #[cfg_attr(not(feature = "library"), entry_point)]
73 | pub fn execute(
74 | deps: DepsMut,
75 | env: Env,
76 | info: MessageInfo,
77 | msg: ExecuteMsg,
78 | ) -> Result {
79 | match msg {
80 | ExecuteMsg::Mint {} => mint(deps, env, info),
81 | }
82 | }
83 |
84 | /// Mint NFT to recipient
85 | pub fn mint(deps: DepsMut, _env: Env, info: MessageInfo) -> Result {
86 | let mut config = CONFIG.load(deps.storage)?;
87 |
88 | // check user is in whitelist
89 | let users = WHITELIST.load(deps.storage)?.users;
90 | let is_whitelisted = users.iter().any(|user| user == &info.sender.to_string());
91 | if !is_whitelisted {
92 | return Err(ContractError::NotWhitelisted {});
93 | }
94 |
95 | let tokens_response: TokensResponse = deps.querier.query_wasm_smart(
96 | config.nft_contract.to_string(),
97 | &Cw721QueryMsg::Tokens:: {
98 | owner: info.sender.to_string(),
99 | start_after: None,
100 | limit: None,
101 | },
102 | )?;
103 |
104 | // ensure mint per user limit is not exceeded
105 | if tokens_response.tokens.len() >= config.mint_per_user as usize {
106 | return Err(ContractError::MaxLimitExceeded {});
107 | }
108 |
109 | let token_id = config.total_tokens;
110 |
111 | let msg = CosmosMsg::Wasm(WasmMsg::Execute {
112 | contract_addr: config.nft_contract.to_string(),
113 | msg: to_binary(&Cw721ExecuteMsg::Mint:: {
114 | token_id: token_id.to_string(),
115 | owner: info.sender.to_string(),
116 | token_uri: None,
117 | extension: Empty {},
118 | })?,
119 | funds: vec![],
120 | });
121 |
122 | // increment total tokens
123 | config.total_tokens += 1;
124 | CONFIG.save(deps.storage, &config)?;
125 |
126 | Ok(Response::new()
127 | .add_attribute("action", "mint")
128 | .add_attribute("recipient", info.sender.to_string())
129 | .add_attribute("token_id", token_id.to_string())
130 | .add_message(msg))
131 | }
132 |
133 | #[cfg_attr(not(feature = "library"), entry_point)]
134 | pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result {
135 | match reply.id {
136 | 1 => {
137 | let res = parse_reply_instantiate_data(reply).unwrap();
138 | let mut config = CONFIG.load(deps.storage)?;
139 | let nft_contract = deps.api.addr_validate(&res.contract_address).unwrap();
140 | config.nft_contract = nft_contract;
141 | CONFIG.save(deps.storage, &config)?;
142 | Ok(Response::default())
143 | }
144 | _ => Ok(Response::default()),
145 | }
146 | }
147 |
148 | #[cfg_attr(not(feature = "library"), entry_point)]
149 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult {
150 | match msg {
151 | QueryMsg::Config {} => to_binary(&query_config(deps)?),
152 | QueryMsg::Whitelist {} => to_binary(&query_whitelist(deps)?),
153 | }
154 | }
155 |
156 | /// Returns contract configuration
157 | fn query_config(deps: Deps) -> StdResult {
158 | let config = CONFIG.load(deps.storage)?;
159 | Ok(config)
160 | }
161 |
162 | /// Returns whitelisted users
163 | fn query_whitelist(deps: Deps) -> StdResult {
164 | let whitelist = WHITELIST.load(deps.storage)?;
165 | Ok(whitelist)
166 | }
167 |
--------------------------------------------------------------------------------
/ctf-10/src/error.rs:
--------------------------------------------------------------------------------
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("User is not whitelisted")]
13 | NotWhitelisted {},
14 |
15 | #[error("Max mint limit exceeded")]
16 | MaxLimitExceeded {},
17 | }
18 |
--------------------------------------------------------------------------------
/ctf-10/src/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | pub mod tests {
3 | use crate::{
4 | msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
5 | state::{Config, Whitelist},
6 | };
7 | use cosmwasm_std::{Addr, Empty};
8 |
9 | use cw_multi_test::{App, Contract, ContractWrapper, Executor};
10 |
11 | pub fn challenge_code() -> Box> {
12 | let contract = ContractWrapper::new(
13 | crate::contract::execute,
14 | crate::contract::instantiate,
15 | crate::contract::query,
16 | )
17 | .with_reply(crate::contract::reply);
18 | Box::new(contract)
19 | }
20 |
21 | fn cw721_code() -> Box> {
22 | let contract = ContractWrapper::new(
23 | cw721_base::entry::execute,
24 | cw721_base::entry::instantiate,
25 | cw721_base::entry::query,
26 | );
27 | Box::new(contract)
28 | }
29 |
30 | pub const ADMIN: &str = "admin";
31 | pub const USER1: &str = "user1";
32 | pub const USER2: &str = "user2";
33 | pub const USER3: &str = "user3";
34 |
35 | pub fn proper_instantiate() -> (App, Addr) {
36 | let mut app = App::default();
37 | let challenge_id = app.store_code(challenge_code());
38 | let cw_721_id = app.store_code(cw721_code());
39 |
40 | // Init challenge
41 | let challenge_inst = InstantiateMsg {
42 | cw721_code_id: cw_721_id,
43 | mint_per_user: 3,
44 | whitelisted_users: vec![USER1.to_owned(), USER2.to_owned(), USER3.to_owned()],
45 | };
46 |
47 | let contract_addr = app
48 | .instantiate_contract(
49 | challenge_id,
50 | Addr::unchecked(ADMIN),
51 | &challenge_inst,
52 | &[],
53 | "test",
54 | None,
55 | )
56 | .unwrap();
57 |
58 | (app, contract_addr)
59 | }
60 |
61 | #[test]
62 | fn basic_flow() {
63 | let (mut app, contract_addr) = proper_instantiate();
64 |
65 | // query config
66 | let config: Config = app
67 | .wrap()
68 | .query_wasm_smart(contract_addr.clone(), &QueryMsg::Config {})
69 | .unwrap();
70 |
71 | // query whitelisted users
72 | let whitelist: Whitelist = app
73 | .wrap()
74 | .query_wasm_smart(contract_addr.clone(), &QueryMsg::Whitelist {})
75 | .unwrap();
76 |
77 | assert!(whitelist.users.contains(&USER1.to_owned()));
78 | assert!(whitelist.users.contains(&USER2.to_owned()));
79 | assert!(whitelist.users.contains(&USER3.to_owned()));
80 |
81 | let user4 = "user4";
82 |
83 | // mint to non-whitelisted user
84 | app.execute_contract(
85 | Addr::unchecked(user4),
86 | contract_addr.clone(),
87 | &ExecuteMsg::Mint {},
88 | &[],
89 | )
90 | .unwrap_err();
91 |
92 | // mint to whitelisted user until max limit
93 | assert_eq!(config.mint_per_user, 3);
94 |
95 | app.execute_contract(
96 | Addr::unchecked(USER1),
97 | contract_addr.clone(),
98 | &ExecuteMsg::Mint {},
99 | &[],
100 | )
101 | .unwrap();
102 | app.execute_contract(
103 | Addr::unchecked(USER1),
104 | contract_addr.clone(),
105 | &ExecuteMsg::Mint {},
106 | &[],
107 | )
108 | .unwrap();
109 | app.execute_contract(
110 | Addr::unchecked(USER1),
111 | contract_addr.clone(),
112 | &ExecuteMsg::Mint {},
113 | &[],
114 | )
115 | .unwrap();
116 |
117 | // exceed max limit fails
118 | app.execute_contract(
119 | Addr::unchecked(USER1),
120 | contract_addr.clone(),
121 | &ExecuteMsg::Mint {},
122 | &[],
123 | )
124 | .unwrap_err();
125 |
126 | // other users can mint freely
127 | app.execute_contract(
128 | Addr::unchecked(USER2),
129 | contract_addr.clone(),
130 | &ExecuteMsg::Mint {},
131 | &[],
132 | )
133 | .unwrap();
134 |
135 | // ensure total tokens increases
136 | let config: Config = app
137 | .wrap()
138 | .query_wasm_smart(contract_addr, &QueryMsg::Config {})
139 | .unwrap();
140 |
141 | assert_eq!(config.total_tokens, 4);
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/ctf-10/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod contract;
2 | mod error;
3 | pub mod integration_tests;
4 | pub mod msg;
5 | pub mod state;
6 |
7 | pub use crate::error::ContractError;
8 |
--------------------------------------------------------------------------------
/ctf-10/src/msg.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 |
3 | #[cw_serde]
4 | pub struct InstantiateMsg {
5 | pub cw721_code_id: u64,
6 | pub mint_per_user: u64,
7 | pub whitelisted_users: Vec,
8 | }
9 |
10 | #[cw_serde]
11 | pub enum ExecuteMsg {
12 | Mint {},
13 | }
14 |
15 | #[cw_serde]
16 | pub enum QueryMsg {
17 | Config {},
18 | Whitelist {},
19 | }
20 |
--------------------------------------------------------------------------------
/ctf-10/src/state.rs:
--------------------------------------------------------------------------------
1 | use cosmwasm_schema::cw_serde;
2 |
3 | use cosmwasm_std::Addr;
4 | use cw_storage_plus::Item;
5 |
6 | #[cw_serde]
7 | pub struct Config {
8 | /// NFT contract address
9 | pub nft_contract: Addr,
10 | /// Mint per user
11 | pub mint_per_user: u64,
12 | /// Total minted tokens
13 | pub total_tokens: u128,
14 | }
15 |
16 | #[cw_serde]
17 | pub struct Whitelist {
18 | /// whitelisted users to receive NFTs
19 | pub users: Vec,
20 | }
21 |
22 | pub const CONFIG: Item = Item::new("config");
23 | pub const WHITELIST: Item = Item::new("whitelist");
24 |
--------------------------------------------------------------------------------