├── .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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
1. Mjolnir6. Hofund
2. Gungnir7. Tyrfing
3. Laevateinn8. Gjallarhorn
4. Gram9. Brisingamen
5. Draupnir10. Mistilteinn
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 | --------------------------------------------------------------------------------