diff --git a/docs/content/developer/iota-move-ctf/challenge_1.mdx b/docs/content/developer/iota-move-ctf/challenge_1.mdx new file mode 100644 index 00000000000..ab3b6f588b4 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_1.mdx @@ -0,0 +1,41 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 1: Checkin + +In this first challenge, your task is to interact with a basic Move contract. The contract defines a `Flag` object, which you need to retrieve by calling a specific function. + +The contract mints a new flag and transfers it to your account. Your goal is to [call the function](../getting-started/publish.mdx#accessing-your-package), capture the flag, and then submit the object ID to verify your success. + +This challenge is designed to be an easy introduction, guiding you through the process of interacting with the blockchain, calling functions, and understanding the basics of flag capture. + + +## Deployed Contract Address: +``` +Package: 0xce9b1471301ffaf1453297cca008a68ce851b6a9ba9ab241c357c346177903f3 +``` + +## Contracts +### `checkin.move` +```move file=/docs/examples/move/ctf/challenge_1/sources/checkin.move +``` + + +## Related Articles + +In this challenge, you must use the IOTA CLI to interact with the blockchain. This set of articles will help you set up your environment and call a deployed contract: + +- [Installing IOTA CLI](../getting-started/install-iota.mdx) +- [Connecting to Alphanet](../getting-started/connect.mdx) +- [Get Iota coins](../getting-started/get-coins.mdx) +- [Accessing Your Package](../getting-started/publish.mdx#accessing-your-package) + + +Good luck in capturing your first flag! + + + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/challenge_2.mdx b/docs/content/developer/iota-move-ctf/challenge_2.mdx new file mode 100644 index 00000000000..1c06d6d564b --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_2.mdx @@ -0,0 +1,46 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 2: Lucky Number + +In this challenge you are supposed to get the flag Event by passing in the right parameters to the `get_flag` function in the `luckynumber` module. If you do this correctly you should get a Flag event in return. + + + +## Deployed Contract Addresses: +``` +Package: 0xb13a3cd66c6aa2ccff512fee9d950176acf0835fbf2091fa32e789d44baabe01 +Counter: 0x88c94654907f9daabbc25e9724997bd71a16e13f55cc4580f5e7c207e3ff28f2 +``` + +## Contracts +### `luckynumber.move` +```move file=/docs/examples/move/ctf/challenge_2/sources/luckynumber.move +``` + +### `counter.move` +```move file=/docs/examples/move/ctf/challenge_2/sources/counter.move +``` + + +## Related Articles +In this stage of the CTF, you should be familiar with how to use the CLI to call a Move function and pass in the right parameters, as well as a general understanding of the Object Model: + +- [IOTA CLI reference](../../references/cli/) +- [Object Model](../iota-101/objects/object-model.mdx) + + + +:::tip +You should check how to use the CLI to call a function in a module and pass in the right parameters. `iota client call --help` might help. +::: + + +Good luck in capturing your second flag! + + + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/challenge_3.mdx b/docs/content/developer/iota-move-ctf/challenge_3.mdx new file mode 100644 index 00000000000..3186d0c6319 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_3.mdx @@ -0,0 +1,48 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 3: MintCoin Mechanics + +In this challenge, you'll dive into the mechanics of "MintCoin," a [coin](../standards/coin.mdx) that allows anyone to mint new tokens using a "Proof of Move" process. However, minting alone will not be enough to get the flag—you'll need to go a step further. + +Your goal is to understand how the system works and figure out the extra steps needed to successfully retrieve the flag. + + +## Deployed Contract Addresses: +``` +CoinMetadata<0xf84628ac335e59ce6e305835cc09c0b9983d7b1695ab28e96cf875b49923e4ed::mintcoin::MINTCOIN>: 0x1bd5dfa2e5f1d3a3825403d92b8199ce3f69a5e70a785f28d698715b54d78321 +Counter: 0xe7877309899ef0618ea0e269327f79e3bdf38ff2860fd01f5d278b46ea8cd630 +TreasuryCap<0xf84628ac335e59ce6e305835cc09c0b9983d7b1695ab28e96cf875b49923e4ed::mintcoin::MINTCOIN>: 0xf3cb314954f0823961fdfe93ba9403314b4c53bb654f73ba37fe3c8400831e23 +Package: 0xf84628ac335e59ce6e305835cc09c0b9983d7b1695ab28e96cf875b49923e4ed +``` + +## Contracts + +#### `mint.move` +```move file=/docs/examples/move/ctf/challenge_3/sources/mint.move +``` + +#### `counter.move` +```move file=/docs/examples/move/ctf/challenge_3/sources/counter.move +``` + + +## Related Articles +This challenge's main contract is written using the Coin Standard. Having been familiar with the [Object Model](../iota-101/objects/object-model.mdx) from the last challenge, you should now be able to understand the Coin Standard and how it works. + +- [Coin Standard](../standards/coin.mdx) + + + +:::tip +Your starting point should be the function `get_flag` in the `mint` module to understand the steps required to capture the flag. To successfully complete the challenge, make sure to follow the contract's logic and requirements. +::: + +Good luck in capturing your third flag! + + + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/challenge_4.mdx b/docs/content/developer/iota-move-ctf/challenge_4.mdx new file mode 100644 index 00000000000..2442c2b30a8 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_4.mdx @@ -0,0 +1,49 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 4: Airdrop + +Your mission is to participate in the "Horse Token" airdrop and capture the elusive flag. You'll need to mint some Horse Tokens and claim your share through the airdrop mechanism. But simply collecting tokens won’t be enough—securing the flag requires a bit more effort. + +Use your command line expertise to interact with the system, track your progress, and perform key actions efficiently. Pay close attention to the airdrop logic, as understanding how the token distribution works will be crucial to successfully capturing the flag. + +## Deployed Contract Addresses: +``` +CoinMetadata<0x913ddcf84345bc087530e0b5d8b183780ecf902ea6cfa64b62d3f8a349ebafd9::airdrop::AIRDROP>: 0x163ef67f9631eea22ef33e58aab3e0d5c243184335e943047c28cb0a30985cf0 +Vault: 0x4ae07fd00773080f9af2e43c2464667536d661ba0001a670e3971efbb01446e2 +Package: 0x913ddcf84345bc087530e0b5d8b183780ecf902ea6cfa64b62d3f8a349ebafd9 +TreasuryCap<0x913ddcf84345bc087530e0b5d8b183780ecf902ea6cfa64b62d3f8a349ebafd9::airdrop::AIRDROP>: 0xa9b65a0be78472f053298980a4fae935d12571731bfd7be3b9d41183a7f100ff +Counter: 0xc96bfaf42e3b8b1f2e5dbf469dc5f7846c911dbbb76966475cfd06cf3893b080 +``` + +## Contracts + +### `airdrop.move` +```move file=/docs/examples/move/ctf/challenge_4/sources/airdrop.move +``` + +### `counter.move` +```move file=/docs/examples/move/ctf/challenge_4/sources/counter.move +``` + +## Related Articles +Challenges 1-3 have introduced you to the basics of interacting with Move contracts, the Object Model, and the Coin Standard. In this challenge, you'll need to apply your knowledge to a more complex scenario involving an airdrop mechanism. +This challnege can be solved with IOTA PTBs, which will also help you in further challenges. + +- [Coin Standard](../standards/coin.mdx) +- [Object Model](../iota-101/objects/object-model.mdx) +- [Programmable Transaction Blocks](../iota-101/transactions/ptb/working-with-ptbs.mdx) + + + +Good luck in capturing your fourth flag! + + +:::tip +Under `Deployed Contract Addresses`, you can find the addresses of the package as well as the `Vault`. Carefully check what the constraints are for the `get_flag` function to work, as it has some assertions that need to be met. +::: + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/challenge_5.mdx b/docs/content/developer/iota-move-ctf/challenge_5.mdx new file mode 100644 index 00000000000..c8ba92196c2 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_5.mdx @@ -0,0 +1,39 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 5: Perfect Pizza + +When in Italy, pizza is a way of life. Crafted from simple ingredients with an artful touch, only the perfect combination will gain the approval of the pizzaiolo (master pizza maker). Choose your ingredients wisely—anything less than perfection and your creation won’t pass the test. + +In this challenge, you'll need to carefully assemble your pizza using the correct ingredients. The pizzaiolo will judge your creation, and only a perfect pizza will earn you the flag. Attention to detail is key—get it right, or it's back to the kitchen! + +## Deployed Contract Address: +``` +Package: 0x84c3037c252e1b9142087a19c2bd776ee86316775484eb78e9f97618d877a577 +``` + +## Contracts + +### `pizza.move` +```move file=/docs/examples/move/ctf/challenge_5/sources/pizza.move +``` + +:::tip +The pizzaiolo uses [bcs::to_bytes](../../references/framework/iota-framework/bcs.mdx#function-to_bytes) to make sure the ingredients are in order. Make sure you understand how to use this function to pass the test. +::: + + +## Related Articles +Now that your are familiar with the basics of Move, this challenge will introduce a function from the iota-framework which you should be familiar with. +After taking a look at the challenge's usage of [bcs::to_bytes](../../references/framework/iota-framework/bcs.mdx#function-to_bytes), we recommend you to take a look at the IOTA Framework documentation to understand how to use it in further challenges. + +- [IOTA Framework](../../references/framework/iota-framework/) + + +Good luck in capturing your fifth flag! + + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/challenge_6.mdx b/docs/content/developer/iota-move-ctf/challenge_6.mdx new file mode 100644 index 00000000000..05587dd8ca2 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_6.mdx @@ -0,0 +1,47 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 6: Go Recycle! + +The City of Venice is tired of all the trash ending up in the canals, especially pizza boxes still filled with a certain type of pizza which are just dumped everywhere are ruining the experience. They decided to start a recycling program rewarding people who do their part with a Venetian Flag; Go grab one! + +For this challenge you might want to investigate the [Transfer to object](../iota-101/objects/transfers/transfer-to-object.mdx) functionality; Pizzaboxes from previous challenges can not be used in this one. + +## Deployed Contract Addresses: +``` +PizzaBoxRecycler: 0x16ddd3ae8cc4fe71f1acdc52838412a645eac93f1176450d05a77642f1816f34 +Package: 0xcbe251b41a23a3952e64036f01367df82f1ccf3498cb139ff3ef44712441abc9 +``` + +## Contracts + +### recycle.move +```move file=/docs/examples/move/ctf/challenge_6/sources/recycle.move +``` + +### pizza.move +```move file=/docs/examples/move/ctf/challenge_6/sources/pizza.move +``` + + +## Related Articles + +The previous challenges covered the basics of the Object Model and how to interact with the blockchain. This challenge's main theme is object transfers, which will be crucial to capturing the flag. +We recommend you to check the following articles: + +- [Object Transfers](../iota-101/objects/transfers/) +- [Transfer to object](../iota-101/objects/transfers/transfer-to-object.mdx) +- [Custom Transfer Rules](../iota-101/objects/transfers/custom-rules.mdx) + + + +Good luck in capturing your sixth flag! + +:::tip +Make sure you recycle enough pizza boxes! +::: + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/challenge_7.mdx b/docs/content/developer/iota-move-ctf/challenge_7.mdx new file mode 100644 index 00000000000..2e534975317 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_7.mdx @@ -0,0 +1,38 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 7: PTBs + +In response to the recent pizza challenge, the city of Venice has implemented a smart contract to efficiently manage ingredients and prevent hoarding. Participants are invited to utilize these perishable ingredients, which must be used immediately to create dough. + +Your objective is to gather the necessary ingredients—flour, water, yeast, and salt—and craft the dough required to capture the flag. This challenge can be solved using the Move CLI and the [`iota client ptb` command.](../../references/cli/ptb.mdx) + +## Deployed Contract Addresses: +``` +Package: 0x202d65a2b1d2de4ba90e9eeb51ef4e16fafdaaa5c8b1dc3cbd8a935e5eb4d25c +``` + +## Contracts + +### `ptb.move` +```move file=/docs/examples/move/ctf/challenge_7/sources/ptb.move +``` + + +## Related Articles + +This challenge will introduce you to the PTB standard and how to use the Move CLI to interact with it. You should be familiar with the PTB standard and how to use the Move CLI to call the `ptb` function. + +- [PTB Standard](../iota-101/transactions/ptb/working-with-ptbs.mdx) +- [IOTA CLI reference](../../references/cli/ptb.mdx) + + + +Good luck in capturing your seventh flag! + + + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/challenge_8.mdx b/docs/content/developer/iota-move-ctf/challenge_8.mdx new file mode 100644 index 00000000000..66cf831c1b7 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/challenge_8.mdx @@ -0,0 +1,56 @@ + +import ChallengeVerifier from '@site/src/components/CTF/ctf-verifier'; + + + + +# Challenge 8: Flash! + +In this challenge, you will explore a decentralized exchange (DEX) with a critical flaw you can exploit to capture the flag. This exchange operates with two tokens—CTFA and CTFB—and features a vault that allows users to take flash loans. Your objective is to manipulate the token balances effectively to obtain the flag by using the vulnerabilities in the DEX's flash loan mechanism. +To solve this challenge, you will have to have a deep understanding of [programmable transaction blocks (PTBs)](../iota-101/transactions/ptb/prog-txn-blocks.mdx) and how to build them using the [TS SDK](../iota-101/transactions/ptb/building-ptb.mdx) or the [CLI](../../references/cli/ptb.mdx). + +## Deployed Contract Addresses: +``` +MintA<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfa::CTFA>: 0x66e8dd865238a68f50db8be7177ee662b754133f409c35c36975f9d6e6f7f6e4 +MintB<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfb::CTFB>: 0x7c4f0f9d2e62bb0c440e5d281fbac69997dc14e1586cfedcea49f547a54eca1b +Package: 0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33 +CoinMetadata<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfa::CTFA>: 0x8f9c961398fcbfff8b9b1f14e1d6731bcfa2480f7c4f26fbb76496498bcc684e +CoinMetadata<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfb::CTFB>: 0xca909dd26da43ac1d0992ebe796e0714d9382e0e4ec35640a1ac9845b3bb087d +``` + +## Contracts + +### `firstcoin.move` +```move file=/docs/examples/move/ctf/challenge_8/sources/firstcoin.move +``` + +### `secondcoin.move` +```move file=/docs/examples/move/ctf/challenge_8/sources/secondcoin.move +``` + +### `vault.move` +```move file=/docs/examples/move/ctf/challenge_8/sources/vault.move +``` + +Good luck in capturing your eighth flag! + + +## Related Articles +This challenge will test your understanding of the Object Model, the Coin Standard, and PTBs. You will need to use your knowledge of these concepts to exploit the DEX's flash loan mechanism and capture the flag. + +- [Coin Standard](../standards/coin.mdx) +- [Object Model](../iota-101/objects/object-model.mdx) +- [Programmable Transaction Blocks](../iota-101/transactions/ptb/working-with-ptbs.mdx) +- [IOTA CLI reference](../../references/cli/ptb.mdx) +- [IOTA TS SDK reference](../iota-101/transactions/ptb/building-ptb.mdx) + + + +:::tip +The DEX programmer pulled an all-nighter before writing the flash loan mechanism, making a critical mistake. +::: + + + + + \ No newline at end of file diff --git a/docs/content/developer/iota-move-ctf/introduction.mdx b/docs/content/developer/iota-move-ctf/introduction.mdx new file mode 100644 index 00000000000..ab5d39c2c13 --- /dev/null +++ b/docs/content/developer/iota-move-ctf/introduction.mdx @@ -0,0 +1,65 @@ +# Move on IOTA Capture The Flag (CTF) + +The **Move on IOTA Capture The Flag (CTF)** is a unique learning experience designed to help developers master the Move programming language by solving a series of challenges. These challenges simulate real-world scenarios that require participants to find and exploit vulnerabilities, optimize contract logic, and interact with IOTA’s alphanet. Whether you're new to Move or experienced with smart contract development, this CTF is an excellent way to sharpen your skills. + +This CTF features **eight challenges**, each built on Move, a language optimized for safety and scalability in decentralized applications. Throughout the event, participants are encouraged to explore Move’s features, including its resource-based design and transaction scripting model, in a competitive environment. However, bear in mind that the provided demonstration contracts may intentionally include distractions, poor design, and obscure naming conventions to increase difficulty. + +## What to Expect + +- **Varied Difficulty:** The challenges are structured progressively. The lower-numbered challenges are easier, while the difficulty increases as you proceed. +- **No Contract Modification:** Participants are prohibited from modifying the provided contracts; the goal is to interact with them via Move scripts, client libraries, or command-line tools. +- **Explore Move in Depth:** As you tackle these challenges, you'll get hands-on experience with Move’s key features, such as resource management, transaction scripts, and smart contract modules. Each challenge is designed to push you to think critically and apply Move concepts effectively. + + +## Capturing Flags + +Upon successfully completing a challenge, you will receive a "flag," which is a Move object generated as proof of completion. To validate your progress, there will be a submission window where you can enter the ID of the flag object. The system will verify whether the submitted flag is correct or not. Only valid flags will be counted toward your overall score in the CTF. + +## Setup + +The CTF contract are already deployed on the IOTA alphanet. To get started, you need to install the IOTA CLI tool and connect to the alphanet. If you haven't done so already, follow these steps: + +1. [Install the IOTA CLI Tool](../getting-started/install-iota.mdx) +2. [Connect to alphanet](../getting-started/connect.mdx) +3. [Create and Address](../getting-started/get-address.mdx) +4. [Fund Your Address](../getting-started/get-coins.mdx) +5. Get the flags! + + + +## Challenge Addresses + +| Challenge | Entity | Address | +|-------------|------------------------------------------------------------------|----------------------------------------------------------------| +| challenge_1 | Package | `0xce9b1471301ffaf1453297cca008a68ce851b6a9ba9ab241c357c346177903f3` | +| challenge_2 | Counter | `0x88c94654907f9daabbc25e9724997bd71a16e13f55cc4580f5e7c207e3ff28f2` | +| | Package | `0xb13a3cd66c6aa2ccff512fee9d950176acf0835fbf2091fa32e789d44baabe01` | +| challenge_3 | `CoinMetadata<0xf84628ac335e59ce6e305835cc09c0b9983d7b1695ab28e96cf875b49923e4ed::mintcoin::MINTCOIN>` | `0x1bd5dfa2e5f1d3a3825403d92b8199ce3f69a5e70a785f28d698715b54d78321` | +| | Counter | `0xe7877309899ef0618ea0e269327f79e3bdf38ff2860fd01f5d278b46ea8cd630` | +| | `TreasuryCap<0xf84628ac335e59ce6e305835cc09c0b9983d7b1695ab28e96cf875b49923e4ed::mintcoin::MINTCOIN>` | `0xf3cb314954f0823961fdfe93ba9403314b4c53bb654f73ba37fe3c8400831e23` | +| | Package | `0xf84628ac335e59ce6e305835cc09c0b9983d7b1695ab28e96cf875b49923e4ed` | +| challenge_4 | `CoinMetadata<0x913ddcf84345bc087530e0b5d8b183780ecf902ea6cfa64b62d3f8a349ebafd9::airdrop::AIRDROP>` | `0x163ef67f9631eea22ef33e58aab3e0d5c243184335e943047c28cb0a30985cf0` | +| | Vault | `0x4ae07fd00773080f9af2e43c2464667536d661ba0001a670e3971efbb01446e2` | +| | Package | `0x913ddcf84345bc087530e0b5d8b183780ecf902ea6cfa64b62d3f8a349ebafd9` | +| | `TreasuryCap<0x913ddcf84345bc087530e0b5d8b183780ecf902ea6cfa64b62d3f8a349ebafd9::airdrop::AIRDROP>` | `0xa9b65a0be78472f053298980a4fae935d12571731bfd7be3b9d41183a7f100ff` | +| | Counter | `0xc96bfaf42e3b8b1f2e5dbf469dc5f7846c911dbbb76966475cfd06cf3893b080` | +| challenge_5 | Package | `0x84c3037c252e1b9142087a19c2bd776ee86316775484eb78e9f97618d877a577` | +| challenge_6 | PizzaBoxRecycler | `0x16ddd3ae8cc4fe71f1acdc52838412a645eac93f1176450d05a77642f1816f34` | +| | Package | `0xcbe251b41a23a3952e64036f01367df82f1ccf3498cb139ff3ef44712441abc9` | +| challenge_7 | Package | `0x202d65a2b1d2de4ba90e9eeb51ef4e16fafdaaa5c8b1dc3cbd8a935e5eb4d25c` | +| challenge_8 | `MintA<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfa::CTFA>` | `0x66e8dd865238a68f50db8be7177ee662b754133f409c35c36975f9d6e6f7f6e4` | +| | `MintB<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfb::CTFB>` | `0x7c4f0f9d2e62bb0c440e5d281fbac69997dc14e1586cfedcea49f547a54eca1b` | +| | Package | `0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33` | +| | `CoinMetadata<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfa::CTFA>` | `0x8f9c961398fcbfff8b9b1f14e1d6731bcfa2480f7c4f26fbb76496498bcc684e` | +| | `CoinMetadata<0x828a5da05496e86075fab366b2b66ec3ba0a3bfbed68ce56140d589d94da9b33::ctfb::CTFB>` | `0xca909dd26da43ac1d0992ebe796e0714d9382e0e4ec35640a1ac9845b3bb087d` | + +``` +-------------------------------------------------------------------------------- +Package ID for Verifier: 0xdf6b7e52065dc5d4ae29fe212763226ba4e12eae81944d2ae6cf507296a767b1 +CTFCap Object ID: 0xc9ee0b68715c3eb8130e25c2bddd67552e5787cc4271f2a75955aab4b0927b99 +Shared Challenges Object ID: 0x44cabb21a9f66f8ce02c3c4881595bf439c2740c41341c7d257ee41e6238a4a0 +-------------------------------------------------------------------------------- +``` + + + diff --git a/docs/content/sidebars/developer.js b/docs/content/sidebars/developer.js index 3214d7deb9e..b96192b54a1 100644 --- a/docs/content/sidebars/developer.js +++ b/docs/content/sidebars/developer.js @@ -174,6 +174,24 @@ const developer = [ 'developer/iota-101/access-time', ], }, + { + type: 'category', + label: 'Capture The Flag', + link: { + type: 'doc', + id: 'developer/iota-move-ctf/introduction', + }, + items: [ + 'developer/iota-move-ctf/challenge_1', + 'developer/iota-move-ctf/challenge_2', + 'developer/iota-move-ctf/challenge_3', + 'developer/iota-move-ctf/challenge_4', + 'developer/iota-move-ctf/challenge_5', + 'developer/iota-move-ctf/challenge_6', + 'developer/iota-move-ctf/challenge_7', + 'developer/iota-move-ctf/challenge_8', + ], + }, { type: 'category', label: 'Standards', diff --git a/docs/examples/move/ctf/README.md b/docs/examples/move/ctf/README.md new file mode 100644 index 00000000000..700e6412b6f --- /dev/null +++ b/docs/examples/move/ctf/README.md @@ -0,0 +1,30 @@ +# Move on IOTA Capture the Flag (CTF) + +## Trying the Challenges + +The challneges are already deployed on the IOTA alphanet. To get started, you need to start with reading the [CTF introduction](../../docs/content/developer/iota-move-ctf/introduction.mdx), follow its steps to interact with the challenges, and capture the flags. + +## Deploying the challneges yourself (Not Required) + +If you want to experiment further with the contracts or deploy the challenges in your own environment, you can set them up yourself using the provided `deploy.py` script. While this is not required to participate in the CTF, deploying the challenges locally can help you explore the contracts in a controlled environment. + +To deploy the challenges yourself: + +1. **Prepare the Environment:** + - Ensure you have Python 3 installed. + - Install the IOTA CLI tool and ensure it is available in your system's `PATH`. + +2. **Run the Deployment Script:** + - Inside the the CTF directory, run the [`deploy.py`](../../../../examples/ctf/deploy.py). + - The script requires two arguments: + 1. The path to the `iota` binary (the CLI tool you installed earlier). You can can find the path by running `which iota` in your terminal in Linux or macOS, or `where iota` in Windows. + 2. The full path to the CTF repository. + - Example command: + ```bash + python3 deploy.py /path/to/iota /path/to/ctf/repository + ``` + +3. **Interact with the Deployed Contracts:** + - Once the challenges are deployed in your local environment, you can interact with them in a similar way as on the devnet. You’ll be able to submit transactions, test edge cases, and attempt to capture the flags using the deployed contracts. + +This setup allows you to modify the contracts and experiment with different scenarios. diff --git a/docs/examples/move/ctf/challenge_1/Move.toml b/docs/examples/move/ctf/challenge_1/Move.toml new file mode 100644 index 00000000000..6cf34846268 --- /dev/null +++ b/docs/examples/move/ctf/challenge_1/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "checkin" +# version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +# iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_1/README.md b/docs/examples/move/ctf/challenge_1/README.md new file mode 100644 index 00000000000..14c79f1bf6f --- /dev/null +++ b/docs/examples/move/ctf/challenge_1/README.md @@ -0,0 +1,3 @@ +# Challenge 1: Hello World + +This contract is used in the [first challenge](../../../../content/developer/iota-move-ctf/challenge_1.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_1/sources/checkin.move b/docs/examples/move/ctf/challenge_1/sources/checkin.move new file mode 100644 index 00000000000..ab0046142e7 --- /dev/null +++ b/docs/examples/move/ctf/challenge_1/sources/checkin.move @@ -0,0 +1,14 @@ +module ctf::checkin { + + public struct Flag has key, store { + id: UID, + user: address + } + + public entry fun get_flag(ctx: &mut TxContext) { + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/challenge_2/Move.toml b/docs/examples/move/ctf/challenge_2/Move.toml new file mode 100644 index 00000000000..8330faabfb6 --- /dev/null +++ b/docs/examples/move/ctf/challenge_2/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "luckynumber" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_2/README.md b/docs/examples/move/ctf/challenge_2/README.md new file mode 100644 index 00000000000..68f5060fcdf --- /dev/null +++ b/docs/examples/move/ctf/challenge_2/README.md @@ -0,0 +1,3 @@ +# Challenge 2: Lucky Number + +This contract is used in the [second challenge](../../../../content/developer/iota-move-ctf/challenge_2.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_2/sources/counter.move b/docs/examples/move/ctf/challenge_2/sources/counter.move new file mode 100644 index 00000000000..9b20c1a1a1c --- /dev/null +++ b/docs/examples/move/ctf/challenge_2/sources/counter.move @@ -0,0 +1,45 @@ +module ctf::counter { + + const MaxCounter: u64 = 10; + const ENoAttemptLeft: u64 = 0; + + /// A shared counter. + public struct Counter has key { + id: UID, + owner: address, + value: u64 + } + + /// Create and share a Counter object. + public(package) fun create_counter(ctx: &mut TxContext) { + transfer::share_object(Counter { + id: object::new(ctx), + owner: tx_context::sender(ctx), + value: 0 + }) + } + + public fun owner(counter: &Counter): address { + counter.owner + } + + public fun value(counter: &Counter): u64 { + counter.value + } + + /// Increment a counter by 1. + public fun increment(counter: &mut Counter) { + counter.value = counter.value + 1; + } + + /// Set value (only runnable by the Counter owner) + public entry fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) { + assert!(counter.owner == tx_context::sender(ctx), 0); + counter.value = value; + } + + /// Check whether the counter has reached the limit. + public fun is_within_limit(counter: &mut Counter) { + assert!(counter.value <= MaxCounter, ENoAttemptLeft); + } +} diff --git a/docs/examples/move/ctf/challenge_2/sources/luckynumber.move b/docs/examples/move/ctf/challenge_2/sources/luckynumber.move new file mode 100644 index 00000000000..b3ba9f733e9 --- /dev/null +++ b/docs/examples/move/ctf/challenge_2/sources/luckynumber.move @@ -0,0 +1,23 @@ +module ctf::luckynumber { + use ctf::counter::{Self, Counter}; + + fun init(ctx: &mut TxContext) { + counter::create_counter(ctx); + } + + public struct Flag has key, store { + id: UID, + user: address + } + + public entry fun get_flag(user_counter: &mut Counter, lucky_num: u64, ctx: &mut TxContext) { + counter::increment(user_counter); + counter::is_within_limit(user_counter); + + let _ = lucky_num; + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/challenge_3/Move.toml b/docs/examples/move/ctf/challenge_3/Move.toml new file mode 100644 index 00000000000..b83d9a7f95b --- /dev/null +++ b/docs/examples/move/ctf/challenge_3/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "airdrop" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_3/README.md b/docs/examples/move/ctf/challenge_3/README.md new file mode 100644 index 00000000000..6ac7f3610f5 --- /dev/null +++ b/docs/examples/move/ctf/challenge_3/README.md @@ -0,0 +1,3 @@ +# Challenge 3: Degen logic + +This contract is used in the [third challenge](../../../../content/developer/iota-move-ctf/challenge_3.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_3/sources/counter.move b/docs/examples/move/ctf/challenge_3/sources/counter.move new file mode 100644 index 00000000000..9b20c1a1a1c --- /dev/null +++ b/docs/examples/move/ctf/challenge_3/sources/counter.move @@ -0,0 +1,45 @@ +module ctf::counter { + + const MaxCounter: u64 = 10; + const ENoAttemptLeft: u64 = 0; + + /// A shared counter. + public struct Counter has key { + id: UID, + owner: address, + value: u64 + } + + /// Create and share a Counter object. + public(package) fun create_counter(ctx: &mut TxContext) { + transfer::share_object(Counter { + id: object::new(ctx), + owner: tx_context::sender(ctx), + value: 0 + }) + } + + public fun owner(counter: &Counter): address { + counter.owner + } + + public fun value(counter: &Counter): u64 { + counter.value + } + + /// Increment a counter by 1. + public fun increment(counter: &mut Counter) { + counter.value = counter.value + 1; + } + + /// Set value (only runnable by the Counter owner) + public entry fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) { + assert!(counter.owner == tx_context::sender(ctx), 0); + counter.value = value; + } + + /// Check whether the counter has reached the limit. + public fun is_within_limit(counter: &mut Counter) { + assert!(counter.value <= MaxCounter, ENoAttemptLeft); + } +} diff --git a/docs/examples/move/ctf/challenge_3/sources/mint.move b/docs/examples/move/ctf/challenge_3/sources/mint.move new file mode 100644 index 00000000000..f3ed5916f1e --- /dev/null +++ b/docs/examples/move/ctf/challenge_3/sources/mint.move @@ -0,0 +1,39 @@ +module ctf::mintcoin { + use iota::coin::{Self, Coin, TreasuryCap}; + use ctf::counter::{Self, Counter}; + + + public struct MINTCOIN has drop {} + public struct Flag has key, store { + id: UID, + user: address + } + + #[allow(lint(share_owned))] + fun init(witness: MINTCOIN, ctx: &mut TxContext) { + counter::create_counter(ctx); + let (coincap, coindata) = coin::create_currency(witness, 0, b"MintCoin", b"Mint Coin", b"A coin that anyone can mint, mind blowing!", option::none(), ctx); + transfer::public_freeze_object(coindata); + transfer::public_share_object(coincap); + } + + public entry fun mint_coin(cap: &mut TreasuryCap, ctx: &mut TxContext) { + coin::mint_and_transfer(cap, 2, tx_context::sender(ctx), ctx); + } + + public entry fun burn_coin(cap: &mut TreasuryCap, coins: Coin) { + coin::burn(cap, coins); + } + + public entry fun get_flag(user_counter: &mut Counter, coins: &mut Coin, ctx: &mut TxContext) { + counter::increment(user_counter); + counter::is_within_limit(user_counter); + + let limit = coin::value(coins); + assert!(limit == 5, 1); + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/challenge_4/Move.toml b/docs/examples/move/ctf/challenge_4/Move.toml new file mode 100644 index 00000000000..b83d9a7f95b --- /dev/null +++ b/docs/examples/move/ctf/challenge_4/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "airdrop" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_4/README.md b/docs/examples/move/ctf/challenge_4/README.md new file mode 100644 index 00000000000..62c65cbae8d --- /dev/null +++ b/docs/examples/move/ctf/challenge_4/README.md @@ -0,0 +1,3 @@ +# Challenge 4: Airdrop + +This contract is used in the [fourth challenge](../../../../content/developer/iota-move-ctf/challenge_4.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_4/sources/airdrop.move b/docs/examples/move/ctf/challenge_4/sources/airdrop.move new file mode 100644 index 00000000000..ac57be4294c --- /dev/null +++ b/docs/examples/move/ctf/challenge_4/sources/airdrop.move @@ -0,0 +1,67 @@ +module ctf::airdrop { + use iota::table::{Self, Table}; + use iota::coin::{Self, Coin}; + use iota::balance::{Self, Balance}; + use ctf::counter::{Self, Counter}; + + + public struct AirdroppedTo has store { + user: address, + } + + public struct Vault has key { + id: UID, + balance: Balance, + userlist: Table, + } + + public struct AIRDROP has drop {} + + public struct Flag has key, store { + id: UID, + user: address + } + + fun init (witness: AIRDROP, ctx: &mut TxContext) { + counter::create_counter(ctx); + + let initializer = tx_context::sender(ctx); + let (mut coincap, coindata) = coin::create_currency(witness, 0, b"HORSE", b"Horse Tokens", b"To The Moon", option::none(), ctx); + let coins_minted = coin::mint(&mut coincap, 10, ctx); + transfer::public_freeze_object(coindata); + transfer::public_transfer(coincap, initializer); + transfer::share_object( + Vault { + id: object::new(ctx), + balance: coin::into_balance(coins_minted), + userlist: table::new(ctx), + } + ); + } + + public entry fun airdrop(vault: &mut Vault, ctx: &mut TxContext) { + let sender = tx_context::sender(ctx); + assert!(!table::contains(&vault.userlist, sender) ,1); + let mut balance_drop = balance::split(&mut vault.balance, 1); + let coin_drop = coin::take(&mut balance_drop, 1, ctx); + transfer::public_transfer(coin_drop, sender); + balance::destroy_zero(balance_drop); + table::add(&mut vault.userlist, sender, AirdroppedTo { + user: sender, + }); + } + + public entry fun get_flag(user_counter: &mut Counter, coin_drop: &mut Coin, ctx: &mut TxContext) { + + counter::increment(user_counter); + counter::is_within_limit(user_counter); + + let expected_value = coin::value(coin_drop); + assert!(expected_value == 2, 2); + + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/challenge_4/sources/counter.move b/docs/examples/move/ctf/challenge_4/sources/counter.move new file mode 100644 index 00000000000..9b20c1a1a1c --- /dev/null +++ b/docs/examples/move/ctf/challenge_4/sources/counter.move @@ -0,0 +1,45 @@ +module ctf::counter { + + const MaxCounter: u64 = 10; + const ENoAttemptLeft: u64 = 0; + + /// A shared counter. + public struct Counter has key { + id: UID, + owner: address, + value: u64 + } + + /// Create and share a Counter object. + public(package) fun create_counter(ctx: &mut TxContext) { + transfer::share_object(Counter { + id: object::new(ctx), + owner: tx_context::sender(ctx), + value: 0 + }) + } + + public fun owner(counter: &Counter): address { + counter.owner + } + + public fun value(counter: &Counter): u64 { + counter.value + } + + /// Increment a counter by 1. + public fun increment(counter: &mut Counter) { + counter.value = counter.value + 1; + } + + /// Set value (only runnable by the Counter owner) + public entry fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) { + assert!(counter.owner == tx_context::sender(ctx), 0); + counter.value = value; + } + + /// Check whether the counter has reached the limit. + public fun is_within_limit(counter: &mut Counter) { + assert!(counter.value <= MaxCounter, ENoAttemptLeft); + } +} diff --git a/docs/examples/move/ctf/challenge_5/Move.toml b/docs/examples/move/ctf/challenge_5/Move.toml new file mode 100644 index 00000000000..b933c266460 --- /dev/null +++ b/docs/examples/move/ctf/challenge_5/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "pizza" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_5/README.md b/docs/examples/move/ctf/challenge_5/README.md new file mode 100644 index 00000000000..2b85010acc4 --- /dev/null +++ b/docs/examples/move/ctf/challenge_5/README.md @@ -0,0 +1,3 @@ +# Challenge 5: Pizza! + +This contract is used in the [fifth challenge](../../../../content/developer/iota-move-ctf/challenge_5.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_5/package.json b/docs/examples/move/ctf/challenge_5/package.json new file mode 100644 index 00000000000..c5285ad251e --- /dev/null +++ b/docs/examples/move/ctf/challenge_5/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "typescript": "^5.4.3" + }, + "dependencies": { + "@mysten/bcs": "^0.11.1" + } +} diff --git a/docs/examples/move/ctf/challenge_5/sources/pizza.move b/docs/examples/move/ctf/challenge_5/sources/pizza.move new file mode 100644 index 00000000000..4389b1ff7e6 --- /dev/null +++ b/docs/examples/move/ctf/challenge_5/sources/pizza.move @@ -0,0 +1,54 @@ +module ctf::pizza { + use std::bcs; + + public struct Pizza has store { + olive_oils: u16, + yeast: u16, + flour: u16, + water: u16, + salt: u16, + tomato_sauce: u16, + cheese: u16, + pineapple: u16, + } + + public struct PizzaBox has key, store { + id: UID, + pizza: Pizza, + } + + public struct Flag has key, store { + id: UID, + user: address + } + + const EMamaMiaNonBene: u64 = 0; + + #[allow(lint(self_transfer))] + public fun cook(olive_oils: u16, yeast: u16, flour: u16, water: u16, salt: u16, tomato_sauce: u16, cheese: u16, pineapple: u16, ctx: &mut tx_context::TxContext) { + let sender = tx_context::sender(ctx); + + let p = Pizza { + olive_oils, + yeast, + flour, + water, + salt, + tomato_sauce, + cheese, + pineapple, + }; + + transfer::public_transfer(PizzaBox { id: object::new(ctx), pizza: p }, sender); + } + + #[allow(lint(self_transfer))] + public fun get_flag(pizzabox: &PizzaBox, ctx: &mut tx_context::TxContext) { + // This is where the Pizzaiolo comes in to judge... + assert!(bcs::to_bytes(&pizzabox.pizza) == x"0a000300620272011200c800b4000000", EMamaMiaNonBene); + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/challenge_6/Move.toml b/docs/examples/move/ctf/challenge_6/Move.toml new file mode 100644 index 00000000000..100c3caa15c --- /dev/null +++ b/docs/examples/move/ctf/challenge_6/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "recycle" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_6/README.md b/docs/examples/move/ctf/challenge_6/README.md new file mode 100644 index 00000000000..7e65c0cd219 --- /dev/null +++ b/docs/examples/move/ctf/challenge_6/README.md @@ -0,0 +1,3 @@ +# Challenge 6: Go Recycle! + +This contract is used in the [sixth challenge](../../../../content/developer/iota-move-ctf/challenge_6.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_6/sources/pizza.move b/docs/examples/move/ctf/challenge_6/sources/pizza.move new file mode 100644 index 00000000000..ca0aa0ba6c7 --- /dev/null +++ b/docs/examples/move/ctf/challenge_6/sources/pizza.move @@ -0,0 +1,50 @@ +module ctf::pizza { + + public struct Pizza has key, store { + id: UID, + olive_oils: u16, + yeast: u16, + flour: u16, + water: u16, + salt: u16, + tomato_sauce: u16, + cheese: u16, + pineapple: u16, + } + + public struct PizzaBox has key, store { + id: UID, + pizza: Pizza, + } + + #[allow(lint(self_transfer))] + public fun cook(olive_oils: u16, yeast: u16, flour: u16, water: u16, salt: u16, tomato_sauce: u16, cheese: u16, pineapple: u16, ctx: &mut tx_context::TxContext) { + let sender = tx_context::sender(ctx); + + let p = Pizza { + id: object::new(ctx), + olive_oils, + yeast, + flour, + water, + salt, + tomato_sauce, + cheese, + pineapple, + }; + + transfer::public_transfer(PizzaBox { id: object::new(ctx), pizza: p }, sender); + } + + public fun has_pineapple_traces(box: &PizzaBox):bool { + box.pizza.pineapple > 0 + } + + public fun recycle_box(box: PizzaBox) { + let PizzaBox {id, pizza} = box; + let Pizza {id: pid, olive_oils: _, yeast: _, flour: _, water: _, salt: _, tomato_sauce: _, cheese: _, pineapple: _} = pizza; + + object::delete(pid); + object::delete(id); + } +} diff --git a/docs/examples/move/ctf/challenge_6/sources/recycle.move b/docs/examples/move/ctf/challenge_6/sources/recycle.move new file mode 100644 index 00000000000..25a5c635ba4 --- /dev/null +++ b/docs/examples/move/ctf/challenge_6/sources/recycle.move @@ -0,0 +1,59 @@ +module ctf::recycle { + use iota::transfer::{Receiving}; + use iota::dynamic_field as df; + use ctf::pizza::{Self, PizzaBox}; + + const ENoPizzaBoxProvidedEver: u64 = 1; + const ENotEnoughRecycledYet: u64 = 2; + const ENoRecyclingNeededJustEatIt: u64 = 3; + + public struct PizzaBoxRecycler has key { + id: object::UID + } + + public struct PizzaPointBalance has store { + amount: u64 + } + + public struct Flag has key, store { + id: UID, + user: address + } + + fun init (ctx: &mut TxContext) { + transfer::share_object( + PizzaBoxRecycler { + id: object::new(ctx) + } + ); + } + + public fun accept_box(recycler: &mut PizzaBoxRecycler, disposed: Receiving, ctx: &mut TxContext) { + let sender = tx_context::sender(ctx); + let box = transfer::public_receive(&mut recycler.id, disposed); + + assert!(pizza::has_pineapple_traces(&box), ENoRecyclingNeededJustEatIt); + + if (df::exists_(&recycler.id, sender)) { + let balance: &mut PizzaPointBalance = df::borrow_mut(&mut recycler.id, sender); + balance.amount = balance.amount + 1 + } else { + df::add(&mut recycler.id, sender, PizzaPointBalance { amount: 1 }); + }; + + pizza::recycle_box(box); + } + + #[allow(lint(self_transfer))] + public fun get_flag(recycler: &mut PizzaBoxRecycler, ctx: &mut TxContext) { + let sender = tx_context::sender(ctx); + // Make sure what we are withdrawing exists + assert!(df::exists_(&recycler.id, sender), ENoPizzaBoxProvidedEver); + let balance: &mut PizzaPointBalance = df::borrow_mut(&mut recycler.id, sender); + assert!(balance.amount > 2, ENotEnoughRecycledYet); + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/challenge_7/Move.toml b/docs/examples/move/ctf/challenge_7/Move.toml new file mode 100644 index 00000000000..85662b8f0b1 --- /dev/null +++ b/docs/examples/move/ctf/challenge_7/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "ptb" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_7/README.md b/docs/examples/move/ctf/challenge_7/README.md new file mode 100644 index 00000000000..809a932a650 --- /dev/null +++ b/docs/examples/move/ctf/challenge_7/README.md @@ -0,0 +1,3 @@ +# Challenge 7: PTBs + +This contract is used in the [seventh challenge](../../../../content/developer/iota-move-ctf/challenge_7.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_7/package.json b/docs/examples/move/ctf/challenge_7/package.json new file mode 100644 index 00000000000..73e4e60ded2 --- /dev/null +++ b/docs/examples/move/ctf/challenge_7/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "typescript": "^5.4.3" + }, + "devDependencies": { + "@mysten/sui.js": "^0.51.2" + } +} diff --git a/docs/examples/move/ctf/challenge_7/sources/ptb.move b/docs/examples/move/ctf/challenge_7/sources/ptb.move new file mode 100644 index 00000000000..33de9196dc7 --- /dev/null +++ b/docs/examples/move/ctf/challenge_7/sources/ptb.move @@ -0,0 +1,29 @@ +module ctf::ptb { + + public struct Flour has copy, drop {} + public struct Water has copy, drop {} + public struct Yeast has copy, drop {} + public struct Salt has copy, drop {} + public struct Dough has copy, drop {} + + public struct Flag has key, store { + id: UID, + user: address + } + + public fun get_ingredients(): (Flour, Water, Yeast, Salt) { + (Flour{}, Water{}, Yeast{}, Salt{}) + } + + public fun make_dough(_: Flour, _: Water, _: Yeast, _: Salt): Dough { + Dough{} + } + + #[allow(lint(self_transfer))] + public fun get_flag(_: Dough, ctx: &mut TxContext) { + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/challenge_8/Move.toml b/docs/examples/move/ctf/challenge_8/Move.toml new file mode 100644 index 00000000000..8fd521d7ec5 --- /dev/null +++ b/docs/examples/move/ctf/challenge_8/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "swap" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/challenge_8/README.md b/docs/examples/move/ctf/challenge_8/README.md new file mode 100644 index 00000000000..84fa57b3bee --- /dev/null +++ b/docs/examples/move/ctf/challenge_8/README.md @@ -0,0 +1,3 @@ +# Challenge 8: Flash! + +This contract is used in the [eighth challenge](../../../../content/developer/iota-move-ctf/challenge_8.mdx) of the IOTA Move CTF. diff --git a/docs/examples/move/ctf/challenge_8/sources/firstcoin.move b/docs/examples/move/ctf/challenge_8/sources/firstcoin.move new file mode 100644 index 00000000000..b00fddec985 --- /dev/null +++ b/docs/examples/move/ctf/challenge_8/sources/firstcoin.move @@ -0,0 +1,34 @@ +module ctf::ctfa { + use iota::coin::{Self, Coin, TreasuryCap}; + + public struct CTFA has drop {} + + public struct MintA has key, store { + id: UID, + cap: TreasuryCap + } + + fun init(witness: CTFA, ctx: &mut TxContext) { + // Get a treasury cap for the coin and give it to the transaction sender + let (treasury_cap, metadata) = coin::create_currency(witness, 1, b"CTFA", b"CTF A Coin", b"Token for the CTF", option::none(), ctx); + let mint = MintA { + id: object::new(ctx), + cap:treasury_cap + }; + transfer::share_object(mint); + transfer::public_freeze_object(metadata); + } + + public(package) fun mint_for_vault(mut mint: MintA, ctx: &mut TxContext): Coin { + let coina = coin::mint(&mut mint.cap, 100, ctx); + coin::mint_and_transfer(&mut mint.cap, 10, tx_context::sender(ctx), ctx); + let MintA { + id: ida, + cap: capa + } = mint; + object::delete(ida); + transfer::public_freeze_object(capa); + coina + } + +} diff --git a/docs/examples/move/ctf/challenge_8/sources/secondcoin.move b/docs/examples/move/ctf/challenge_8/sources/secondcoin.move new file mode 100644 index 00000000000..52434344fde --- /dev/null +++ b/docs/examples/move/ctf/challenge_8/sources/secondcoin.move @@ -0,0 +1,34 @@ +module ctf::ctfb { + use iota::coin::{Self, Coin, TreasuryCap}; + + public struct CTFB has drop {} + + public struct MintB has key, store { + id: UID, + cap: TreasuryCap + } + + fun init(witness: CTFB, ctx: &mut TxContext) { + // Get a treasury cap for the coin and give it to the transaction sender + let (treasury_cap, metadata) = coin::create_currency(witness, 1, b"CTFB", b"CTF B Coin", b"Token for the CTF", option::none(), ctx); + let mint = MintB { + id: object::new(ctx), + cap:treasury_cap + }; + transfer::share_object(mint); + transfer::public_freeze_object(metadata); + } + + public(package) fun mint_for_vault(mut mint: MintB, ctx: &mut TxContext): Coin { + let coinb = coin::mint(&mut mint.cap, 100, ctx); + coin::mint_and_transfer(&mut mint.cap, 10, tx_context::sender(ctx), ctx); + let MintB { + id: ida, + cap: capa + } = mint; + object::delete(ida); + transfer::public_freeze_object(capa); + coinb + } + +} diff --git a/docs/examples/move/ctf/challenge_8/sources/vault.move b/docs/examples/move/ctf/challenge_8/sources/vault.move new file mode 100644 index 00000000000..392bb0b73e9 --- /dev/null +++ b/docs/examples/move/ctf/challenge_8/sources/vault.move @@ -0,0 +1,93 @@ +module ctf::vault{ + use iota::coin::{Self, Coin}; + use iota::balance::{Self, Balance}; + use ctf::ctfa::{Self, MintA}; + use ctf::ctfb::{Self, MintB}; + + public struct Vault has key { + id: UID, + coin_a: Balance, + coin_b: Balance, + flashed: bool + } + + public struct Flag has key, store { + id: UID, + user: address + } + + public struct Receipt { + id: ID, + a_to_b: bool, + repay_amount: u64 + } + + public entry fun initialize(capa: MintA, capb: MintB,ctx: &mut TxContext) { + let vault = Vault { + id: object::new(ctx), + coin_a: coin::into_balance(ctfa::mint_for_vault(capa, ctx)), + coin_b: coin::into_balance(ctfb::mint_for_vault(capb, ctx)), + flashed: false + }; + transfer::share_object(vault); + } + + public fun flash(vault: &mut Vault, amount: u64, a_to_b: bool, ctx: &mut TxContext): (Coin, Coin, Receipt) { + assert!(!vault.flashed, 1); + let (coin_a, coin_b) = if (a_to_b) { + (coin::zero(ctx), coin::from_balance(balance::split(&mut vault.coin_b, amount ), ctx)) + } + else { + (coin::from_balance(balance::split(&mut vault.coin_a, amount ), ctx), coin::zero(ctx)) + }; + + let receipt = Receipt { + id: object::id(vault), + a_to_b, + repay_amount: amount + }; + vault.flashed = true; + + (coin_a, coin_b, receipt) + + } + + public fun repay_flash(vault: &mut Vault, coina: Coin, coinb: Coin, receipt: Receipt) { + let Receipt { + id: _, + a_to_b: a2b, + repay_amount: amount + } = receipt; + if (a2b) { + assert!(coin::value(&coinb) >= amount, 0); + } else { + assert!(coin::value(&coina) >= amount, 1); + }; + balance::join(&mut vault.coin_a, coin::into_balance(coina)); + balance::join(&mut vault.coin_b, coin::into_balance(coinb)); + vault.flashed = false; + } + + public fun swap_a_to_b(vault: &mut Vault, coina:Coin, ctx: &mut TxContext): Coin { + let amount_out_B = coin::value(&coina) * balance::value(&vault.coin_b) / balance::value(&vault.coin_a); + coin::put(&mut vault.coin_a, coina); + coin::take(&mut vault.coin_b, amount_out_B, ctx) + } + + public fun swap_b_to_a(vault: &mut Vault, coinb:Coin, ctx: &mut TxContext): Coin { + let amount_out_A = coin::value(&coinb) * balance::value(&vault.coin_a) / balance::value(&vault.coin_b); + coin::put(&mut vault.coin_b, coinb); + coin::take(&mut vault.coin_a, amount_out_A, ctx) + } + + #[allow(lint(self_transfer))] + public fun get_flag(vault: &Vault, ctx: &mut TxContext) { + assert!( + balance::value(&vault.coin_a) == 0 && balance::value(&vault.coin_b) == 0, 123 + ); + transfer::public_transfer(Flag { + id: object::new(ctx), + user: tx_context::sender(ctx) + }, tx_context::sender(ctx)); + } +} diff --git a/docs/examples/move/ctf/deploy.py b/docs/examples/move/ctf/deploy.py new file mode 100644 index 00000000000..33cebb9835f --- /dev/null +++ b/docs/examples/move/ctf/deploy.py @@ -0,0 +1,110 @@ +import os +import glob +import subprocess +import sys +import json +import time +from pprint import pp + +def execute_in_directories(executable_path, base_directory): + # Check if the executable exists + base_directory = os.path.abspath(base_directory) + + if not os.path.isfile(executable_path): + print(f"Executable not found: {executable_path}") + sys.exit(1) + + to_whitelist = [] + package = None + cap = None + challenges = None + + # Iterate over all directories in the base_directory + for folder in sorted(glob.glob(os.path.join(base_directory, '*'))): + if os.path.isdir(folder) and 'challenge_' in folder: + print(folder.rsplit('/', 1)[1]) + try: + # Change to the current directory + os.chdir(folder) + # Execute the command + data = subprocess.run([executable_path, 'client', 'publish', '--json', '--gas-budget=100000000', '--skip-dependency-verification'], check=False, capture_output=True, text=True) + #print(data.stdout) + data.check_returncode() + deployed = json.loads(data.stdout) + for oc in deployed['objectChanges']: + if oc['type'] == 'published': + pid = oc['packageId'] + modules = oc['modules'] + print(f"Package: {pid}") + + for module in modules: + to_whitelist.append('%s::%s::Flag' % (pid, module)) + + elif oc['type'] == 'created': + otype = oc['objectType'].split('::', 2)[2] + oid = oc['objectId'] + print(f"{otype}: {oid}") + + except subprocess.CalledProcessError as e: + print(f"Error executing in {folder}: {e}") + except Exception as e: + print(f"An unexpected error occurred in {folder}: {e}") + finally: + # Change back to the base directory + os.chdir(base_directory) + time.sleep(5) + + elif os.path.isdir(folder) and 'verifier' in folder: + try: + # Change to the current directory + os.chdir(folder) + # Execute the command + data = subprocess.run([executable_path, 'client', 'publish', '--json', '--gas-budget=100000000', '--skip-dependency-verification'], check=True, capture_output=True, text=True) + deployed = json.loads(data.stdout) + for oc in deployed['objectChanges']: + if oc['type'] == 'published': + package = oc['packageId'] + elif oc['type'] == 'created': + if oc['objectType'].endswith('CTFCap'): + cap = oc['objectId'] + if oc['objectType'].endswith('Challenges'): + challenges = oc['objectId'] + + except subprocess.CalledProcessError as e: + print(f"Error executing in {folder}: {e}") + except Exception as e: + print(f"An unexpected error occurred in {folder}: {e}") + finally: + # Change back to the base directory + os.chdir(base_directory) + time.sleep(5) + + print("Done") + + + print("Adding allowed flags to the verifier contract, false positives will fail") + for module in to_whitelist: + try: + # iota client call --function allow_flag_type --module verifier --package $PACKAGE --args $CAP $SB --type-args 0xe6d41a7f9530acb58fae2318f33eb27f80f84406268a23043ab9289d318f3ddc::checkin::Flag --gas-budget 100000000 + subprocess.run([executable_path, 'client', 'call', '--function', 'allow_flag_type', '--module', 'verifier', '--package', package, '--args', cap, challenges, '--type-args', module, '--gas-budget', '100000000'], check=True, capture_output=True, text=True) + except Exception as e: + print(f"Failed to allow flag `{module}`, it probably does not exist (false positive)") + + print('All Done!\n') + print('-' * 80) + print('Package ID for Verifier: %s' % package) + print('CTFCap Object ID: %s' % cap) + print('Shared Challenges Object ID: %s' % challenges) + print('-' * 80) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python deploy.py ") + sys.exit(1) + + executable_path = sys.argv[1] + base_directory = sys.argv[2] + + execute_in_directories(executable_path, base_directory) + diff --git a/docs/examples/move/ctf/verifier/Move.toml b/docs/examples/move/ctf/verifier/Move.toml new file mode 100644 index 00000000000..bbd6fa578d1 --- /dev/null +++ b/docs/examples/move/ctf/verifier/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "verifier" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "v0.2.0" } + +[addresses] +ctf = "0x0" +iota = "0000000000000000000000000000000000000000000000000000000000000002" diff --git a/docs/examples/move/ctf/verifier/README.md b/docs/examples/move/ctf/verifier/README.md new file mode 100644 index 00000000000..cf6818488e6 --- /dev/null +++ b/docs/examples/move/ctf/verifier/README.md @@ -0,0 +1,3 @@ +# CTF Verifier, track participants! + +This solver accepts flags and counts the amount of valid flags per participant, can be used to figure out who the winner is diff --git a/docs/examples/move/ctf/verifier/sources/verifier.move b/docs/examples/move/ctf/verifier/sources/verifier.move new file mode 100644 index 00000000000..a40908766ce --- /dev/null +++ b/docs/examples/move/ctf/verifier/sources/verifier.move @@ -0,0 +1,134 @@ +module ctf::verifier { + + use std::ascii::{String}; + use std::type_name::{get, into_string}; + use iota::table::{Self, Table}; + use iota::clock::{Self, Clock}; + use iota::address; + use iota::event; + + public struct CapturedFlag has store { + flag_type: String, + capture_time: u64 + } + + public struct Score has store { + captured_flags: u64, + latest_capture: u64 + } + + public struct ScoreBoard has copy, drop { + scores: vector + } + + public struct ScoreBoardElement has copy, drop { + sender: address, + captured_flags: u64, + latest_capture: u64 + } + + public struct Challenges has key { + id: UID, + /// The type name of the flags that are allowed to be submitted + allowed_flags: vector, + captured_flags: Table>, + participants: vector
, + scores: Table + } + + public struct CTFCap has key, store { + id: UID + } + + const EFlagNotAllowed: u64 = 0; + const EFlagAlreadySubmitted: u64 = 1; + + fun init (ctx: &mut TxContext) { + transfer::public_transfer( + CTFCap { + id: object::new(ctx) + }, tx_context::sender(ctx) + ); + + transfer::share_object( + Challenges { + id: object::new(ctx), + allowed_flags: vector::empty(), + captured_flags: table::new>(ctx), + participants: vector::empty
(), + scores: table::new(ctx) + } + ); + } + + /// Admin functionality to enable a new flag type to be submitted + public fun allow_flag_type(_cap: &CTFCap, challenges: &mut Challenges) { + let flag_type_string = get().into_string(); + challenges.allowed_flags.push_back(flag_type_string); + } + + /// Capture function to call after sending the flag here! + public fun capture(challenges: &mut Challenges, captured_flag: T, clock: &Clock, ctx: &mut TxContext) { + let sender = tx_context::sender(ctx); + + let tname = get().into_string(); + + // Check if this flag is allowed to be submitted + assert!(vector::contains(&challenges.allowed_flags, &tname), EFlagNotAllowed); + + let cflag = CapturedFlag { flag_type: tname, capture_time: clock::timestamp_ms(clock)}; + + // Add the empty vector for non-existing flags + if(!table::contains(&challenges.captured_flags, sender)) { + table::add(&mut challenges.captured_flags, sender, vector::empty()); + }; + + // Initialize a scoreboard object for this sender if not there yet + if(!table::contains(&challenges.scores, sender)) { + table::add(&mut challenges.scores, sender, Score { captured_flags: 0, latest_capture: 0}); + challenges.participants.push_back(sender); + }; + + // Check if flag was already captured + let mut i = 0; + let submitted = table::borrow(&challenges.captured_flags, sender); + + while(i < submitted.length()) { + let tmp_cflag = &submitted[i]; + assert!(tmp_cflag.flag_type != tname, EFlagAlreadySubmitted); + i = i + 1; + }; + + let captured_flags = table::borrow_mut(&mut challenges.captured_flags, sender); + captured_flags.push_back(cflag); + + let scoreboard = table::borrow_mut(&mut challenges.scores, sender); + scoreboard.captured_flags = scoreboard.captured_flags + 1; + scoreboard.latest_capture = clock::timestamp_ms(clock); + + // Burn the flag! + transfer::public_transfer(captured_flag, address::from_u256(1337)); + } + + /// Return the global scoreboard + public fun scoreboard(challenges: &Challenges): vector { + let mut i = 0; + let mut score_vector = vector::empty(); + + while(i < challenges.participants.length()) { + let addr = challenges.participants[i]; + let score = &challenges.scores[addr]; + score_vector.push_back(ScoreBoardElement { sender: addr, captured_flags: score.captured_flags, latest_capture: score.latest_capture }); + i = i + 1; + }; + + event::emit(ScoreBoard { scores: score_vector }); + score_vector + } + + /// Return the captures for a given address + public fun captured(challenges: &Challenges, addr: address): &vector { + table::borrow(&challenges.captured_flags, addr) + } + +} diff --git a/docs/site/src/components/CTF/ctf-verifier.tsx b/docs/site/src/components/CTF/ctf-verifier.tsx new file mode 100644 index 00000000000..cc25c579746 --- /dev/null +++ b/docs/site/src/components/CTF/ctf-verifier.tsx @@ -0,0 +1,94 @@ +import React, { useState, useMemo } from 'react'; +import { + IotaClientProvider, + WalletProvider, +} from '@iota/dapp-kit'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { getFullnodeUrl, IotaClient } from '@iota/iota-sdk/client'; + +// Define props interface +interface ChallengeVerifierProps { + expectedObjectType: string; // Prop for the expected Object Type +} + +// Define network configurations +const NETWORKS = { + devnet: { url: getFullnodeUrl('devnet') }, + alphanet: { url: 'https://api.iota-rebased-alphanet.iota.cafe:443' }, +}; + +// Main ChallengeVerifier component +const ChallengeVerifier: React.FC = ({ expectedObjectType }) => { + const [inputText, setInputText] = useState(''); + const [coins, setCoins] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async () => { + setLoading(true); + setError(''); + setCoins(null); + + try { + const client = new IotaClient({ url: NETWORKS.alphanet.url }); + const result = await client.getObject({ id: inputText, options: { showType: true } }); + + const message = result.data.type === expectedObjectType + ? 'Congratulations! You have successfully completed this level!' + : 'Invalid Flag Object Id. Please try again.'; + + setCoins(message); + } catch (err: any) { + setError(err.message || 'An error occurred. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( +
+ setInputText(e.target.value)} + placeholder="Enter Flag Object Id" + /> + + + {error &&

{error}

} + + {coins && ( +
+

Result

+
{JSON.stringify(coins, null, 2)}
+
+ )} +
+ ); +}; + +// Higher-order function to provide necessary context +const withProviders = (Component: React.FC) => { + return ({ expectedObjectType }: ChallengeVerifierProps) => { + if (typeof window === 'undefined') { + return null; + } + + const queryClient = useMemo(() => new QueryClient(), []); + + return ( + + + + + + + + ); + }; +}; + +// Export the component wrapped in providers +export default withProviders(ChallengeVerifier);