diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 60af44d0..0820624b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,12 @@ ## Summary + What does this PR implement? ## Testing + - [ ] Unit tests added - [ ] Tests passing locally ## Notes -Any assumptions or trade-offs? \ No newline at end of file + +Any assumptions or trade-offs? diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3aed9df2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + formatting: + name: Code Formatting (Prettier) + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Run Prettier check + run: npx prettier --check . diff --git a/.gitignore b/.gitignore index ab1b93b7..04eea848 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,37 @@ package-lock.json node_modules +# Dependencies +lib/ + # Rust build artifacts /target **/target + +# Hardhat / Foundry build artifacts +artifacts/ +cache/ +typechain-types/ +out/ +build-info/ + +# Solidity compiler binary +soljson-latest.js + +# Hardhat Ignition build info (large generated files) +ignition/deployments/**/build-info/ + +# Logs +*.log + +# OS files +.DS_Store +Thumbs.db + +# IDE +.idea/ +*.swp +*.swo + + +lib/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..feb451eb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "assignments/foundry-test-assignment/lib/openzeppelin-contracts"] + path = assignments/foundry-test-assignment/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.prettierrc b/.prettierrc index d4fed1f5..eba5a682 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,4 +4,4 @@ "trailingComma": "es5", "printWidth": 80, "tabWidth": 2 -} \ No newline at end of file +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cbd55a65..0309b840 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,19 @@ # How to Contribute 1. Getting Started - - Fork the repository - - Sync with upstream - - Create a feature branch + +- Fork the repository +- Sync with upstream +- Create a feature branch 2. Branch Naming Convention -Example: -`submission/assignment-01/` + Example: + `submission/assignment-01/` 3. Commit Message Guidelines Example (Conventional Commits–lite): + ``` feat: implement token transfer logic fix: handle zero-address edge case @@ -19,17 +21,20 @@ test: add coverage for revert conditions ``` 4. Pull Request Checklist + - [x] Code compiles - [x] Tests pass - [x] README included - [x] No changes outside /submissions 5. PR Review Process + - [x] Automated checks - [x] Manual review by mentors - [x] Required fixes before merge 6. Common Mistakes to Avoid + - Editing assignment specs - Submitting compiled artifacts -- Force-pushing after review \ No newline at end of file +- Force-pushing after review diff --git a/README.md b/README.md index 1f9b46e7..de87369e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # Cohort 8 + Blockchain Development Bootcamp — Cohort 8 This repository contains official technical materials, specifications, assignments, and collaboration artifacts for the 8th edition of our blockchain program. Dev participants are expected to actively use this repository throughout the program for learning, development, version control, and peer collaboration. #### Repository Structure + The repository will evolve over time, but a typical structure looks like: + ```cohort-8/ -├── README.md # Repository overview and instructions +├── README.md # Repository overview and instructions ├── assignments/ # Coding exercises and assessments ├── projects/ │ ├── templates/ # Starter project templates @@ -15,12 +18,14 @@ The repository will evolve over time, but a typical structure looks like: ``` #### Setup + Clone the repository: + ``` git clone https://github.com/BlockheaderWeb3-Community/cohort-8.git cd cohort-8 ``` #### Code Formatting -This repository uses Prettier for formatting. Install the Prettier extension in your editor and enable Format on Save. +This repository uses Prettier for formatting. Install the Prettier extension in your editor and enable Format on Save. diff --git a/assignment05/README.md b/assignment05/README.md new file mode 100644 index 00000000..2ec24333 --- /dev/null +++ b/assignment05/README.md @@ -0,0 +1 @@ +https://hackmd.io/@gXPhwY6gQKmHFQ55mXXm1g/S1b3oc_LWg \ No newline at end of file diff --git a/assignment05/package.json b/assignment05/package.json new file mode 100644 index 00000000..edbc7bcb --- /dev/null +++ b/assignment05/package.json @@ -0,0 +1,21 @@ +{ + "dependencies": { + "bip32": "^5.0.0", + "bip39": "^3.1.0", + "ethereumjs-util": "^7.1.5", + "hdkey": "^2.1.0", + "keccak": "^3.0.4", + "secp256k1": "^5.0.1", + "tiny-secp256k1": "^2.2.4" + }, + "name": "key-key", + "version": "1.0.0", + "main": "key.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/assignment05/walletad.js b/assignment05/walletad.js new file mode 100644 index 00000000..d313e12e --- /dev/null +++ b/assignment05/walletad.js @@ -0,0 +1,41 @@ +const bip39 = require("bip39"); +const { BIP32Factory } = require("bip32"); +const tinysecp = require("tiny-secp256k1"); +const bip32 = BIP32Factory(tinysecp); +const secp256k1 = require("secp256k1"); +const { keccak256 } = require("ethereumjs-util"); +const crypto = require("crypto"); + +const entropy = crypto.randomBytes(32); + + +const mnemonic = bip39.entropyToMnemonic(entropy); +console.log("Mnemonic Phrase:\n", mnemonic); + +const seed = bip39.mnemonicToSeedSync(mnemonic); + +const root = bip32.fromSeed(seed); + +const path = "m/44'/60'/0'/0/0"; +const child = root.derivePath(path);const ADDRESS_COUNT = 4; + +for (let i = 0; i < ADDRESS_COUNT; i++) { + const path = `m/44'/60'/0'/0/${i}`; + const child = root.derivePath(path); + + const privateKey = child.privateKey; + // console.log("\nPrivate Key:\n", Buffer.from(privateKey).toString("hex")); + + const publicKeyArray = secp256k1.publicKeyCreate(privateKey, false).slice(1); + const publicKey = Buffer.from(publicKeyArray); + // console.log("\nPublic Key (64 bytes):\n", publicKey.toString("hex")); + + const hash = keccak256(publicKey); + + const address = "0x" + hash.slice(-20).toString("hex"); + // console.log("\nEthereum Address:\n", address); + console.log(`\nAddress #${i}`); + console.log("\nPrivate Key:\n", Buffer.from(privateKey).toString("hex")); + console.log("\nPublic Key (64 bytes):\n", publicKey.toString("hex")); + console.log("\nEthereum Address:\n", address); +}; \ No newline at end of file diff --git a/assignments/1/1-gas/README.md b/assignments/1/1-gas/README.md index 85d6be90..bf1bc78c 100644 --- a/assignments/1/1-gas/README.md +++ b/assignments/1/1-gas/README.md @@ -10,6 +10,7 @@ This is a **conceptual example**, not an exact implementation of Ethereum’s ga ## What This Program Does The program: + - Defines a unit of computational work - Defines a price per unit of work - Multiplies the two values to compute a gas fee @@ -24,10 +25,10 @@ Gas Fee = Computational Work × Price Per Unit -### Gas +### Gas Gas is a unit of measurement. It measures how much computation a transaction uses -### Gas Limit +### Gas Limit Gas limit is the maximum amount of gas you are willing to use for a transaction. ### Gas price @@ -40,3 +41,4 @@ A base fee is calculated automatically by the network It changes depending on network congestion This base fee is burned (destroyed forever) **Burned ETH is removed from circulation → makes ETH deflationary** +``` diff --git a/assignments/1/1-merkle-root/README.md b/assignments/1/1-merkle-root/README.md index c6039865..4eefbd03 100644 --- a/assignments/1/1-merkle-root/README.md +++ b/assignments/1/1-merkle-root/README.md @@ -3,6 +3,7 @@ This project demonstrates how a **Merkle Tree** works by hashing a list of transactions and recursively combining them to produce a **Merkle root**. The implementation is written in Rust and shows: + - How transactions are hashed - How Merkle tree levels are built - How the final Merkle root is computed @@ -35,3 +36,4 @@ Pairwise hash concatenation Repeated hashing of levels ↓ Merkle Root +``` diff --git a/assignments/3-block-observation/README.md b/assignments/3-block-observation/README.md index d3098ee5..33345220 100644 --- a/assignments/3-block-observation/README.md +++ b/assignments/3-block-observation/README.md @@ -1,8 +1,9 @@ # Observations on a Block explorer(Etherscan) and a Transaction hash - ### 1. Exploring the block explorer + While on the block explorer, I observed some standard metrics which were: + - Ether price: This gives the current market value of one `ether` - Market cap: Short for market capitalisation, it is calculated by multiplying the current value of ether by the total circulating supply. - Transactions: This shows the total number of transactions processed in ethereum over a period of time. @@ -11,7 +12,9 @@ While on the block explorer, I observed some standard metrics which were: - Latest transaction: This shows all the recent transaction that were mined in a block. ### 2. Exploring a block on etherscan (Block explorer) + After clicking on a recent block under the `Latest blocks` section, there were some details I observed which were: + - Block Height: It shows the number in which the block is positioned, right from the genesis block. It shows the length of the blockchain. - Status: It shows if a block is fiinalised/confirmed - Timestamp: It shows the date and time at which the block was created. @@ -21,17 +24,20 @@ After clicking on a recent block under the `Latest blocks` section, there were s - Block difficulty: This is the total difficulty of the chain up until the recent recent block. ### 3. Exploring a transaction hash + + - **Sending ETH to my address** -On my rabby wallet, I clicked on `Send` button which prompted me to input an amount and put in the receivers' address, since I'm sending to myself, I inputted my wallet address. After that I clicked `Send` then I signed and confirmed the transaction. Immediately I was redirected to `etherscan.io`, there I observed some details like: + On my rabby wallet, I clicked on `Send` button which prompted me to input an amount and put in the receivers' address, since I'm sending to myself, I inputted my wallet address. After that I clicked `Send` then I signed and confirmed the transaction. Immediately I was redirected to `etherscan.io`, there I observed some details like: - Transaction hash: It is unique identifier that is generated cryptographically when a transaction is created, it's more like a proof that a transaction a has gone through. - Status: This shows if a transaction was successful or not - Block: This shows the block height which this transaction is included. - Timestamp: The date a time in which this transaction was created. -- From: Senders' address +- From: Senders' address - To: Receivers' address - Value: The value of ETH I'm spending ## Conclusion + The ethereum block explorer is a powerful tool that shows in real time the processes and production of blocks and transactions. -As well as a transaction hash that serves as a proof that an exchange of value has already taken place. \ No newline at end of file +As well as a transaction hash that serves as a proof that an exchange of value has already taken place. diff --git a/assignments/3-private-key/generate_address.js b/assignments/3-private-key/generate_address.js new file mode 100644 index 00000000..345aa69f --- /dev/null +++ b/assignments/3-private-key/generate_address.js @@ -0,0 +1,210 @@ +import * as bip39 from 'bip39'; +import * as bip32 from 'bip32'; +import * as ecc from 'tiny-secp256k1'; +import crypto from 'crypto'; +import keccak256 from 'js-sha3'; + +function generateEntropy(entropyBits = 128) { + // entropyBits can be 128, 160, 192, 224, or 256 + const entropyBytes = entropyBits / 8; + const entropy = crypto.randomBytes(entropyBytes).toString('hex'); + console.log('\n=== STEP 1: Generate Entropy ==='); + console.log(`Entropy (${entropyBits} bits):\n${entropy}`); + return entropy; +} + +function generateMnemonic(entropy) { + console.log('\n=== STEP 2: BIP 39 - Generate Mnemonic ==='); + + // Convert entropy to mnemonic (wordlist is used internally) + const mnemonic = bip39.entropyToMnemonic(entropy); + console.log(`Mnemonic (${mnemonic.split(' ').length} words):\n${mnemonic}`); + + // Verify the mnemonic is valid + const isValid = bip39.validateMnemonic(mnemonic); + console.log(`Mnemonic valid: ${isValid}`); + + return mnemonic; +} + +function generateSeed(mnemonic, passphrase = '') { + console.log('\n=== STEP 3: BIP 39 - Generate Seed ==='); + + // mnemonicToSeed uses PBKDF2 with SHA-512 + // Parameters: PBKDF2(password="mnemonic" + passphrase, salt="mnemonic" + passphrase, iterations=2048, hash=SHA512) + const seed = bip39.mnemonicToSeedSync(mnemonic, passphrase); + console.log(`Seed (512 bits / 64 bytes):\n${seed.toString('hex')}`); + console.log(`Passphrase used: "${passphrase || '(empty)'}"`); + + return seed; +} + +function createMasterKey(seed) { + console.log('\n=== STEP 4: BIP 32 - Create Master Key ==='); + + // Create master BIP32 node (for Bitcoin/Ethereum, uses "Bitcoin seed" as HMAC key) + const masterNode = bip32.BIP32Factory(ecc).fromSeed(seed); + + console.log(`Master Private Key:\n${masterNode.privateKey.toString('hex')}`); + console.log(`Master Public Key:\n${masterNode.publicKey.toString('hex')}`); + console.log(`Master Chain Code:\n${masterNode.chainCode.toString('hex')}`); + console.log(`Master Extended Private Key (xprv):\n${masterNode.toBase58()}`); + + return masterNode; +} + +/** + * Where: + * - purpose' = 44' (BIP 44) + * - coin_type' = 0' (Bitcoin), 1' (Bitcoin Testnet), 60' (Ethereum), etc. + * - account' = 0' (first account) + * - change = 0 (receiving addresses), 1 (change addresses) + * - address_index = 0, 1, 2, ... (individual address index) + */ +function deriveBIP44Path( + masterNode, + coinType = 0, + account = 0, + change = 0, + addressIndex = 0 +) { + console.log('\n=== STEP 5: BIP 44 - Derive Keys ==='); + + // BIP 44 path: m/44'/coin_type'/account'/change/address_index + const purpose = 44; // BIP 44 + + // Common coin types: + // 0' = Bitcoin, 1' = Bitcoin Testnet, 60' = Ethereum, 111' = Testnet (for various) + + console.log( + `\nBIP 44 Derivation Path: m/44'/${coinType}'/0'/0/${addressIndex}` + ); + console.log(`- purpose: ${purpose}' (BIP 44)`); + console.log(`- coin_type: ${coinType}' (0=Bitcoin, 60=Ethereum)`); + console.log(`- account: ${account}'`); + console.log(`- change: ${change} (0=receiving, 1=change)`); + console.log(`- address_index: ${addressIndex}`); + + let node = masterNode + .deriveHardened(purpose) // m/44' + .deriveHardened(coinType) // m/44'/coin_type' + .deriveHardened(account) // m/44'/coin_type'/account' + .derive(change) // m/44'/coin_type'/account'/change + .derive(addressIndex); // m/44'/coin_type'/account'/change/address_index + + console.log(`\nDerived Private Key:\n${node.privateKey.toString('hex')}`); + console.log(`Derived Public Key:\n${node.publicKey.toString('hex')}`); + + return node; +} + +function generateAddress(publicKey, coinType = 0) { + console.log('\n=== STEP 6: Generate Address ==='); + + if (coinType === 0) { + // Bitcoin address (P2PKH Legacy format) + return generateBitcoinAddress(publicKey); + } else if (coinType === 60) { + // Ethereum address + return generateEthereumAddress(publicKey); + } +} + +function generateBitcoinAddress(publicKey) { + const sha256 = crypto.createHash('sha256').update(publicKey).digest(); + + // For RIPEMD-160, we need crypto.createHash('ripemd160') + const ripemd160 = crypto.createHash('ripemd160').update(sha256).digest(); + + // Add version byte for Bitcoin mainnet + const withVersion = Buffer.concat([Buffer.from([0x00]), ripemd160]); + + // Calculate checksum (first 4 bytes of double SHA256) + const checksum = crypto + .createHash('sha256') + .update(crypto.createHash('sha256').update(withVersion).digest()) + .digest() + .slice(0, 4); + + // Combine and encode in Base58 + const address = base58Encode(Buffer.concat([withVersion, checksum])); + + console.log(`Bitcoin Address (P2PKH):\n${address}`); + return address; +} + +function generateEthereumAddress(publicKey) { + const publicKeyHex = publicKey.toString('hex'); + const keccak256Hash = keccak256.keccak256; + const hash = keccak256Hash.update(Buffer.from(publicKeyHex, 'hex')).digest(); + + // Take last 20 bytes and format as address + const address = '0x' + Buffer.from(hash).slice(-20).toString('hex'); + + console.log(`Ethereum Address:\n${address}`); + return address; +} + +function base58Encode(buffer) { + const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + + let num = 0n; + for (const byte of buffer) { + num = num * 256n + BigInt(byte); + } + + let encoded = ''; + while (num > 0n) { + encoded = ALPHABET[Number(num % 58n)] + encoded; + num = num / 58n; + } + + // Handle leading zeros + for (const byte of buffer) { + if (byte === 0) encoded = '1' + encoded; + else break; + } + + return encoded || '1'; +} + +function deriveAddressFromEntropy(entropy = null, coinType = 0) { + console.log('╔════════════════════════════════════════════════════════╗'); + console.log('║ BIP 39, 32, 44 - Address Derivation Workflow ║'); + console.log('╚════════════════════════════════════════════════════════╝'); + + try { + const finalEntropy = entropy || generateEntropy(128); + + const mnemonic = generateMnemonic(finalEntropy); + + const seed = generateSeed(mnemonic, ''); + + const masterNode = createMasterKey(seed); + + const derivedNode = deriveBIP44Path(masterNode, coinType, 0, 0, 0); + + const address = generateAddress(derivedNode.publicKey, coinType); + + console.log('\n╔════════════════════════════════════════════════════════╗'); + console.log('║ Summary ║'); + console.log('╚════════════════════════════════════════════════════════╝'); + console.log(`Entropy: ${finalEntropy}`); + console.log(`Mnemonic: ${mnemonic}`); + console.log(`Address: ${address}`); + + return { + entropy: finalEntropy, + mnemonic: mnemonic, + seed: seed.toString('hex'), + masterNode: masterNode, + derivedNode: derivedNode, + address: address, + }; + } catch (error) { + console.error('Error during derivation:', error.message); + throw error; + } +} + +deriveAddressFromEntropy(null, 60); diff --git a/assignments/4-terminologies/README.md b/assignments/4-terminologies/README.md index 53863914..38add77b 100644 --- a/assignments/4-terminologies/README.md +++ b/assignments/4-terminologies/README.md @@ -1,6 +1,7 @@ ## Exec hash, finalized root, epoch and block hash, slots, fork choice -### Block hash +### Block hash + A block hash is a short fingerprint of a block. It’s created by hashing the block’s contents (transactions, metadata, etc.) Looks like random hex: 0x9f3a... @@ -9,21 +10,24 @@ If anything in the block changes → the hash changes This makes blocks tamper-evident ### Execution hash (execution payload hash) + Ethereum has two layers now: Execution layer → transactions, balances, smart contracts Consensus layer → validators, slots, finality Execution hash is: A hash that represents the result of executing transactions in a block It commits to: + - Account balances - Contract storage - Gas usage - Logs, etc. -**Why it exists**: -Consensus layer needs to know “this is the exact execution result” -Prevents lying about transaction outcomes + **Why it exists**: + Consensus layer needs to know “this is the exact execution result” + Prevents lying about transaction outcomes ### Finalized root: + A cryptographic commitment to Ethereum’s state that is locked in forever When something is finalized: It cannot be reverted @@ -34,6 +38,7 @@ Exchanges wait for finalization before crediting deposits Apps trust finalized data ### Epoch + 🔹 Slot Time unit: 12 seconds Each slot can have one block @@ -51,14 +56,17 @@ Validators vote during epochs Finalization happens at epoch boundaries ### Fork choice + Ethereum is decentralized, so: Sometimes two blocks appear at the same time Network temporarily disagrees **Fork choice rule answers**: + ``` “Which chain should I follow?” ``` + Ethereum uses a rule called `LMD-GHOST`: Follow the chain with the most validator support Heavier = more votes, not more computation @@ -71,15 +79,15 @@ Resolves temporary splits. You can say it's a rule for picking the “real” ch Multiple branches → pick the branch with more people standing on it ### How everything connects (big picture) -Time → Slots → Blocks → Epochs - ↓ - Block Hash - ↓ - Execution Hash - ↓ - Finalized Root +Time → Slots → Blocks → Epochs +↓ +Block Hash +↓ +Execution Hash +↓ +Finalized Root Fork choice decides which blocks matter Epochs organize validator voting -Finalized root locks history forever \ No newline at end of file +Finalized root locks history forever diff --git a/assignments/5-json-rpc-methods/README.md b/assignments/5-json-rpc-methods/README.md index 6e863fcf..df110630 100644 --- a/assignments/5-json-rpc-methods/README.md +++ b/assignments/5-json-rpc-methods/README.md @@ -1,15 +1,19 @@ # JSON RPC METHODS ## What is `JSON RPC` in Ethereum? + JSON-RPC is just a way for your app to talk to an Ethereum node. **Think of it like this:** + - Ethereum node = a server that knows the blockchain - JSON-RPC = the language you use to ask it questions or give it instructions - You send a JSON message - The node replies with JSON ### Basic structure of a JSON-RPC request + Every request looks roughly like this: + ```rust { "jsonrpc": "2.0", @@ -19,6 +23,7 @@ Every request looks roughly like this: } ``` + **In here,** `jsonrpc` => The JSON-RPC protocol version `method` => What you eant to do @@ -26,26 +31,27 @@ Every request looks roughly like this: `id` => This is the request ID ## Main Ethereum JSON-RPC Method Groups -Ethereum methods are grouped by prefix: - -Prefix Purpose -eth_ Core Ethereum blockchain operations -net_ Network info -web3_ Client utilities -debug_ Debugging (advanced) -trace_ Execution tracing (advanced) - - +Ethereum methods are grouped by prefix: +Prefix Purpose +eth* Core Ethereum blockchain operations +net* Network info +web3* Client utilities +debug* Debugging (advanced) +trace\_ Execution tracing (advanced) ## Essential Ethereum JSON-RPC Methods (Beginner List) + ### 1️⃣ Blockchain Info + `eth_blockNumber` 📦 Get the latest block number + ``` "What is the most recent block?" ``` + Used for: syncing checking chain progress @@ -54,6 +60,7 @@ checking chain progress 📦 Get block details by block number "Give me block #18000000" Can return: + - transactions - block hash - miner @@ -61,55 +68,59 @@ Can return: `eth_getBlockByHash` 📦 Get block details using block hash + ```rust "Give me the block with this hash" ``` ### 2️⃣ Accounts & Balances + `eth_getBalance` 💰 Get ETH balance of an address + ``` "How much ETH does this address have?" ``` + Returns balance in wei (smallest ETH unit). `eth_getTransactionCount` 🔢 Get nonce for an address "How many transactions has this address sent?" Important for: + - creating transactions - preventing replay - - ### 3️⃣ Transactions + `eth_getTransactionByHash` 📨 Get transaction details + ``` "Show me this transaction" ``` - `eth_getTransactionReceipt` 🧾 Check if transaction succeeded or failed Returns: + - status (success/fail) - gas used - logs (events) - - `eth_sendRawTransaction` 🚀 Send a signed transaction + ``` "Broadcast this already-signed transaction" ``` + NOTE: Node does not sign for you, Wallet signs → node broadcasts - - ### 4️⃣ Smart Contracts + `eth_call` 🤖 Call a contract function (read-only) "Call this function, but don’t change state" @@ -122,13 +133,13 @@ instant response ⛽ Estimate gas for a transaction "How much gas will this transaction need?" - `eth_getCode` 📜 Get contract bytecode "Is there a contract at this address?" If result is 0x → no contract. ### 5️⃣ Logs & Events + `eth_getLogs` 📡 Get smart-contract events "Show me all Transfer events for this token" @@ -138,6 +149,7 @@ explorers analytics ### 6️⃣ Network Info + `net_version` 🌐 Get chain ID "Which network is this?" @@ -151,22 +163,22 @@ Examples: 👥 How many peers connected ### 7️⃣ Utility Methods + `web3_clientVersion` 🧠 Get node software info "Geth? Nethermind? Erigon?" - `web3_sha3` 🔐 Keccak-256 hash "Hash this data the Ethereum way" **Example: Balance Check in Plain English** + > App → Node: > “What’s the balance of 0xABC... at the latest block?” That’s just: + ```rust eth_getBalance(address, "latest") ``` - - diff --git a/assignments/5-rpc-methods/README.md b/assignments/5-rpc-methods/README.md new file mode 100644 index 00000000..6e863fcf --- /dev/null +++ b/assignments/5-rpc-methods/README.md @@ -0,0 +1,172 @@ +# JSON RPC METHODS + +## What is `JSON RPC` in Ethereum? +JSON-RPC is just a way for your app to talk to an Ethereum node. +**Think of it like this:** +- Ethereum node = a server that knows the blockchain +- JSON-RPC = the language you use to ask it questions or give it instructions +- You send a JSON message +- The node replies with JSON + +### Basic structure of a JSON-RPC request +Every request looks roughly like this: +```rust +{ + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [], + "id": 1 +} + +``` +**In here,** +`jsonrpc` => The JSON-RPC protocol version +`method` => What you eant to do +`params` => data you pass in +`id` => This is the request ID + +## Main Ethereum JSON-RPC Method Groups +Ethereum methods are grouped by prefix: + +Prefix Purpose +eth_ Core Ethereum blockchain operations +net_ Network info +web3_ Client utilities +debug_ Debugging (advanced) +trace_ Execution tracing (advanced) + + + + + +## Essential Ethereum JSON-RPC Methods (Beginner List) +### 1️⃣ Blockchain Info +`eth_blockNumber` +📦 Get the latest block number +``` +"What is the most recent block?" +``` +Used for: +syncing +checking chain progress + +`eth_getBlockByNumber` +📦 Get block details by block number +"Give me block #18000000" +Can return: +- transactions +- block hash +- miner +- timestamp + +`eth_getBlockByHash` +📦 Get block details using block hash +```rust +"Give me the block with this hash" +``` + +### 2️⃣ Accounts & Balances +`eth_getBalance` +💰 Get ETH balance of an address +``` +"How much ETH does this address have?" +``` +Returns balance in wei (smallest ETH unit). + +`eth_getTransactionCount` +🔢 Get nonce for an address +"How many transactions has this address sent?" +Important for: +- creating transactions +- preventing replay + + + +### 3️⃣ Transactions +`eth_getTransactionByHash` +📨 Get transaction details +``` +"Show me this transaction" +``` + + +`eth_getTransactionReceipt` +🧾 Check if transaction succeeded or failed +Returns: +- status (success/fail) +- gas used +- logs (events) + + + +`eth_sendRawTransaction` +🚀 Send a signed transaction +``` +"Broadcast this already-signed transaction" +``` +NOTE: Node does not sign for you, +Wallet signs → node broadcasts + + + +### 4️⃣ Smart Contracts +`eth_call` +🤖 Call a contract function (read-only) +"Call this function, but don’t change state" +Used for: +reading contract data +no gas cost +instant response + +`eth_estimateGas` +⛽ Estimate gas for a transaction +"How much gas will this transaction need?" + + +`eth_getCode` +📜 Get contract bytecode +"Is there a contract at this address?" +If result is 0x → no contract. + +### 5️⃣ Logs & Events +`eth_getLogs` +📡 Get smart-contract events +"Show me all Transfer events for this token" +Used heavily by: +indexers +explorers +analytics + +### 6️⃣ Network Info +`net_version` +🌐 Get chain ID +"Which network is this?" + +Examples: +1 → Ethereum Mainnet +5 → Goerli (deprecated) +11155111 → Sepolia + +`net_peerCount` +👥 How many peers connected + +### 7️⃣ Utility Methods +`web3_clientVersion` +🧠 Get node software info +"Geth? Nethermind? Erigon?" + + +`web3_sha3` +🔐 Keccak-256 hash +"Hash this data the Ethereum way" + +**Example: Balance Check in Plain English** +> App → Node: +> “What’s the balance of 0xABC... at the latest block?” + +That’s just: +```rust +eth_getBalance(address, "latest") +``` + + diff --git a/assignments/6-address-derivation/README.md b/assignments/6-address-derivation/README.md index 5bd28ce5..ed537df4 100644 --- a/assignments/6-address-derivation/README.md +++ b/assignments/6-address-derivation/README.md @@ -3,6 +3,7 @@ This project demonstrates how to generate an **Ethereum wallet** in Rust using industry standards such as **BIP-39**, **BIP-32/BIP-44**, and **Keccak256**. The program: + - Generates cryptographically secure entropy - Creates a 12-word mnemonic phrase - Derives an Ethereum private key using a BIP-44 path @@ -39,4 +40,5 @@ Private Key ↓ Public Key ↓ -Ethereum Address \ No newline at end of file +Ethereum Address +``` diff --git a/assignments/6-address_derivative/Cargo.lock b/assignments/6-address_derivative/Cargo.lock new file mode 100644 index 00000000..19bd30d2 --- /dev/null +++ b/assignments/6-address_derivative/Cargo.lock @@ -0,0 +1,738 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "address_derivative" +version = "0.1.0" +dependencies = [ + "bip39", + "hex", + "k256", + "sha3", + "tiny-hderive", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base58" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "hex-conservative", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.9", + "rand_core 0.6.4", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.9", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle 2.6.1", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.9", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hmac" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" +dependencies = [ + "crypto-mac", + "digest 0.8.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e570451493f10f6581b48cdd530413b63ea9e780f544bfd3bdcaa0d89d1a7b" +dependencies = [ + "digest 0.8.1", + "generic-array 0.12.4", + "hmac 0.7.1", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.9", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libsecp256k1" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc1e2c808481a63dc6da2074752fdd4336a3c8fcc68b83db6f1fd5224ae7962" +dependencies = [ + "arrayref", + "crunchy", + "digest 0.8.1", + "hmac-drbg", + "rand 0.7.3", + "sha2 0.8.2", + "subtle 2.6.1", + "typenum", +] + +[[package]] +name = "memzero" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c0d11ac30a033511ae414355d80f70d9f29a44a49140face477117a1ee90db" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle 2.6.1", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.9", + "pkcs8", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tiny-hderive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b874a4992538d4b2f4fbbac11b9419d685f4b39bdc3fed95b04e07bfd76040" +dependencies = [ + "base58", + "hmac 0.7.1", + "libsecp256k1", + "memzero", + "sha2 0.8.2", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/assignments/6-address_derivative/Cargo.toml b/assignments/6-address_derivative/Cargo.toml new file mode 100644 index 00000000..521706ed --- /dev/null +++ b/assignments/6-address_derivative/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "address_derivative" +version = "0.1.0" +edition = "2024" + +[dependencies] +bip39 = { version = "2.0", features = ["rand"] } +hex = "0.4.3" +tiny-hderive = "0.3" +k256 = { version = "0.13", features = ["ecdsa"] } +sha3 = "0.10" \ No newline at end of file diff --git a/assignments/6-address_derivative/README.md b/assignments/6-address_derivative/README.md new file mode 100644 index 00000000..5bd28ce5 --- /dev/null +++ b/assignments/6-address_derivative/README.md @@ -0,0 +1,42 @@ +# Ethereum Wallet Generator in Rust + +This project demonstrates how to generate an **Ethereum wallet** in Rust using industry standards such as **BIP-39**, **BIP-32/BIP-44**, and **Keccak256**. + +The program: +- Generates cryptographically secure entropy +- Creates a 12-word mnemonic phrase +- Derives an Ethereum private key using a BIP-44 path +- Computes the public key +- Generates an Ethereum address + +--- + +## Dependencies + +This project uses the following crates: + +- **bip39** – mnemonic phrase generation (BIP-39) +- **tiny-hderive** – hierarchical deterministic key derivation (BIP-32 / BIP-44) +- **k256** – secp256k1 elliptic curve cryptography (Ethereum keys) +- **sha3** – Keccak256 hashing (Ethereum address format) +- **hex** – hexadecimal encoding +- **rand (OsRng)** – secure randomness from the operating system + +--- + +## What the Code Does (High-Level) + +```text +OS Randomness + ↓ +Entropy + ↓ +12-word Mnemonic + ↓ +Seed + ↓ +Private Key + ↓ +Public Key + ↓ +Ethereum Address \ No newline at end of file diff --git a/assignments/6-address_derivative/src/main.rs b/assignments/6-address_derivative/src/main.rs new file mode 100644 index 00000000..12276a88 --- /dev/null +++ b/assignments/6-address_derivative/src/main.rs @@ -0,0 +1,46 @@ +use bip39::rand::RngCore; +use bip39::{Language, Mnemonic}; +use bip39::rand::rngs::OsRng; + +use tiny_hderive::bip32::ExtendedPrivKey; + +use k256::ecdsa::SigningKey; +use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::elliptic_curve::FieldBytes; + +use sha3::{Digest, Keccak256}; + + +fn main() { + let mut rng = OsRng; + + let mut array = [0u8; 12]; + OsRng.fill_bytes(&mut array); + println!("Entropy: {:?}", array); + + let mnemonic = Mnemonic::generate_in_with(&mut rng, Language::English, 12).expect("failed to generate mnemonic"); + println!("Mnemonic phrase"); + println!("{}", mnemonic); + + + let entropy = mnemonic.to_entropy(); + println!("Entropy (hex): {}", hex::encode(entropy)); + + + let seed = mnemonic.to_seed(""); + + let child_key = ExtendedPrivKey::derive(seed.as_slice(),"m/44'/60'/0'/0/0").expect("Derivation failed"); + + let signing_key = SigningKey::from_slice(&child_key.secret()) + .expect("Invalid private key"); + + let public_key = signing_key.verifying_key(); + let encoded = public_key.to_encoded_point(false); + let pubkey_bytes = encoded.as_bytes(); + let pubkey = &pubkey_bytes[1..]; + + let hash = Keccak256::digest(pubkey); + let address = &hash[12..]; + + println!("Ethereum address: 0x{}", hex::encode(address)); +} \ No newline at end of file diff --git a/assignments/7-wallet/README.md b/assignments/7-wallet/README.md new file mode 100644 index 00000000..e69de29b diff --git a/assignments/7-wallet/english.txt b/assignments/7-wallet/english.txt new file mode 100644 index 00000000..f78ccaf2 --- /dev/null +++ b/assignments/7-wallet/english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo \ No newline at end of file diff --git a/assignments/7-wallet/package.json b/assignments/7-wallet/package.json new file mode 100644 index 00000000..863083ed --- /dev/null +++ b/assignments/7-wallet/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "js-sha3": "^0.9.3" + } +} diff --git a/assignments/7-wallet/wallet.js b/assignments/7-wallet/wallet.js new file mode 100644 index 00000000..72e1327f --- /dev/null +++ b/assignments/7-wallet/wallet.js @@ -0,0 +1,306 @@ +const crypto = require("crypto"); +const fs = require("fs"); +const { keccak256 } = require("js-sha3"); + +/* ============================================================ + STEP 0: Load BIP-39 wordlist (2048 words) +============================================================ */ + +const WORDLIST = fs.readFileSync("english.txt", "utf8").trim().split("\n"); + + +/** + * Convert a byte array into a binary string + * Example: + * Buffer [255] -> "11111111" + */ +function bytesToBinary(bytes) { + let binary = ""; + + for (let i = 0; i < bytes.length; i++) { + // Convert each byte to binary + let byteInBinary = bytes[i].toString(2); + + // Make sure each byte is 8 bits long + while (byteInBinary.length < 8) { + byteInBinary = "0" + byteInBinary; + } + + binary += byteInBinary; + } + + return binary; +} + +/** + * Modulo function that always returns a positive value + */ +function mod(number, modulus) { + let result = number % modulus; + + if (result < 0n) { + result = result + modulus; + } + + return result; +} + + +/** + * Calculate modular inverse using + * the Extended Euclidean Algorithm + **/ +function modInv(a, m) { + let low = mod(a, m); + let high = m; + + let lm = 1n; + let hm = 0n; + + while (low > 1n) { + // How many times does high fit into low + let ratio = high / low; + + // Save previous values + let newLm = hm - lm * ratio; + let newLow = high - low * ratio; + + // Move values forward + hm = lm; + lm = newLm; + + high = low; + low = newLow; + } + + return mod(lm, m); +} +/* ============================================================ + STEP 1: Generate entropy +============================================================ */ + +function generateEntropy(bits = 128) { + console.log("==== STEP 1: ENTROPY ===="); + const entropy = crypto.randomBytes(bits / 8); + console.log(entropy.toString("hex"), "\n"); + return entropy; +} + +/* ============================================================ + STEP 2: Entropy → Mnemonic (REAL BIP-39) + ENT + checksum → 11-bit chunks → wordlist +============================================================ */ + +function entropyToMnemonic(entropy) { + console.log("==== STEP 2: MNEMONIC PHRASE ===="); + + const ENT = entropy.length * 8; + const CS = ENT / 32; + + const hash = crypto.createHash("sha256").update(entropy).digest(); + + const entropyBits = bytesToBinary(entropy); + const checksumBits = bytesToBinary(hash).slice(0, CS); + + const bits = entropyBits + checksumBits; + + const chunks = bits.match(/.{1,11}/g); + + const mnemonic = chunks + .map(bin => WORDLIST[parseInt(bin, 2)]) + .join(" "); + + console.log(mnemonic, "\n"); + return mnemonic; +} + +/* ============================================================ + STEP 3: Mnemonic → Seed (PBKDF2-HMAC-SHA512) +============================================================ */ + +function mnemonicToSeed(mnemonic) { + console.log("==== STEP 3: SEED (512-bit) ===="); + + const seed = crypto.pbkdf2Sync( + mnemonic, + "mnemonic", + 2048, + 64, + "sha512" + ); + + console.log(seed.toString("hex"), "\n"); + return seed; +} + +/* ============================================================ + STEP 4: Seed → Private Key (256 bits) +============================================================ */ + +function seedToPrivateKey(seed) { + console.log("==== STEP 4: PRIVATE KEY ===="); + + const privateKey = seed.slice(0, 32); + console.log(privateKey.toString("hex"), "\n"); + return privateKey; +} + +/* ============================================================ + STEP 5: Private Key → Public Key (secp256k1) + PublicKey = PrivateKey × G +============================================================ */ + +// secp256k1 constants +const CURVE_PRIME = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"); +const G_X = BigInt("0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"); +const G_Y = BigInt("0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"); + +// Elliptic curve point addition +function isValidPoint(p) { + return ( + p !== null && + Array.isArray(p) && + p.length === 2 && + typeof p[0] === "bigint" && + typeof p[1] === "bigint" + ); +} + +function addPoints(pointA, pointB) { + if (pointA === null) return pointB; + if (pointB === null) return pointA; + + if (!isValidPoint(pointA) || !isValidPoint(pointB)) { + console.error("Bad points:", pointA, pointB); + return null; + } + + const [x1, y1] = pointA; + const [x2, y2] = pointB; + + if (x1 === x2 && y1 !== y2) { + return null; + } + + let slope; + + if (x1 === x2 && y1 === y2) { + const top = 3n * x1 * x1; + const bottom = modInv(2n * y1, CURVE_PRIME); + slope = top * bottom; + } else { + const top = y2 - y1; + const bottom = modInv(x2 - x1, CURVE_PRIME); + slope = top * bottom; + } + + slope = mod(slope, CURVE_PRIME); + + const x3 = mod(slope * slope - x1 - x2, CURVE_PRIME); + const y3 = mod(slope * (x1 - x3) - y1, CURVE_PRIME); + + return [x3, y3]; +} + + +// Scalar multiplication (double-and-add) +function scalarMultiply(k, point) { + let result = null; + let addend = point; + + while (k > 0n) { + if (k & 1n) { + result = addPoints(result, addend); + } + + addend = addPoints(addend, addend); + + if (addend === null) { + return result; + } + + k >>= 1n; + } + + return result; +} + + +function privateKeyToPublicKey(privateKey) { + console.log("==== STEP 5: PUBLIC KEY ===="); + + // Convert private key buffer into a number + const privateNumber = BigInt("0x" + privateKey.toString("hex")); + + // Starting point on the curve + const G_X = 1n; + const G_Y = 3n; + const generatorPoint = [G_X, G_Y]; + + // Public key = private key × generator point + const publicPoint = scalarMultiply(privateNumber, generatorPoint); + + const xHex = publicPoint[0].toString(16).padStart(64, "0"); + const yHex = publicPoint[1].toString(16).padStart(64, "0"); + + const publicKey = xHex + yHex; + + console.log(publicKey, "\n"); + + return Buffer.from(publicKey, "hex"); +} + +/* ============================================================ + STEP 6: Public Key → Keccak-256 +============================================================ */ + +function publicKeyToHash(publicKey) { + console.log("==== STEP 6: KECCAK-256 ===="); + + const hash = keccak256(publicKey); + console.log(hash, "\n"); + return hash; +} + +/* ============================================================ + STEP 7: Hash → Ethereum Address + Last 20 bytes (40 hex chars) +============================================================ */ + +function hashToAddress(hash) { + console.log("==== STEP 7: ETHEREUM ADDRESS ===="); + + const address = "0x" + hash.slice(-40); + console.log(address, "\n"); + return address; +} + +/* ============================================================ + GenerateEthereumWallet Fxn +============================================================ */ + +function generateEthereumWallet() { + const entropy = generateEntropy(); + const mnemonic = entropyToMnemonic(entropy); + const seed = mnemonicToSeed(mnemonic); + const privateKey = seedToPrivateKey(seed); + const publicKey = privateKeyToPublicKey(privateKey); + const hash = publicKeyToHash(publicKey); + const address = hashToAddress(hash); + + return { + mnemonic, + privateKey: privateKey.toString("hex"), + publicKey: publicKey.toString("hex"), + address + }; +} + +/* ============================================================ + Test +============================================================ */ + +const wallet = generateEthereumWallet(); + +console.log("==== WALLET OBJECT ===="); +console.log(wallet); diff --git a/assignments/Class/.gitignore b/assignments/Class/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/Class/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/assignments/Class/README.md b/assignments/Class/README.md new file mode 100644 index 00000000..62a34711 --- /dev/null +++ b/assignments/Class/README.md @@ -0,0 +1,30 @@ +## MilestoneEscrow + +A simple and secure milestone-based escrow smart contract that enables a client to fund a project upfront and release payments to a freelancer per milestone as work is completed and approved. +This contract is designed for freelance or contractor workflows where payments are tied to deliverables instead of a single lump-sum transfer. + +Contract Address: 0x5DBE332243125b5E8E71F8A59bEEC7C9EccF49Fc + +Etherscan Verification: +(etherscan_link)[https://sepolia.etherscan.io/address/0x5DBE332243125b5E8E71F8A59bEEC7C9EccF49Fc] + +### Overview + +MilestoneEscrow allows: +A client to deposit the full project payment upfront +A freelancer to mark milestones as completed +The client (or anyone after timeout) to approve and release payments +Automatic approval after a timeout period +Cancellation with refund of remaining funds +Dispute signaling via events +Each milestone has a fixed payment amount, and funds are released incrementally. + +### Roles + +- Client + Deploys and funds the contract + Approves milestones + Can cancel the contract and reclaim remaining funds +- Freelancer + Marks milestones as completed + Receives milestone payments diff --git a/assignments/Class/contracts/Factory.sol b/assignments/Class/contracts/Factory.sol new file mode 100644 index 00000000..3b14a347 --- /dev/null +++ b/assignments/Class/contracts/Factory.sol @@ -0,0 +1,76 @@ + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import "./MilestoneEscrow.sol"; + +contract MilestoneEscrowFactory { + + address[] public allEscrows; + + // user => escrows they are involved in + mapping(address => address[]) public userEscrows; + + event EscrowCreated( + address indexed client, + address indexed freelancer, + address escrow, + uint256 milestones, + uint256 amountPerMilestone + ); + + function createEscrow( + address _freelancer, + uint256 _milestoneCount, + uint256 _amountPerMilestone + ) external payable returns (address) { + + require(_freelancer != address(0), "Invalid freelancer"); + require(_freelancer != msg.sender, "Self-hire not allowed"); + require(_milestoneCount > 0, "Milestones = 0"); + require(_amountPerMilestone > 0, "Amount = 0"); + + uint256 totalCost = _milestoneCount * _amountPerMilestone; + require(msg.value == totalCost, "Wrong ETH sent"); + + MilestoneEscrow escrow = new MilestoneEscrow{value: msg.value}( + _freelancer, + _milestoneCount, + _amountPerMilestone + ); + + address escrowAddr = address(escrow); + + allEscrows.push(escrowAddr); + + userEscrows[msg.sender].push(escrowAddr); + userEscrows[_freelancer].push(escrowAddr); + + emit EscrowCreated( + msg.sender, + _freelancer, + escrowAddr, + _milestoneCount, + _amountPerMilestone + ); + + return escrowAddr; + } + + function getUserEscrows(address user) + external + view + returns (address[] memory) + { + return userEscrows[user]; + } + + function getAllEscrows() + external + view + returns (address[] memory) + { + return allEscrows; + } +} + diff --git a/assignments/Class/contracts/MilestoneEscrow.sol b/assignments/Class/contracts/MilestoneEscrow.sol new file mode 100644 index 00000000..abf819a0 --- /dev/null +++ b/assignments/Class/contracts/MilestoneEscrow.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract MilestoneEscrow { + address public immutable client; + address public immutable freelancer; + + uint256 public immutable milestoneCount; + uint256 public immutable amountPerMilestone; + + uint256 public approvedMilestones; + uint256 public releasedMilestones; + + mapping(uint256 => bool) public completed; + mapping(uint256 => bool) public approved; + mapping(uint256 => uint256) public completionTime; + + bool public cancelled; + + uint256 public constant AUTO_APPROVE_TIMEOUT = 14 days; + + event Funded( + address indexed client, + address indexed freelancer, + uint256 totalAmount, + uint256 milestoneCount, + uint256 amountPerMilestone + ); + + event MilestoneCompleted( + uint256 indexed milestoneId, + address indexed freelancer, + uint256 timestamp + ); + + event MilestoneApproved( + uint256 indexed milestoneId, + address indexed approver, + uint256 amountReleased + ); + + + event MilestoneAutoApproved( + uint256 indexed milestoneId, + uint256 timeoutTimestamp, + uint256 amountReleased + ); + + + event ContractCancelled( + address indexed caller, + uint256 refundAmount, + uint256 timestamp + ); + + + event DisputeRaised( + uint256 indexed milestoneId, + address indexed raiser, + string reason + ); + + + event AllMilestonesReleased( + address indexed freelancer, + uint256 totalReleased + ); + + constructor( + address _freelancer, + uint256 _milestoneCount, + uint256 _amountPerMilestone + ) payable { + require(_freelancer != address(0), "Invalid freelancer address"); + require(_freelancer != msg.sender, "Client cannot be freelancer"); + require(_milestoneCount > 0, "At least one milestone required"); + require(_amountPerMilestone > 0, "Amount per milestone must be > 0"); + + uint256 expectedDeposit = _milestoneCount * _amountPerMilestone; + require(msg.value == expectedDeposit, "Must fund all milestones"); + + client = msg.sender; + freelancer = _freelancer; + + milestoneCount = _milestoneCount; + amountPerMilestone = _amountPerMilestone; + + emit Funded( + msg.sender, + _freelancer, + msg.value, + _milestoneCount, + _amountPerMilestone + ); + } + + function markCompleted(uint256 id) public { + require(msg.sender == freelancer, "Only freelancer can mark complete"); + require(id < milestoneCount, "Invalid milestone id"); + require(!completed[id], "Already marked as completed"); + require(!cancelled, "Contract is cancelled"); + + completed[id] = true; + completionTime[id] = block.timestamp; + + emit MilestoneCompleted(id, msg.sender, block.timestamp); + } + + + function approveMilestone(uint256 id) external { + _requireCanApprove(id); + + approved[id] = true; + approvedMilestones++; + releasedMilestones++; + + _safeTransfer(freelancer, amountPerMilestone); + + emit MilestoneApproved(id, msg.sender, amountPerMilestone); + + if (releasedMilestones == milestoneCount) { + emit AllMilestonesReleased(freelancer, address(this).balance); + } + } + + + function autoApprove(uint256 id) external { + _requireCanApprove(id); + + require( + block.timestamp >= completionTime[id] + AUTO_APPROVE_TIMEOUT, + "Timeout not reached yet" + ); + + approved[id] = true; + approvedMilestones++; + releasedMilestones++; + + _safeTransfer(freelancer, amountPerMilestone); + + emit MilestoneAutoApproved(id, block.timestamp, amountPerMilestone); + emit MilestoneApproved(id, address(0), amountPerMilestone); + + if (releasedMilestones == milestoneCount) { + emit AllMilestonesReleased(freelancer, address(this).balance); + } + } + + + function cancel() external { + require(msg.sender == client, "Only client can cancel"); + require(!cancelled, "Already cancelled"); + + cancelled = true; + + uint256 remaining = address(this).balance; + if (remaining > 0) { + _safeTransfer(client, remaining); + } + + emit ContractCancelled(msg.sender, remaining, block.timestamp); + } + + //TODO: Internal Helpers + function _requireCanApprove(uint256 id) internal view { + require(id < milestoneCount, "Invalid milestone id"); + require(completed[id], "Milestone not completed yet"); + require(!approved[id], "Already approved"); + require(!cancelled, "Contract is cancelled"); + } + + + function _safeTransfer(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(""); + require(success, "ETH transfer failed"); + } + + function isFullyReleased() external view returns (bool) { + return releasedMilestones == milestoneCount; + } + + function getRemainingBalance() external view returns (uint256) { + return address(this).balance; + } + + function canAutoApprove(uint256 id) external view returns (bool) { + if (id >= milestoneCount || !completed[id] || approved[id]) return false; + return block.timestamp >= completionTime[id] + AUTO_APPROVE_TIMEOUT; + } + + function raiseDispute(uint256 id, string calldata reason) external { + require(msg.sender == client || msg.sender == freelancer); + require(completed[id], "Not completed yet"); + require(!approved[id], "Already approved"); + + emit DisputeRaised(id, msg.sender, reason); + } +} diff --git a/assignments/Class/hardhat.config.ts b/assignments/Class/hardhat.config.ts new file mode 100644 index 00000000..64f25861 --- /dev/null +++ b/assignments/Class/hardhat.config.ts @@ -0,0 +1,44 @@ +import hardhatToolboxMochaEthersPlugin from '@nomicfoundation/hardhat-toolbox-mocha-ethers'; +import { configVariable, defineConfig } from 'hardhat/config'; +import 'dotenv/config'; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: '0.8.28', + }, + production: { + version: '0.8.28', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: 'edr-simulated', + chainType: 'l1', + }, + hardhatOp: { + type: 'edr-simulated', + chainType: 'op', + }, + sepolia: { + type: 'http', + chainType: 'l1', + url: configVariable('SEPOLIA_RPC_URL'), + accounts: [configVariable('SEPOLIA_PRIVATE_KEY')], + }, + }, + verify: { + etherscan: { + apiKey: configVariable('ETHERSCAN_API_KEY'), + }, + }, +}); diff --git a/assignments/Class/ignition/deployments/chain-11155111/artifacts/MileStoneEscrowModule#MilestoneEscrow.json b/assignments/Class/ignition/deployments/chain-11155111/artifacts/MileStoneEscrowModule#MilestoneEscrow.json new file mode 100644 index 00000000..687f9090 --- /dev/null +++ b/assignments/Class/ignition/deployments/chain-11155111/artifacts/MileStoneEscrowModule#MilestoneEscrow.json @@ -0,0 +1,583 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "MilestoneEscrow", + "sourceName": "contracts/MilestoneEscrow.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_freelancer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_milestoneCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_amountPerMilestone", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "freelancer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalReleased", + "type": "uint256" + } + ], + "name": "AllMilestonesReleased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "refundAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "ContractCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "milestoneId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "raiser", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "name": "DisputeRaised", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "client", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "freelancer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "milestoneCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountPerMilestone", + "type": "uint256" + } + ], + "name": "Funded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "milestoneId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "approver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountReleased", + "type": "uint256" + } + ], + "name": "MilestoneApproved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "milestoneId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timeoutTimestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountReleased", + "type": "uint256" + } + ], + "name": "MilestoneAutoApproved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "milestoneId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "freelancer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "MilestoneCompleted", + "type": "event" + }, + { + "inputs": [], + "name": "AUTO_APPROVE_TIMEOUT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "amountPerMilestone", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "approveMilestone", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "approved", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "approvedMilestones", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "autoApprove", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "canAutoApprove", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cancel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "cancelled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "client", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "completed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "completionTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "freelancer", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRemainingBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isFullyReleased", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "markCompleted", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "milestoneCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "name": "raiseDispute", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "releasedMilestones", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x6101006040526040516111be3803806111be8339810160408190526100239161023c565b6001600160a01b03831661007e5760405162461bcd60e51b815260206004820152601a60248201527f496e76616c696420667265656c616e636572206164647265737300000000000060448201526064015b60405180910390fd5b336001600160a01b038416036100d65760405162461bcd60e51b815260206004820152601b60248201527f436c69656e742063616e6e6f7420626520667265656c616e63657200000000006044820152606401610075565b5f82116101255760405162461bcd60e51b815260206004820152601f60248201527f4174206c65617374206f6e65206d696c6573746f6e65207265717569726564006044820152606401610075565b5f81116101745760405162461bcd60e51b815260206004820181905260248201527f416d6f756e7420706572206d696c6573746f6e65206d757374206265203e20306044820152606401610075565b5f61017f828461027b565b90508034146101d05760405162461bcd60e51b815260206004820152601860248201527f4d7573742066756e6420616c6c206d696c6573746f6e657300000000000000006044820152606401610075565b3360808190526001600160a01b03851660a081905260c085905260e084905260408051348152602081018790529081018590529091907fd67ae805400a673f32340691a1a850cf59e211ab6ae7d8ae2f20d6a345dd73d69060600160405180910390a3505050506102a4565b5f5f5f6060848603121561024e575f5ffd5b83516001600160a01b0381168114610264575f5ffd5b602085015160409095015190969495509392505050565b808202811582820484141761029e57634e487b7160e01b5f52601160045260245ffd5b92915050565b60805160a05160c05160e051610e696103555f395f81816102cb0152818161054a01528181610577015281816105cd0152818161071d015261074601525f81816101250152818161026c0152818161062201528181610819015281816109880152610b3b01525f818161022a01528181610358015281816105290152818161064b015281816106fc01526107a001525f818161015f0152818161032601528181610a170152610add0152610e695ff3fe608060405234801561000f575f5ffd5b506004361061011c575f3560e01c8063a37dda2c116100a9578063c438b40f1161006e578063c438b40f146102b3578063c8860f0e146102c6578063cac5c512146102ed578063d067493f14610300578063ea8a1af014610313575f5ffd5b8063a37dda2c14610225578063a9f1dd9c1461024c578063b45d941214610254578063bade2b4014610267578063c0cca5ef14610291575f5ffd5b80635aef573c116100ef5780635aef573c146101a85780637962a2dd146101bd5780637d4061e6146101c75780638897bbca146101f95780639a82a09a14610218575f5ffd5b80630681ca5514610120578063109e94cf1461015a5780632fba2c85146101995780633c9d31321461019f575b5f5ffd5b6101477f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b6101817f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610151565b47610147565b61014760015481565b6101bb6101b6366004610d32565b61031b565b005b6101476212750081565b6101e96101d5366004610da9565b60036020525f908152604090205460ff1681565b6040519015158152602001610151565b610147610207366004610da9565b60046020525f908152604090205481565b6005546101e99060ff1681565b6101817f000000000000000000000000000000000000000000000000000000000000000081565b6101475f5481565b6101bb610262366004610da9565b610472565b6001547f0000000000000000000000000000000000000000000000000000000000000000146101e9565b6101e961029f366004610da9565b60026020525f908152604090205460ff1681565b6101bb6102c1366004610da9565b6106b0565b6101477f000000000000000000000000000000000000000000000000000000000000000081565b6101bb6102fb366004610da9565b610795565b6101e961030e366004610da9565b610985565b6101bb610a0c565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061037a5750336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016145b610382575f5ffd5b5f8381526002602052604090205460ff166103d85760405162461bcd60e51b8152602060048201526011602482015270139bdd0818dbdb5c1b195d1959081e595d607a1b60448201526064015b60405180910390fd5b5f8381526003602052604090205460ff16156104295760405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48185c1c1c9bdd995960821b60448201526064016103cf565b336001600160a01b0316837f1b84372106d77c6daea0dda35bbc0229d10a83f58ec8990928849251936823418484604051610465929190610dc0565b60405180910390a3505050565b61047b81610b39565b5f81815260046020526040902054610497906212750090610e02565b4210156104e65760405162461bcd60e51b815260206004820152601760248201527f54696d656f7574206e6f7420726561636865642079657400000000000000000060448201526064016103cf565b5f818152600360205260408120805460ff191660011790558054908061050b83610e1b565b909155505060018054905f61051f83610e1b565b919050555061056e7f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000610c98565b604080514281527f0000000000000000000000000000000000000000000000000000000000000000602082015282917f64f9693f2536a415f3acc25ae27ad5957258acb86e95611cdcf4c6033fa3551a910160405180910390a26040517f000000000000000000000000000000000000000000000000000000000000000081525f9082907ffe731b8534f38a55c98725a977efe67da793f35fb32ca4d1d947c01d80259bc2906020015b60405180910390a37f0000000000000000000000000000000000000000000000000000000000000000600154036106ad577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03167fb729145b9d9a99bae64e0063dc83af15e00d8cc280817302286e6907c421fd4e476040516106a491815260200190565b60405180910390a25b50565b6106b981610b39565b5f818152600360205260408120805460ff19166001179055805490806106de83610e1b565b909155505060018054905f6106f283610e1b565b91905055506107417f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000610c98565b6040517f00000000000000000000000000000000000000000000000000000000000000008152339082907ffe731b8534f38a55c98725a977efe67da793f35fb32ca4d1d947c01d80259bc290602001610618565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146108175760405162461bcd60e51b815260206004820152602160248201527f4f6e6c7920667265656c616e6365722063616e206d61726b20636f6d706c65746044820152606560f81b60648201526084016103cf565b7f0000000000000000000000000000000000000000000000000000000000000000811061087d5760405162461bcd60e51b8152602060048201526014602482015273125b9d985b1a59081b5a5b195cdd1bdb99481a5960621b60448201526064016103cf565b5f8181526002602052604090205460ff16156108db5760405162461bcd60e51b815260206004820152601b60248201527f416c7265616479206d61726b656420617320636f6d706c65746564000000000060448201526064016103cf565b60055460ff16156109265760405162461bcd60e51b815260206004820152601560248201527410dbdb9d1c9858dd081a5cc818d85b98d95b1b1959605a1b60448201526064016103cf565b5f818152600260209081526040808320805460ff19166001179055600482529182902042908190559151918252339183917f785f24b6a98eafa7fd61a09cf31775b7bbc039cc966c9c6fad18743e13a9e1ea910160405180910390a350565b5f7f0000000000000000000000000000000000000000000000000000000000000000821015806109c357505f8281526002602052604090205460ff16155b806109db57505f8281526003602052604090205460ff165b156109e757505f919050565b5f82815260046020526040902054610a03906212750090610e02565b42101592915050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a7d5760405162461bcd60e51b815260206004820152601660248201527513db9b1e4818db1a595b9d0818d85b8818d85b98d95b60521b60448201526064016103cf565b60055460ff1615610ac45760405162461bcd60e51b8152602060048201526011602482015270105b1c9958591e4818d85b98d95b1b1959607a1b60448201526064016103cf565b6005805460ff19166001179055478015610b0257610b027f000000000000000000000000000000000000000000000000000000000000000082610c98565b6040805182815242602082015233917f6c5600c2f5d7b4db3a633e0dc94afdafdc7108ffb86326d40b5fddb30237c48191016106a4565b7f00000000000000000000000000000000000000000000000000000000000000008110610b9f5760405162461bcd60e51b8152602060048201526014602482015273125b9d985b1a59081b5a5b195cdd1bdb99481a5960621b60448201526064016103cf565b5f8181526002602052604090205460ff16610bfc5760405162461bcd60e51b815260206004820152601b60248201527f4d696c6573746f6e65206e6f7420636f6d706c6574656420796574000000000060448201526064016103cf565b5f8181526003602052604090205460ff1615610c4d5760405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48185c1c1c9bdd995960821b60448201526064016103cf565b60055460ff16156106ad5760405162461bcd60e51b815260206004820152601560248201527410dbdb9d1c9858dd081a5cc818d85b98d95b1b1959605a1b60448201526064016103cf565b5f826001600160a01b0316826040515f6040518083038185875af1925050503d805f8114610ce1576040519150601f19603f3d011682016040523d82523d5f602084013e610ce6565b606091505b5050905080610d2d5760405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b60448201526064016103cf565b505050565b5f5f5f60408486031215610d44575f5ffd5b83359250602084013567ffffffffffffffff811115610d61575f5ffd5b8401601f81018613610d71575f5ffd5b803567ffffffffffffffff811115610d87575f5ffd5b866020828401011115610d98575f5ffd5b939660209190910195509293505050565b5f60208284031215610db9575f5ffd5b5035919050565b60208152816020820152818360408301375f818301604090810191909152601f909201601f19160101919050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610e1557610e15610dee565b92915050565b5f60018201610e2c57610e2c610dee565b506001019056fea2646970667358221220fd8a44cfa2c0fde7a86be3779410d0f1721d7b4a6be97ec22064f767ce80d86e64736f6c634300081c0033", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b506004361061011c575f3560e01c8063a37dda2c116100a9578063c438b40f1161006e578063c438b40f146102b3578063c8860f0e146102c6578063cac5c512146102ed578063d067493f14610300578063ea8a1af014610313575f5ffd5b8063a37dda2c14610225578063a9f1dd9c1461024c578063b45d941214610254578063bade2b4014610267578063c0cca5ef14610291575f5ffd5b80635aef573c116100ef5780635aef573c146101a85780637962a2dd146101bd5780637d4061e6146101c75780638897bbca146101f95780639a82a09a14610218575f5ffd5b80630681ca5514610120578063109e94cf1461015a5780632fba2c85146101995780633c9d31321461019f575b5f5ffd5b6101477f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b6101817f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610151565b47610147565b61014760015481565b6101bb6101b6366004610d32565b61031b565b005b6101476212750081565b6101e96101d5366004610da9565b60036020525f908152604090205460ff1681565b6040519015158152602001610151565b610147610207366004610da9565b60046020525f908152604090205481565b6005546101e99060ff1681565b6101817f000000000000000000000000000000000000000000000000000000000000000081565b6101475f5481565b6101bb610262366004610da9565b610472565b6001547f0000000000000000000000000000000000000000000000000000000000000000146101e9565b6101e961029f366004610da9565b60026020525f908152604090205460ff1681565b6101bb6102c1366004610da9565b6106b0565b6101477f000000000000000000000000000000000000000000000000000000000000000081565b6101bb6102fb366004610da9565b610795565b6101e961030e366004610da9565b610985565b6101bb610a0c565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061037a5750336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016145b610382575f5ffd5b5f8381526002602052604090205460ff166103d85760405162461bcd60e51b8152602060048201526011602482015270139bdd0818dbdb5c1b195d1959081e595d607a1b60448201526064015b60405180910390fd5b5f8381526003602052604090205460ff16156104295760405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48185c1c1c9bdd995960821b60448201526064016103cf565b336001600160a01b0316837f1b84372106d77c6daea0dda35bbc0229d10a83f58ec8990928849251936823418484604051610465929190610dc0565b60405180910390a3505050565b61047b81610b39565b5f81815260046020526040902054610497906212750090610e02565b4210156104e65760405162461bcd60e51b815260206004820152601760248201527f54696d656f7574206e6f7420726561636865642079657400000000000000000060448201526064016103cf565b5f818152600360205260408120805460ff191660011790558054908061050b83610e1b565b909155505060018054905f61051f83610e1b565b919050555061056e7f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000610c98565b604080514281527f0000000000000000000000000000000000000000000000000000000000000000602082015282917f64f9693f2536a415f3acc25ae27ad5957258acb86e95611cdcf4c6033fa3551a910160405180910390a26040517f000000000000000000000000000000000000000000000000000000000000000081525f9082907ffe731b8534f38a55c98725a977efe67da793f35fb32ca4d1d947c01d80259bc2906020015b60405180910390a37f0000000000000000000000000000000000000000000000000000000000000000600154036106ad577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03167fb729145b9d9a99bae64e0063dc83af15e00d8cc280817302286e6907c421fd4e476040516106a491815260200190565b60405180910390a25b50565b6106b981610b39565b5f818152600360205260408120805460ff19166001179055805490806106de83610e1b565b909155505060018054905f6106f283610e1b565b91905055506107417f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000610c98565b6040517f00000000000000000000000000000000000000000000000000000000000000008152339082907ffe731b8534f38a55c98725a977efe67da793f35fb32ca4d1d947c01d80259bc290602001610618565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146108175760405162461bcd60e51b815260206004820152602160248201527f4f6e6c7920667265656c616e6365722063616e206d61726b20636f6d706c65746044820152606560f81b60648201526084016103cf565b7f0000000000000000000000000000000000000000000000000000000000000000811061087d5760405162461bcd60e51b8152602060048201526014602482015273125b9d985b1a59081b5a5b195cdd1bdb99481a5960621b60448201526064016103cf565b5f8181526002602052604090205460ff16156108db5760405162461bcd60e51b815260206004820152601b60248201527f416c7265616479206d61726b656420617320636f6d706c65746564000000000060448201526064016103cf565b60055460ff16156109265760405162461bcd60e51b815260206004820152601560248201527410dbdb9d1c9858dd081a5cc818d85b98d95b1b1959605a1b60448201526064016103cf565b5f818152600260209081526040808320805460ff19166001179055600482529182902042908190559151918252339183917f785f24b6a98eafa7fd61a09cf31775b7bbc039cc966c9c6fad18743e13a9e1ea910160405180910390a350565b5f7f0000000000000000000000000000000000000000000000000000000000000000821015806109c357505f8281526002602052604090205460ff16155b806109db57505f8281526003602052604090205460ff165b156109e757505f919050565b5f82815260046020526040902054610a03906212750090610e02565b42101592915050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a7d5760405162461bcd60e51b815260206004820152601660248201527513db9b1e4818db1a595b9d0818d85b8818d85b98d95b60521b60448201526064016103cf565b60055460ff1615610ac45760405162461bcd60e51b8152602060048201526011602482015270105b1c9958591e4818d85b98d95b1b1959607a1b60448201526064016103cf565b6005805460ff19166001179055478015610b0257610b027f000000000000000000000000000000000000000000000000000000000000000082610c98565b6040805182815242602082015233917f6c5600c2f5d7b4db3a633e0dc94afdafdc7108ffb86326d40b5fddb30237c48191016106a4565b7f00000000000000000000000000000000000000000000000000000000000000008110610b9f5760405162461bcd60e51b8152602060048201526014602482015273125b9d985b1a59081b5a5b195cdd1bdb99481a5960621b60448201526064016103cf565b5f8181526002602052604090205460ff16610bfc5760405162461bcd60e51b815260206004820152601b60248201527f4d696c6573746f6e65206e6f7420636f6d706c6574656420796574000000000060448201526064016103cf565b5f8181526003602052604090205460ff1615610c4d5760405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48185c1c1c9bdd995960821b60448201526064016103cf565b60055460ff16156106ad5760405162461bcd60e51b815260206004820152601560248201527410dbdb9d1c9858dd081a5cc818d85b98d95b1b1959605a1b60448201526064016103cf565b5f826001600160a01b0316826040515f6040518083038185875af1925050503d805f8114610ce1576040519150601f19603f3d011682016040523d82523d5f602084013e610ce6565b606091505b5050905080610d2d5760405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b60448201526064016103cf565b505050565b5f5f5f60408486031215610d44575f5ffd5b83359250602084013567ffffffffffffffff811115610d61575f5ffd5b8401601f81018613610d71575f5ffd5b803567ffffffffffffffff811115610d87575f5ffd5b866020828401011115610d98575f5ffd5b939660209190910195509293505050565b5f60208284031215610db9575f5ffd5b5035919050565b60208152816020820152818360408301375f818301604090810191909152601f909201601f19160101919050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610e1557610e15610dee565b92915050565b5f60018201610e2c57610e2c610dee565b506001019056fea2646970667358221220fd8a44cfa2c0fde7a86be3779410d0f1721d7b4a6be97ec22064f767ce80d86e64736f6c634300081c0033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": { + "3": [ + { + "length": 32, + "start": 351 + }, + { + "length": 32, + "start": 806 + }, + { + "length": 32, + "start": 2583 + }, + { + "length": 32, + "start": 2781 + } + ], + "5": [ + { + "length": 32, + "start": 554 + }, + { + "length": 32, + "start": 856 + }, + { + "length": 32, + "start": 1321 + }, + { + "length": 32, + "start": 1611 + }, + { + "length": 32, + "start": 1788 + }, + { + "length": 32, + "start": 1952 + } + ], + "7": [ + { + "length": 32, + "start": 293 + }, + { + "length": 32, + "start": 620 + }, + { + "length": 32, + "start": 1570 + }, + { + "length": 32, + "start": 2073 + }, + { + "length": 32, + "start": 2440 + }, + { + "length": 32, + "start": 2875 + } + ], + "9": [ + { + "length": 32, + "start": 715 + }, + { + "length": 32, + "start": 1354 + }, + { + "length": 32, + "start": 1399 + }, + { + "length": 32, + "start": 1485 + }, + { + "length": 32, + "start": 1821 + }, + { + "length": 32, + "start": 1862 + } + ] + }, + "inputSourceName": "project/contracts/MilestoneEscrow.sol", + "buildInfoId": "solc-0_8_28-7bdabc20085237a11eb5670fa33121eb3a55f7b5" +} \ No newline at end of file diff --git a/assignments/Class/ignition/deployments/chain-11155111/deployed_addresses.json b/assignments/Class/ignition/deployments/chain-11155111/deployed_addresses.json new file mode 100644 index 00000000..64bacb7d --- /dev/null +++ b/assignments/Class/ignition/deployments/chain-11155111/deployed_addresses.json @@ -0,0 +1,3 @@ +{ + "MileStoneEscrowModule#MilestoneEscrow": "0x5DBE332243125b5E8E71F8A59bEEC7C9EccF49Fc" +} diff --git a/assignments/Class/ignition/deployments/chain-11155111/journal.jsonl b/assignments/Class/ignition/deployments/chain-11155111/journal.jsonl new file mode 100644 index 00000000..925f455e --- /dev/null +++ b/assignments/Class/ignition/deployments/chain-11155111/journal.jsonl @@ -0,0 +1,8 @@ + +{"chainId":11155111,"type":"DEPLOYMENT_INITIALIZE"} +{"artifactId":"MileStoneEscrowModule#MilestoneEscrow","constructorArgs":["0x910e783EaCbdC2453827B443Bf125Bac4B76E5C2",{"_kind":"bigint","value":"5"},{"_kind":"bigint","value":"2"}],"contractName":"MilestoneEscrow","dependencies":[],"from":"0x4e1b1d9af926e7e0fbcb9c5b23eeda9d80642b99","futureId":"MileStoneEscrowModule#MilestoneEscrow","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"10"}} +{"futureId":"MileStoneEscrowModule#MilestoneEscrow","networkInteraction":{"data":"0x6101006040526040516111be3803806111be8339810160408190526100239161023c565b6001600160a01b03831661007e5760405162461bcd60e51b815260206004820152601a60248201527f496e76616c696420667265656c616e636572206164647265737300000000000060448201526064015b60405180910390fd5b336001600160a01b038416036100d65760405162461bcd60e51b815260206004820152601b60248201527f436c69656e742063616e6e6f7420626520667265656c616e63657200000000006044820152606401610075565b5f82116101255760405162461bcd60e51b815260206004820152601f60248201527f4174206c65617374206f6e65206d696c6573746f6e65207265717569726564006044820152606401610075565b5f81116101745760405162461bcd60e51b815260206004820181905260248201527f416d6f756e7420706572206d696c6573746f6e65206d757374206265203e20306044820152606401610075565b5f61017f828461027b565b90508034146101d05760405162461bcd60e51b815260206004820152601860248201527f4d7573742066756e6420616c6c206d696c6573746f6e657300000000000000006044820152606401610075565b3360808190526001600160a01b03851660a081905260c085905260e084905260408051348152602081018790529081018590529091907fd67ae805400a673f32340691a1a850cf59e211ab6ae7d8ae2f20d6a345dd73d69060600160405180910390a3505050506102a4565b5f5f5f6060848603121561024e575f5ffd5b83516001600160a01b0381168114610264575f5ffd5b602085015160409095015190969495509392505050565b808202811582820484141761029e57634e487b7160e01b5f52601160045260245ffd5b92915050565b60805160a05160c05160e051610e696103555f395f81816102cb0152818161054a01528181610577015281816105cd0152818161071d015261074601525f81816101250152818161026c0152818161062201528181610819015281816109880152610b3b01525f818161022a01528181610358015281816105290152818161064b015281816106fc01526107a001525f818161015f0152818161032601528181610a170152610add0152610e695ff3fe608060405234801561000f575f5ffd5b506004361061011c575f3560e01c8063a37dda2c116100a9578063c438b40f1161006e578063c438b40f146102b3578063c8860f0e146102c6578063cac5c512146102ed578063d067493f14610300578063ea8a1af014610313575f5ffd5b8063a37dda2c14610225578063a9f1dd9c1461024c578063b45d941214610254578063bade2b4014610267578063c0cca5ef14610291575f5ffd5b80635aef573c116100ef5780635aef573c146101a85780637962a2dd146101bd5780637d4061e6146101c75780638897bbca146101f95780639a82a09a14610218575f5ffd5b80630681ca5514610120578063109e94cf1461015a5780632fba2c85146101995780633c9d31321461019f575b5f5ffd5b6101477f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b6101817f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610151565b47610147565b61014760015481565b6101bb6101b6366004610d32565b61031b565b005b6101476212750081565b6101e96101d5366004610da9565b60036020525f908152604090205460ff1681565b6040519015158152602001610151565b610147610207366004610da9565b60046020525f908152604090205481565b6005546101e99060ff1681565b6101817f000000000000000000000000000000000000000000000000000000000000000081565b6101475f5481565b6101bb610262366004610da9565b610472565b6001547f0000000000000000000000000000000000000000000000000000000000000000146101e9565b6101e961029f366004610da9565b60026020525f908152604090205460ff1681565b6101bb6102c1366004610da9565b6106b0565b6101477f000000000000000000000000000000000000000000000000000000000000000081565b6101bb6102fb366004610da9565b610795565b6101e961030e366004610da9565b610985565b6101bb610a0c565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061037a5750336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016145b610382575f5ffd5b5f8381526002602052604090205460ff166103d85760405162461bcd60e51b8152602060048201526011602482015270139bdd0818dbdb5c1b195d1959081e595d607a1b60448201526064015b60405180910390fd5b5f8381526003602052604090205460ff16156104295760405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48185c1c1c9bdd995960821b60448201526064016103cf565b336001600160a01b0316837f1b84372106d77c6daea0dda35bbc0229d10a83f58ec8990928849251936823418484604051610465929190610dc0565b60405180910390a3505050565b61047b81610b39565b5f81815260046020526040902054610497906212750090610e02565b4210156104e65760405162461bcd60e51b815260206004820152601760248201527f54696d656f7574206e6f7420726561636865642079657400000000000000000060448201526064016103cf565b5f818152600360205260408120805460ff191660011790558054908061050b83610e1b565b909155505060018054905f61051f83610e1b565b919050555061056e7f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000610c98565b604080514281527f0000000000000000000000000000000000000000000000000000000000000000602082015282917f64f9693f2536a415f3acc25ae27ad5957258acb86e95611cdcf4c6033fa3551a910160405180910390a26040517f000000000000000000000000000000000000000000000000000000000000000081525f9082907ffe731b8534f38a55c98725a977efe67da793f35fb32ca4d1d947c01d80259bc2906020015b60405180910390a37f0000000000000000000000000000000000000000000000000000000000000000600154036106ad577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03167fb729145b9d9a99bae64e0063dc83af15e00d8cc280817302286e6907c421fd4e476040516106a491815260200190565b60405180910390a25b50565b6106b981610b39565b5f818152600360205260408120805460ff19166001179055805490806106de83610e1b565b909155505060018054905f6106f283610e1b565b91905055506107417f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000610c98565b6040517f00000000000000000000000000000000000000000000000000000000000000008152339082907ffe731b8534f38a55c98725a977efe67da793f35fb32ca4d1d947c01d80259bc290602001610618565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146108175760405162461bcd60e51b815260206004820152602160248201527f4f6e6c7920667265656c616e6365722063616e206d61726b20636f6d706c65746044820152606560f81b60648201526084016103cf565b7f0000000000000000000000000000000000000000000000000000000000000000811061087d5760405162461bcd60e51b8152602060048201526014602482015273125b9d985b1a59081b5a5b195cdd1bdb99481a5960621b60448201526064016103cf565b5f8181526002602052604090205460ff16156108db5760405162461bcd60e51b815260206004820152601b60248201527f416c7265616479206d61726b656420617320636f6d706c65746564000000000060448201526064016103cf565b60055460ff16156109265760405162461bcd60e51b815260206004820152601560248201527410dbdb9d1c9858dd081a5cc818d85b98d95b1b1959605a1b60448201526064016103cf565b5f818152600260209081526040808320805460ff19166001179055600482529182902042908190559151918252339183917f785f24b6a98eafa7fd61a09cf31775b7bbc039cc966c9c6fad18743e13a9e1ea910160405180910390a350565b5f7f0000000000000000000000000000000000000000000000000000000000000000821015806109c357505f8281526002602052604090205460ff16155b806109db57505f8281526003602052604090205460ff165b156109e757505f919050565b5f82815260046020526040902054610a03906212750090610e02565b42101592915050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a7d5760405162461bcd60e51b815260206004820152601660248201527513db9b1e4818db1a595b9d0818d85b8818d85b98d95b60521b60448201526064016103cf565b60055460ff1615610ac45760405162461bcd60e51b8152602060048201526011602482015270105b1c9958591e4818d85b98d95b1b1959607a1b60448201526064016103cf565b6005805460ff19166001179055478015610b0257610b027f000000000000000000000000000000000000000000000000000000000000000082610c98565b6040805182815242602082015233917f6c5600c2f5d7b4db3a633e0dc94afdafdc7108ffb86326d40b5fddb30237c48191016106a4565b7f00000000000000000000000000000000000000000000000000000000000000008110610b9f5760405162461bcd60e51b8152602060048201526014602482015273125b9d985b1a59081b5a5b195cdd1bdb99481a5960621b60448201526064016103cf565b5f8181526002602052604090205460ff16610bfc5760405162461bcd60e51b815260206004820152601b60248201527f4d696c6573746f6e65206e6f7420636f6d706c6574656420796574000000000060448201526064016103cf565b5f8181526003602052604090205460ff1615610c4d5760405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48185c1c1c9bdd995960821b60448201526064016103cf565b60055460ff16156106ad5760405162461bcd60e51b815260206004820152601560248201527410dbdb9d1c9858dd081a5cc818d85b98d95b1b1959605a1b60448201526064016103cf565b5f826001600160a01b0316826040515f6040518083038185875af1925050503d805f8114610ce1576040519150601f19603f3d011682016040523d82523d5f602084013e610ce6565b606091505b5050905080610d2d5760405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b60448201526064016103cf565b505050565b5f5f5f60408486031215610d44575f5ffd5b83359250602084013567ffffffffffffffff811115610d61575f5ffd5b8401601f81018613610d71575f5ffd5b803567ffffffffffffffff811115610d87575f5ffd5b866020828401011115610d98575f5ffd5b939660209190910195509293505050565b5f60208284031215610db9575f5ffd5b5035919050565b60208152816020820152818360408301375f818301604090810191909152601f909201601f19160101919050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610e1557610e15610dee565b92915050565b5f60018201610e2c57610e2c610dee565b506001019056fea2646970667358221220fd8a44cfa2c0fde7a86be3779410d0f1721d7b4a6be97ec22064f767ce80d86e64736f6c634300081c0033000000000000000000000000910e783eacbdc2453827b443bf125bac4b76e5c200000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000002","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"10"}},"type":"NETWORK_INTERACTION_REQUEST"} +{"futureId":"MileStoneEscrowModule#MilestoneEscrow","networkInteractionId":1,"nonce":3,"type":"TRANSACTION_PREPARE_SEND"} +{"futureId":"MileStoneEscrowModule#MilestoneEscrow","networkInteractionId":1,"nonce":3,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"11616164521"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1439189"}},"hash":"0xf8f776e9de9771b8030c61686ae9ddcf44335a64fb296c2c52e1953bdfd1f2ea"},"type":"TRANSACTION_SEND"} +{"futureId":"MileStoneEscrowModule#MilestoneEscrow","hash":"0xf8f776e9de9771b8030c61686ae9ddcf44335a64fb296c2c52e1953bdfd1f2ea","networkInteractionId":1,"receipt":{"blockHash":"0x5373d68d8e8b34fba671f2bbaedb758f7cb84ac7ab9184b0a7085c183f644708","blockNumber":10244536,"contractAddress":"0x5DBE332243125b5E8E71F8A59bEEC7C9EccF49Fc","logs":[{"address":"0x5DBE332243125b5E8E71F8A59bEEC7C9EccF49Fc","data":"0x000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000002","logIndex":360,"topics":["0xd67ae805400a673f32340691a1a850cf59e211ab6ae7d8ae2f20d6a345dd73d6","0x0000000000000000000000004e1b1d9af926e7e0fbcb9c5b23eeda9d80642b99","0x000000000000000000000000910e783eacbdc2453827b443bf125bac4b76e5c2"]}],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} +{"futureId":"MileStoneEscrowModule#MilestoneEscrow","result":{"address":"0x5DBE332243125b5E8E71F8A59bEEC7C9EccF49Fc","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} \ No newline at end of file diff --git a/assignments/Class/ignition/modules/MileStoneEscrow.ts b/assignments/Class/ignition/modules/MileStoneEscrow.ts new file mode 100644 index 00000000..1331bad2 --- /dev/null +++ b/assignments/Class/ignition/modules/MileStoneEscrow.ts @@ -0,0 +1,14 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; + +export default buildModule('MileStoneEscrowModule', (m) => { + let address = '0x910e783EaCbdC2453827B443Bf125Bac4B76E5C2'; + let milestoneCount = BigInt(5); + let amountPerMilestone = BigInt(2); + const mileStoneEscrow = m.contract( + 'MilestoneEscrow', + [address, milestoneCount, amountPerMilestone], + { value: BigInt(milestoneCount * amountPerMilestone) } + ); + + return { mileStoneEscrow }; +}); diff --git a/assignments/Class/package.json b/assignments/Class/package.json new file mode 100644 index 00000000..5d19ae51 --- /dev/null +++ b/assignments/Class/package.json @@ -0,0 +1,24 @@ +{ + "name": "Class", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.8", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.6", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + }, + "dependencies": { + "dotenv": "^17.2.4", + "viem": "^2.45.3" + } +} diff --git a/assignments/Class/scripts/keys.js b/assignments/Class/scripts/keys.js new file mode 100644 index 00000000..d3bce1dd --- /dev/null +++ b/assignments/Class/scripts/keys.js @@ -0,0 +1,7 @@ +import { generatePrivateKey } from 'viem/accounts'; +import { privateKeyToAccount } from 'viem/accounts'; + +const pk = generatePrivateKey(); +const account = privateKeyToAccount(pk); +console.log(pk); +console.log(account.address); diff --git a/assignments/Class/scripts/send-op-tx.ts b/assignments/Class/scripts/send-op-tx.ts new file mode 100644 index 00000000..681a76e0 --- /dev/null +++ b/assignments/Class/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from 'hardhat'; + +const { ethers } = await network.connect({ + network: 'hardhatOp', + chainType: 'op', +}); + +console.log('Sending transaction using the OP chain type'); + +const [sender] = await ethers.getSigners(); + +console.log('Sending 1 wei from', sender.address, 'to itself'); + +console.log('Sending L2 transaction'); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log('Transaction sent successfully'); diff --git a/assignments/Class/test/MileStoneEscrow.ts b/assignments/Class/test/MileStoneEscrow.ts new file mode 100644 index 00000000..ddc39bf1 --- /dev/null +++ b/assignments/Class/test/MileStoneEscrow.ts @@ -0,0 +1,640 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; +//import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +//import { time } from "@nomicfoundation/hardhat-network-helpers"; +//import { MilestoneEscrow } from "../typechain-types"; +//import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +const { ethers, networkHelpers } = await network.connect(); +const { time, loadFixture } = networkHelpers; +// ─── Constants ────────────────────────────────────────────────────────────── +const AUTO_APPROVE_TIMEOUT = 14 * 24 * 60 * 60; // 14 days in seconds +const MILESTONE_COUNT = 3; +const AMOUNT_PER_MILESTONE = ethers.parseEther('1'); +const TOTAL_AMOUNT = AMOUNT_PER_MILESTONE * BigInt(MILESTONE_COUNT); + +// ─── Fixtures ──────────────────────────────────────────────────────────────── +async function deployEscrowFixture() { + const [, client, freelancer, stranger] = await ethers.getSigners(); + + const factory = await ethers.getContractFactory('MilestoneEscrow', client); + const escrow = (await factory.deploy( + freelancer.address, + MILESTONE_COUNT, + AMOUNT_PER_MILESTONE, + { value: TOTAL_AMOUNT } + )) as unknown as any; + + await escrow.waitForDeployment(); + + return { escrow, client, freelancer, stranger }; +} + +/** Deploys a single-milestone escrow for simpler tests */ +async function deploySingleMilestoneFixture() { + const [, client, freelancer] = await ethers.getSigners(); + const amount = ethers.parseEther('1'); + + const factory = await ethers.getContractFactory('MilestoneEscrow', client); + const escrow = (await factory.deploy(freelancer.address, 1, amount, { + value: amount, + })) as unknown as any; + + await escrow.waitForDeployment(); + return { escrow, client, freelancer, amount }; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── +async function completeAndApprove( + escrow: any, + freelancer: any, + client: any, + id: number +) { + await escrow.connect(freelancer).markCompleted(id); + await escrow.connect(client).approveMilestone(id); +} + +/** Manual balance-delta helper — avoids changeEtherBalance type issues */ +async function getBalanceDelta( + account: any, + action: () => Promise +): Promise { + const before = await ethers.provider.getBalance(account.address); + await action(); + const after = await ethers.provider.getBalance(account.address); + return after - before; +} + +// ════════════════════════════════════════════════════════════════════════════ +// TEST SUITE +// ════════════════════════════════════════════════════════════════════════════ +describe('MilestoneEscrow', () => { + // ── Deployment ───────────────────────────────────────────────────────────── + describe('Deployment', () => { + it('sets client, freelancer, milestoneCount, amountPerMilestone correctly', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + + expect(await escrow.client()).to.equal(client.address); + expect(await escrow.freelancer()).to.equal(freelancer.address); + expect(await escrow.milestoneCount()).to.equal(MILESTONE_COUNT); + expect(await escrow.amountPerMilestone()).to.equal(AMOUNT_PER_MILESTONE); + }); + + it('holds the full escrow balance after deployment', async () => { + const { escrow } = await loadFixture(deployEscrowFixture); + expect(await escrow.getRemainingBalance()).to.equal(TOTAL_AMOUNT); + }); + + it('emits Funded event on deployment', async () => { + const [, client, freelancer] = await ethers.getSigners(); + const factory = await ethers.getContractFactory( + 'MilestoneEscrow', + client + ); + + // Deploy once, then inspect the deployment transaction + const escrow = await factory.deploy( + freelancer.address, + MILESTONE_COUNT, + AMOUNT_PER_MILESTONE, + { value: TOTAL_AMOUNT } + ); + await escrow.waitForDeployment(); + + await expect(escrow.deploymentTransaction()) + .to.emit(escrow, 'Funded') + .withArgs( + client.address, + freelancer.address, + TOTAL_AMOUNT, + MILESTONE_COUNT, + AMOUNT_PER_MILESTONE + ); + }); + + it('reverts when freelancer is zero address', async () => { + const [, client] = await ethers.getSigners(); + const factory = await ethers.getContractFactory( + 'MilestoneEscrow', + client + ); + + await expect( + factory.deploy(ethers.ZeroAddress, 1, AMOUNT_PER_MILESTONE, { + value: AMOUNT_PER_MILESTONE, + }) + ).to.be.revertedWith('Invalid freelancer address'); + }); + + it('reverts when client and freelancer are the same address', async () => { + const [, client] = await ethers.getSigners(); + const factory = await ethers.getContractFactory( + 'MilestoneEscrow', + client + ); + + await expect( + factory.deploy(client.address, 1, AMOUNT_PER_MILESTONE, { + value: AMOUNT_PER_MILESTONE, + }) + ).to.be.revertedWith('Client cannot be freelancer'); + }); + + it('reverts when milestoneCount is zero', async () => { + const [, client, freelancer] = await ethers.getSigners(); + const factory = await ethers.getContractFactory( + 'MilestoneEscrow', + client + ); + + await expect( + factory.deploy(freelancer.address, 0, AMOUNT_PER_MILESTONE, { + value: 0, + }) + ).to.be.revertedWith('At least one milestone required'); + }); + + it('reverts when amountPerMilestone is zero', async () => { + const [, client, freelancer] = await ethers.getSigners(); + const factory = await ethers.getContractFactory( + 'MilestoneEscrow', + client + ); + + await expect( + factory.deploy(freelancer.address, 1, 0, { value: 0 }) + ).to.be.revertedWith('Amount per milestone must be > 0'); + }); + + it('reverts when msg.value does not match expected deposit', async () => { + const [, client, freelancer] = await ethers.getSigners(); + const factory = await ethers.getContractFactory( + 'MilestoneEscrow', + client + ); + + await expect( + factory.deploy(freelancer.address, 2, AMOUNT_PER_MILESTONE, { + value: AMOUNT_PER_MILESTONE, // should be 2× but only sending 1× + }) + ).to.be.revertedWith('Must fund all milestones'); + }); + }); + + // ── markCompleted ────────────────────────────────────────────────────────── + describe('markCompleted', () => { + it('freelancer can mark a milestone complete', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + + await escrow.connect(freelancer).markCompleted(0); + expect(await escrow.completed(0)).to.be.true; + }); + + it('records the completion timestamp', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + const tx = await escrow.connect(freelancer).markCompleted(0); + const block = await ethers.provider.getBlock(tx.blockNumber!); + + expect(await escrow.completionTime(0)).to.equal(block!.timestamp); + }); + + it('emits MilestoneCompleted event', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + + await expect(escrow.connect(freelancer).markCompleted(0)) + .to.emit(escrow, 'MilestoneCompleted') + .withArgs(0, freelancer.address, (v: bigint) => v > 0n); + }); + + it('reverts when called by non-freelancer', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + + await expect(escrow.connect(client).markCompleted(0)).to.be.revertedWith( + 'Only freelancer can mark complete' + ); + }); + + it('reverts for out-of-range milestone id', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + + await expect( + escrow.connect(freelancer).markCompleted(MILESTONE_COUNT) + ).to.be.revertedWith('Invalid milestone id'); + }); + + it('reverts when already completed', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + await expect( + escrow.connect(freelancer).markCompleted(0) + ).to.be.revertedWith('Already marked as completed'); + }); + + it('reverts when contract is cancelled', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(client).cancel(); + + await expect( + escrow.connect(freelancer).markCompleted(0) + ).to.be.revertedWith('Contract is cancelled'); + }); + }); + + // ── approveMilestone ─────────────────────────────────────────────────────── + describe('approveMilestone', () => { + it('client can approve a completed milestone and freelancer receives funds', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + const delta = await getBalanceDelta(freelancer, async () => { + await escrow.connect(client).approveMilestone(0); + }); + + expect(delta).to.equal(AMOUNT_PER_MILESTONE); + }); + + it('marks milestone as approved and increments counters', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).approveMilestone(0); + + expect(await escrow.approved(0)).to.be.true; + expect(await escrow.approvedMilestones()).to.equal(1); + expect(await escrow.releasedMilestones()).to.equal(1); + }); + + it('emits MilestoneApproved event', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + await expect(escrow.connect(client).approveMilestone(0)) + .to.emit(escrow, 'MilestoneApproved') + .withArgs(0, client.address, AMOUNT_PER_MILESTONE); + }); + + it('emits AllMilestonesReleased when last milestone is approved', async () => { + const { escrow, client, freelancer } = await loadFixture( + deploySingleMilestoneFixture + ); + await escrow.connect(freelancer).markCompleted(0); + + await expect(escrow.connect(client).approveMilestone(0)).to.emit( + escrow, + 'AllMilestonesReleased' + ); + }); + + it('reduces contract balance after approval', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).approveMilestone(0); + + expect(await escrow.getRemainingBalance()).to.equal( + TOTAL_AMOUNT - AMOUNT_PER_MILESTONE + ); + }); + + it('reverts when milestone is not completed', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + + await expect( + escrow.connect(client).approveMilestone(0) + ).to.be.revertedWith('Milestone not completed yet'); + }); + + it('reverts when already approved', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).approveMilestone(0); + + await expect( + escrow.connect(client).approveMilestone(0) + ).to.be.revertedWith('Already approved'); + }); + + it('reverts for invalid milestone id', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + + await expect( + escrow.connect(client).approveMilestone(999) + ).to.be.revertedWith('Invalid milestone id'); + }); + + it('reverts when contract is cancelled', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).cancel(); + + await expect( + escrow.connect(client).approveMilestone(0) + ).to.be.revertedWith('Contract is cancelled'); + }); + + it('stranger can approve (no onlyClient guard — documents current behaviour)', async () => { + const { escrow, freelancer, stranger } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + const delta = await getBalanceDelta(freelancer, async () => { + await escrow.connect(stranger).approveMilestone(0); + }); + + expect(delta).to.equal(AMOUNT_PER_MILESTONE); + }); + }); + + // ── autoApprove ─────────────────────────────────────────────────────────── + describe('autoApprove', () => { + it('reverts before the 14-day timeout', async () => { + const { escrow, freelancer, stranger } = + await loadFixture(deployEscrowFixture); + const tx = await escrow.connect(freelancer).markCompleted(0); + const block = await ethers.provider.getBlock(tx.blockNumber!); + const completionTs = block!.timestamp; + + // Pin the autoApprove call to land exactly 1 second before the deadline + await time.setNextBlockTimestamp(completionTs + AUTO_APPROVE_TIMEOUT - 1); + + await expect(escrow.connect(stranger).autoApprove(0)).to.be.revertedWith( + 'Timeout not reached yet' + ); + }); + + it('succeeds after the 14-day timeout and pays freelancer', async () => { + const { escrow, freelancer, stranger } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + await time.increase(AUTO_APPROVE_TIMEOUT); + + const delta = await getBalanceDelta(freelancer, async () => { + await escrow.connect(stranger).autoApprove(0); + }); + + expect(delta).to.equal(AMOUNT_PER_MILESTONE); + }); + + it('emits MilestoneAutoApproved and MilestoneApproved events', async () => { + const { escrow, freelancer, stranger } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await time.increase(AUTO_APPROVE_TIMEOUT); + + const tx = escrow.connect(stranger).autoApprove(0); + + await expect(tx).to.emit(escrow, 'MilestoneAutoApproved'); + await expect(tx) + .to.emit(escrow, 'MilestoneApproved') + .withArgs(0, ethers.ZeroAddress, AMOUNT_PER_MILESTONE); + }); + + it('canAutoApprove returns false before timeout', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await time.increase(AUTO_APPROVE_TIMEOUT - 10); + + expect(await escrow.canAutoApprove(0)).to.be.false; + }); + + it('canAutoApprove returns true after timeout', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await time.increase(AUTO_APPROVE_TIMEOUT); + + expect(await escrow.canAutoApprove(0)).to.be.true; + }); + + it('canAutoApprove returns false if not completed', async () => { + const { escrow } = await loadFixture(deployEscrowFixture); + expect(await escrow.canAutoApprove(0)).to.be.false; + }); + + it('canAutoApprove returns false if already approved', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).approveMilestone(0); + await time.increase(AUTO_APPROVE_TIMEOUT); + + expect(await escrow.canAutoApprove(0)).to.be.false; + }); + + it('canAutoApprove returns false for out-of-range id', async () => { + const { escrow } = await loadFixture(deployEscrowFixture); + expect(await escrow.canAutoApprove(999)).to.be.false; + }); + + it('reverts on autoApprove of already-approved milestone', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).approveMilestone(0); + await time.increase(AUTO_APPROVE_TIMEOUT); + + await expect(escrow.connect(client).autoApprove(0)).to.be.revertedWith( + 'Already approved' + ); + }); + }); + + // ── cancel ───────────────────────────────────────────────────────────────── + describe('cancel', () => { + it('client can cancel and receives full refund (minus gas)', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + + const delta = await getBalanceDelta(client, async () => { + await escrow.connect(client).cancel(); + }); + + // Client receives TOTAL_AMOUNT minus a small amount of gas + expect(delta).to.be.gt(TOTAL_AMOUNT - ethers.parseEther('0.01')); + expect(delta).to.be.lte(TOTAL_AMOUNT); + }); + + it('contract balance reaches zero after cancel', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + await escrow.connect(client).cancel(); + + expect(await escrow.getRemainingBalance()).to.equal(0n); + }); + + it('sets cancelled flag to true', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + await escrow.connect(client).cancel(); + + expect(await escrow.cancelled()).to.be.true; + }); + + it('emits ContractCancelled event with correct refund amount', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + + await expect(escrow.connect(client).cancel()) + .to.emit(escrow, 'ContractCancelled') + .withArgs(client.address, TOTAL_AMOUNT, (v: bigint) => v > 0n); + }); + + it('refunds only the remaining balance after partial payouts', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + + // Approve one milestone first + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).approveMilestone(0); + + const remaining = TOTAL_AMOUNT - AMOUNT_PER_MILESTONE; + + await expect(escrow.connect(client).cancel()) + .to.emit(escrow, 'ContractCancelled') + .withArgs(client.address, remaining, (v: bigint) => v > 0n); + }); + + it('reverts when called by non-client', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + + await expect(escrow.connect(freelancer).cancel()).to.be.revertedWith( + 'Only client can cancel' + ); + }); + + it('reverts when already cancelled', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + await escrow.connect(client).cancel(); + + await expect(escrow.connect(client).cancel()).to.be.revertedWith( + 'Already cancelled' + ); + }); + }); + + // ── raiseDispute ─────────────────────────────────────────────────────────── + describe('raiseDispute', () => { + it('client can raise a dispute on a completed milestone', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + await expect( + escrow.connect(client).raiseDispute(0, 'Work unsatisfactory') + ) + .to.emit(escrow, 'DisputeRaised') + .withArgs(0, client.address, 'Work unsatisfactory'); + }); + + it('freelancer can raise a dispute on a completed milestone', async () => { + const { escrow, freelancer } = await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + await expect( + escrow.connect(freelancer).raiseDispute(0, 'Payment withheld') + ) + .to.emit(escrow, 'DisputeRaised') + .withArgs(0, freelancer.address, 'Payment withheld'); + }); + + it('reverts when milestone is not completed', async () => { + const { escrow, client } = await loadFixture(deployEscrowFixture); + + await expect( + escrow.connect(client).raiseDispute(0, 'reason') + ).to.be.revertedWith('Not completed yet'); + }); + + it('reverts when milestone is already approved', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + await escrow.connect(client).approveMilestone(0); + + await expect( + escrow.connect(client).raiseDispute(0, 'reason') + ).to.be.revertedWith('Already approved'); + }); + + it('reverts without reason when called by a stranger', async () => { + // The contract has a bare require(caller == client || caller == freelancer) + // with no message string, so it reverts without reason. + const { escrow, freelancer, stranger } = + await loadFixture(deployEscrowFixture); + await escrow.connect(freelancer).markCompleted(0); + + await expect( + escrow.connect(stranger).raiseDispute(0, 'reason') + ).to.be.revertedWithoutReason(ethers); + }); + }); + + // ── View helpers ─────────────────────────────────────────────────────────── + describe('View helpers', () => { + it('isFullyReleased returns false initially', async () => { + const { escrow } = await loadFixture(deployEscrowFixture); + expect(await escrow.isFullyReleased()).to.be.false; + }); + + it('isFullyReleased returns true after all milestones approved', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + + for (let i = 0; i < MILESTONE_COUNT; i++) { + await completeAndApprove(escrow, freelancer, client, i); + } + + expect(await escrow.isFullyReleased()).to.be.true; + }); + + it('getRemainingBalance decreases with each milestone release', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + + for (let i = 0; i < MILESTONE_COUNT; i++) { + await completeAndApprove(escrow, freelancer, client, i); + const expected = TOTAL_AMOUNT - AMOUNT_PER_MILESTONE * BigInt(i + 1); + expect(await escrow.getRemainingBalance()).to.equal(expected); + } + }); + }); + + // ── Full happy-path flow ─────────────────────────────────────────────────── + describe('Full happy-path flow', () => { + it('processes all milestones and leaves zero balance', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + + for (let i = 0; i < MILESTONE_COUNT; i++) { + await escrow.connect(freelancer).markCompleted(i); + await escrow.connect(client).approveMilestone(i); + } + + expect(await escrow.getRemainingBalance()).to.equal(0n); + expect(await escrow.isFullyReleased()).to.be.true; + expect(await escrow.releasedMilestones()).to.equal(MILESTONE_COUNT); + }); + + it('freelancer total payout equals total locked amount', async () => { + const { escrow, client, freelancer } = + await loadFixture(deployEscrowFixture); + + let totalPaid = 0n; + + for (let i = 0; i < MILESTONE_COUNT; i++) { + await escrow.connect(freelancer).markCompleted(i); + + const before = await ethers.provider.getBalance(freelancer.address); + await escrow.connect(client).approveMilestone(i); + const after = await ethers.provider.getBalance(freelancer.address); + + totalPaid += after - before; + } + + expect(totalPaid).to.equal(TOTAL_AMOUNT); + }); + }); +}); diff --git a/assignments/Class/tsconfig.json b/assignments/Class/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/Class/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} diff --git a/assignments/EscrowAssigmnent/.gitignore b/assignments/EscrowAssigmnent/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/EscrowAssigmnent/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/assignments/EscrowAssigmnent/README.md b/assignments/EscrowAssigmnent/README.md new file mode 100644 index 00000000..c80d3062 --- /dev/null +++ b/assignments/EscrowAssigmnent/README.md @@ -0,0 +1,57 @@ +## Escrow Smart Contract + +A secure on-chain escrow contract that facilitates trustless transactions between a buyer and a seller, with an escrow agent acting as a neutral mediator. +The contract ensures funds are only released when delivery is confirmed or disputes are resolved, with optional automatic release after a timeout. + +### 📌 Overview + +This escrow system enables: +Secure ETH deposits by a buyer +Seller delivery confirmation +Controlled fund release by an escrow agent +Dispute handling and resolution +Automatic release after a timeout +Refunds when necessary +It is suitable for freelance work, marketplace transactions, and peer-to-peer commerce. + +### 👥 Roles + +- Buyer + Deposits funds into escrow + Can raise disputes +- Seller + Confirms delivery + Can raise disputes +- Escrow Agent + Deployer of the contract + Releases funds + Issues refunds + Resolves disputes + Collects escrow fees + +### ⚙️ Escrow Lifecycle + +1️⃣ Awaiting Payment +Contract deployed +Buyer deposits ETH via deposit() +2️⃣ Awaiting Delivery +Seller delivers goods/services +Seller calls confirmDelivery() +3️⃣ Completion +Escrow agent releases funds +Seller receives payment +Agent receives fee +4️⃣ Refund +Agent refunds buyer if needed +5️⃣ Dispute +Buyer or seller raises dispute +Agent resolves in favor of either party +💰 Fees +Defined in basis points +100 = 1% +Max allowed: 10% (1000 basis points) +Collected only when seller is paid + +contract address: 0xf99919a45050f413A7BdbEF9D1E6450973E0935d +etherscan_link: +(etherscan_link)[https://eth-sepolia.blockscout.com/address/0xf99919a45050f413A7BdbEF9D1E6450973E0935d#code] diff --git a/assignments/EscrowAssigmnent/contracts/Escrow.sol b/assignments/EscrowAssigmnent/contracts/Escrow.sol new file mode 100644 index 00000000..10586b03 --- /dev/null +++ b/assignments/EscrowAssigmnent/contracts/Escrow.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract Escrow { + // State variables + address public buyer; + address public seller; + address public escrowAgent; + + uint256 public escrowAmount; + uint256 public immutable escrowFee; // Fee percentage (e.g., 100 = 1%) + uint256 public immutable disputeTimeout; // Time before auto-release + uint256 public depositTimestamp; + + bool public deliveryConfirmed; + bool public disputed; + + enum EscrowState { + AWAITING_PAYMENT, + AWAITING_DELIVERY, + COMPLETE, + REFUNDED, + DISPUTED + } + + EscrowState public escrowState; + + // Events for transparency + event Deposited(address indexed buyer, uint256 amount, uint256 timestamp); + event DeliveryConfirmed(address indexed seller, uint256 timestamp); + event FundsReleased(address indexed seller, uint256 amount, uint256 fee); + event RefundIssued(address indexed buyer, uint256 amount); + event DisputeRaised(address indexed initiator, uint256 timestamp); + event DisputeResolved(address indexed winner, uint256 amount); + + // Custom errors (gas efficient) + error Unauthorized(); + error InvalidState(); + error InvalidAmount(); + error TransferFailed(); + error DisputeActive(); + error TimeoutNotReached(); + + constructor( + address _buyer, + address _seller, + uint256 _escrowFee, // in basis points (100 = 1%) + uint256 _disputeTimeout // in seconds + ) { + require(_buyer != address(0) && _seller != address(0), "Invalid address"); + require(_buyer != _seller, "Buyer and seller must differ"); + require(_escrowFee <= 1000, "Fee too high"); // Max 10% + + buyer = _buyer; + seller = _seller; + escrowAgent = msg.sender; + escrowFee = _escrowFee; + disputeTimeout = _disputeTimeout; + escrowState = EscrowState.AWAITING_PAYMENT; + } + + // Modifiers + modifier onlyEscrowAgent() { + if (msg.sender != escrowAgent) revert Unauthorized(); + _; + } + + modifier onlyBuyer() { + if (msg.sender != buyer) revert Unauthorized(); + _; + } + + modifier onlySeller() { + if (msg.sender != seller) revert Unauthorized(); + _; + } + + modifier inState(EscrowState _state) { + if (escrowState != _state) revert InvalidState(); + _; + } + + modifier noDispute() { + if (disputed) revert DisputeActive(); + _; + } + + /** + * @notice Buyer deposits funds into escrow + * @dev Only buyer can deposit, only once + */ + function deposit() external payable onlyBuyer inState(EscrowState.AWAITING_PAYMENT) { + if (msg.value == 0) revert InvalidAmount(); + + escrowAmount = msg.value; + depositTimestamp = block.timestamp; + escrowState = EscrowState.AWAITING_DELIVERY; + + emit Deposited(buyer, msg.value, block.timestamp); + } + + /** + * @notice Seller confirms delivery of goods/services + * @dev Sets flag that agent can use to release funds + */ + function confirmDelivery() external onlySeller inState(EscrowState.AWAITING_DELIVERY) noDispute { + deliveryConfirmed = true; + emit DeliveryConfirmed(seller, block.timestamp); + } + + /** + * @notice Escrow agent releases funds to seller + */ + function releaseFunds() external onlyEscrowAgent inState(EscrowState.AWAITING_DELIVERY) noDispute { + require(deliveryConfirmed, "Delivery not confirmed"); + + uint256 amount = escrowAmount; + uint256 fee = (amount * escrowFee) / 10000; + uint256 sellerAmount = amount - fee; + + escrowState = EscrowState.COMPLETE; + escrowAmount = 0; + + (bool successSeller,) = payable(seller).call{value: sellerAmount}(""); + if (!successSeller) revert TransferFailed(); + + if (fee > 0) { + (bool successAgent,) = payable(escrowAgent).call{value: fee}(""); + if (!successAgent) revert TransferFailed(); + } + + emit FundsReleased(seller, sellerAmount, fee); + } + + /** + * @notice Escrow agent refunds buyer + */ + function refundBuyer() external onlyEscrowAgent inState(EscrowState.AWAITING_DELIVERY) { + uint256 amount = escrowAmount; + + escrowState = EscrowState.REFUNDED; + escrowAmount = 0; + + (bool success,) = payable(buyer).call{value: amount}(""); + if (!success) revert TransferFailed(); + + emit RefundIssued(buyer, amount); + } + + /** + * @notice Either party can raise a dispute + * @dev Freezes the escrow until agent resolves + */ + function raiseDispute() external inState(EscrowState.AWAITING_DELIVERY) { + if (msg.sender != buyer && msg.sender != seller) revert Unauthorized(); + + disputed = true; + escrowState = EscrowState.DISPUTED; + + emit DisputeRaised(msg.sender, block.timestamp); + } + + /** + * @notice Escrow agent resolves dispute + * @param favorBuyer true to refund buyer, false to pay seller + */ + function resolveDispute(bool favorBuyer) external onlyEscrowAgent inState(EscrowState.DISPUTED) { + uint256 amount = escrowAmount; + address winner = favorBuyer ? buyer : seller; + + // Update state before external calls + escrowState = favorBuyer ? EscrowState.REFUNDED : EscrowState.COMPLETE; + escrowAmount = 0; + disputed = false; + + uint256 fee = favorBuyer ? 0 : (amount * escrowFee) / 10000; + uint256 payoutAmount = amount - fee; + + (bool success,) = payable(winner).call{value: payoutAmount}(""); + if (!success) revert TransferFailed(); + + if (fee > 0) { + (bool successFee,) = payable(escrowAgent).call{value: fee}(""); + if (!successFee) revert TransferFailed(); + } + + emit DisputeResolved(winner, payoutAmount); + } + + /** + * @notice Auto-release funds after timeout if no dispute + * @dev Prevents indefinite lock of funds + */ + function autoRelease() external inState(EscrowState.AWAITING_DELIVERY) noDispute { + if (block.timestamp < depositTimestamp + disputeTimeout) { + revert TimeoutNotReached(); + } + + uint256 amount = escrowAmount; + uint256 fee = (amount * escrowFee) / 10000; + uint256 sellerAmount = amount - fee; + + escrowState = EscrowState.COMPLETE; + escrowAmount = 0; + + (bool successSeller,) = payable(seller).call{value: sellerAmount}(""); + if (!successSeller) revert TransferFailed(); + + if (fee > 0) { + (bool successAgent,) = payable(escrowAgent).call{value: fee}(""); + if (!successAgent) revert TransferFailed(); + } + + emit FundsReleased(seller, sellerAmount, fee); + } + + /** + * @notice Get contract balance + */ + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @notice Check if timeout has been reached + */ + function isTimeoutReached() external view returns (bool) { + if (escrowState != EscrowState.AWAITING_DELIVERY) return false; + return block.timestamp >= depositTimestamp + disputeTimeout; + } + + // Reject direct ETH transfers + receive() external payable { + revert("Use deposit() function"); + } +} diff --git a/assignments/EscrowAssigmnent/contracts/Escrow.t.sol b/assignments/EscrowAssigmnent/contracts/Escrow.t.sol new file mode 100644 index 00000000..650ae0de --- /dev/null +++ b/assignments/EscrowAssigmnent/contracts/Escrow.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "forge-std/Test.sol"; +import "../contracts/Escrow.sol"; + +/// @notice Minimal contract that can receive ETH +contract Receiver { + receive() external payable {} +} + +contract EscrowTest is Test { + Escrow escrow; + + address buyer; + Receiver seller; // Use a contract so it can receive ETH + address escrowAgent; // This test contract acts as the agent + + uint256 escrowFee = 100; // 1% + uint256 disputeTimeout = 1 days; + receive() external payable {} + + function setUp() public { + // Create buyer EOA + buyer = vm.addr(1); + vm.deal(buyer, 10 ether); + + // Deploy seller as Receiver contract + seller = new Receiver(); + + // Escrow agent is this test contract + escrowAgent = address(this); + + // Deploy Escrow contract with proper addresses + escrow = new Escrow(buyer, address(seller), escrowFee, disputeTimeout); + } + + /// @notice Test buyer deposit + function testDeposit() public { + vm.prank(buyer); + escrow.deposit{value: 5 ether}(); + + assertEq(escrow.escrowAmount(), 5 ether); + assertEq(uint256(escrow.escrowState()), uint256(Escrow.EscrowState.AWAITING_DELIVERY)); + } + + /// @notice Test that only buyer can deposit + function testDepositUnauthorized() public { + vm.expectRevert(Escrow.Unauthorized.selector); + escrow.deposit{value: 1 ether}(); // caller is this contract, not buyer + } + + /// @notice Test delivery confirmation + function testConfirmDelivery() public { + vm.prank(buyer); + escrow.deposit{value: 5 ether}(); + + vm.prank(address(seller)); + escrow.confirmDelivery(); + + assertTrue(escrow.deliveryConfirmed()); + } + + /// @notice Test that only seller can confirm delivery + function testConfirmDeliveryUnauthorized() public { + vm.prank(buyer); + escrow.deposit{value: 5 ether}(); + + vm.expectRevert(Escrow.Unauthorized.selector); + escrow.confirmDelivery(); // caller is test contract + } + + /// @notice Test release funds to seller + function testReleaseFunds() public { + vm.prank(buyer); + escrow.deposit{value: 10 ether}(); + + vm.prank(address(seller)); + escrow.confirmDelivery(); + + uint256 sellerBalanceBefore = address(seller).balance; + uint256 agentBalanceBefore = address(this).balance; + + uint256 fee = (10 ether * escrowFee) / 10000; + uint256 sellerAmount = 10 ether - fee; + + escrow.releaseFunds(); + + assertEq(address(seller).balance, sellerBalanceBefore + sellerAmount); + assertEq(address(this).balance, agentBalanceBefore + fee); + assertEq(uint256(escrow.escrowState()), uint256(Escrow.EscrowState.COMPLETE)); + } + + /// @notice Test raising and resolving dispute + function testRaiseAndResolveDispute() public { + vm.prank(buyer); + escrow.deposit{value: 10 ether}(); + + // Raise dispute + vm.prank(buyer); + escrow.raiseDispute(); + + assertEq(uint256(escrow.escrowState()), uint256(Escrow.EscrowState.DISPUTED)); + assertTrue(escrow.disputed()); + + // Resolve dispute in favor of buyer + uint256 buyerBalanceBefore = buyer.balance; + escrow.resolveDispute(true); + + assertEq(buyer.balance, buyerBalanceBefore + 10 ether); + assertEq(uint256(escrow.escrowState()), uint256(Escrow.EscrowState.REFUNDED)); + assertFalse(escrow.disputed()); + } + + /// @notice Test auto-release after timeout + function testAutoRelease() public { + vm.prank(buyer); + escrow.deposit{value: 10 ether}(); + + vm.prank(address(seller)); + escrow.confirmDelivery(); + + // Move time forward past disputeTimeout + vm.warp(block.timestamp + disputeTimeout + 1); + + uint256 sellerBalanceBefore = address(seller).balance; + uint256 agentBalanceBefore = address(this).balance; + + uint256 fee = (10 ether * escrowFee) / 10000; + uint256 sellerAmount = 10 ether - fee; + + escrow.autoRelease(); + + assertEq(address(seller).balance, sellerBalanceBefore + sellerAmount); + assertEq(address(this).balance, agentBalanceBefore + fee); + assertEq(uint256(escrow.escrowState()), uint256(Escrow.EscrowState.COMPLETE)); + } + + /// @notice Test refund buyer + function testRefundBuyer() public { + vm.prank(buyer); + escrow.deposit{value: 5 ether}(); + + uint256 buyerBalanceBefore = buyer.balance; + + escrow.refundBuyer(); + + assertEq(buyer.balance, buyerBalanceBefore + 5 ether); + assertEq(uint256(escrow.escrowState()), uint256(Escrow.EscrowState.REFUNDED)); + } +} diff --git a/assignments/EscrowAssigmnent/hardhat.config.ts b/assignments/EscrowAssigmnent/hardhat.config.ts new file mode 100644 index 00000000..64f25861 --- /dev/null +++ b/assignments/EscrowAssigmnent/hardhat.config.ts @@ -0,0 +1,44 @@ +import hardhatToolboxMochaEthersPlugin from '@nomicfoundation/hardhat-toolbox-mocha-ethers'; +import { configVariable, defineConfig } from 'hardhat/config'; +import 'dotenv/config'; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: '0.8.28', + }, + production: { + version: '0.8.28', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: 'edr-simulated', + chainType: 'l1', + }, + hardhatOp: { + type: 'edr-simulated', + chainType: 'op', + }, + sepolia: { + type: 'http', + chainType: 'l1', + url: configVariable('SEPOLIA_RPC_URL'), + accounts: [configVariable('SEPOLIA_PRIVATE_KEY')], + }, + }, + verify: { + etherscan: { + apiKey: configVariable('ETHERSCAN_API_KEY'), + }, + }, +}); diff --git a/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/artifacts/EscrowModule#Escrow.json b/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/artifacts/EscrowModule#Escrow.json new file mode 100644 index 00000000..f6d73416 --- /dev/null +++ b/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/artifacts/EscrowModule#Escrow.json @@ -0,0 +1,444 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "Escrow", + "sourceName": "contracts/Escrow.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_buyer", + "type": "address" + }, + { + "internalType": "address", + "name": "_seller", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_escrowFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_disputeTimeout", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "DisputeActive", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidState", + "type": "error" + }, + { + "inputs": [], + "name": "TimeoutNotReached", + "type": "error" + }, + { + "inputs": [], + "name": "TransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "seller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "DeliveryConfirmed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "initiator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "DisputeRaised", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "winner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DisputeResolved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "seller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "FundsReleased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RefundIssued", + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "fallback" + }, + { + "inputs": [], + "name": "autoRelease", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "buyer", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "confirmDelivery", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "deliveryConfirmed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "depositTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "disputeTimeout", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "disputed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "escrowAgent", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "escrowAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "escrowFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "escrowState", + "outputs": [ + { + "internalType": "enum Escrow.EscrowState", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isTimeoutReached", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "raiseDispute", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "refundBuyer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "releaseFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "favorBuyer", + "type": "bool" + } + ], + "name": "resolveDispute", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "seller", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bytecode": "0x60c060405234801561000f575f5ffd5b506040516110c73803806110c783398101604081905261002e916101a1565b6001600160a01b0384161580159061004e57506001600160a01b03831615155b6100915760405162461bcd60e51b815260206004820152600f60248201526e496e76616c6964206164647265737360881b60448201526064015b60405180910390fd5b826001600160a01b0316846001600160a01b0316036100f25760405162461bcd60e51b815260206004820152601c60248201527f427579657220616e642073656c6c6572206d75737420646966666572000000006044820152606401610088565b6103e88211156101335760405162461bcd60e51b815260206004820152600c60248201526b08ccaca40e8dede40d0d2ced60a31b6044820152606401610088565b5f80546001600160a01b039586166001600160a01b03199182161790915560018054949095169381169390931790935560028054909216331790915560805260a0526005805462ff0000191690556101e1565b80516001600160a01b038116811461019c575f5ffd5b919050565b5f5f5f5f608085870312156101b4575f5ffd5b6101bd85610186565b93506101cb60208601610186565b6040860151606090960151949790965092505050565b60805160a051610ea961021e5f395f818161037601528181610acd0152610d6901525f81816102b0015281816105b401526108ea0152610ea95ff3fe608060405260043610610117575f3560e01c80637150d8ae1161009f578063c04c22a411610063578063c04c22a414610365578063d0e30db014610398578063e1f6c098146103a0578063e8a61cc8146103bf578063e908154e146103d357610162565b80637150d8ae146102e657806389e1e82a146103045780639317190014610323578063a36693b11461033c578063a58541231461035057610162565b806347a7846f116100e657806347a7846f146102605780635e10177b1461027557806369d895751461028b5780636a5b428b1461029f5780636daa2d44146102d257610162565b80630695c46c146101ae57806308551a53146101e157806312065fe0146102185780631b6701911461023457610162565b366101625760405162461bcd60e51b81526020600482015260166024820152752ab9b2903232b837b9b4ba141490333ab731ba34b7b760511b60448201526064015b60405180910390fd5b34801561016d575f5ffd5b5060405162461bcd60e51b8152602060048201526015602482015274125b9d985b1a5908199d5b98dd1a5bdb8818d85b1b605a1b6044820152606401610159565b3480156101b9575f5ffd5b506005546101cc90610100900460ff1681565b60405190151581526020015b60405180910390f35b3480156101ec575f5ffd5b50600154610200906001600160a01b031681565b6040516001600160a01b0390911681526020016101d8565b348015610223575f5ffd5b50475b6040519081526020016101d8565b34801561023f575f5ffd5b506005546102539062010000900460ff1681565b6040516101d89190610db1565b34801561026b575f5ffd5b5061022660035481565b348015610280575f5ffd5b506102896103e7565b005b348015610296575f5ffd5b506102896104cc565b3480156102aa575f5ffd5b506102267f000000000000000000000000000000000000000000000000000000000000000081565b3480156102dd575f5ffd5b50610289610745565b3480156102f1575f5ffd5b505f54610200906001600160a01b031681565b34801561030f575f5ffd5b5061028961031e366004610dd7565b610808565b34801561032e575f5ffd5b506005546101cc9060ff1681565b348015610347575f5ffd5b50610289610a64565b34801561035b575f5ffd5b5061022660045481565b348015610370575f5ffd5b506102267f000000000000000000000000000000000000000000000000000000000000000081565b610289610b19565b3480156103ab575f5ffd5b50600254610200906001600160a01b031681565b3480156103ca575f5ffd5b50610289610c02565b3480156103de575f5ffd5b506101cc610d3d565b6001546001600160a01b03163314610411576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff16600481111561043157610431610d9d565b1461044f5760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff161561047857604051637c9d2ba760e01b815260040160405180910390fd5b6005805460ff19166001908117909155546040514281526001600160a01b03909116907f28720c586a6e736146008f9bf260b09f29b8dcc37314fed637613ea3aec614f8906020015b60405180910390a250565b6002546001600160a01b031633146104f6576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff16600481111561051657610516610d9d565b146105345760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff161561055d57604051637c9d2ba760e01b815260040160405180910390fd5b60055460ff166105a85760405162461bcd60e51b815260206004820152601660248201527511195b1a5d995c9e481b9bdd0818dbdb999a5c9b595960521b6044820152606401610159565b6003545f6127106105d97f000000000000000000000000000000000000000000000000000000000000000084610e11565b6105e39190610e2e565b90505f6105f08284610e4d565b6005805462ff00001916620200001790555f600381905560015460405192935090916001600160a01b039091169083908381818185875af1925050503d805f8114610656576040519150601f19603f3d011682016040523d82523d5f602084013e61065b565b606091505b505090508061067d576040516312171d8360e31b815260040160405180910390fd5b82156106f6576002546040515f916001600160a01b03169085908381818185875af1925050503d805f81146106cd576040519150601f19603f3d011682016040523d82523d5f602084013e6106d2565b606091505b50509050806106f4576040516312171d8360e31b815260040160405180910390fd5b505b60015460408051848152602081018690526001600160a01b03909216917fc90c76155d237557f9162f07cd0da2ff40418cd82028c0e89535270fb9de5104910160405180910390a25050505050565b60018060055462010000900460ff16600481111561076557610765610d9d565b146107835760405163baf3f0f760e01b815260040160405180910390fd5b5f546001600160a01b031633148015906107a857506001546001600160a01b03163314155b156107c5576040516282b42960e81b815260040160405180910390fd5b6005805462ffff0019166204010017905560405142815233907fbd189a57edd98c9c61d4dd3cf0d28749a2cdda08f9bc354f8864830e04520824906020016104c1565b6002546001600160a01b03163314610832576040516282b42960e81b815260040160405180910390fd5b60048060055462010000900460ff16600481111561085257610852610d9d565b146108705760405163baf3f0f760e01b815260040160405180910390fd5b6003545f8361088a576001546001600160a01b0316610896565b5f546001600160a01b03165b9050836108a45760026108a7565b60035b6005805462ff00001916620100008360048111156108c7576108c7610d9d565b02179055505f60038190556005805461ff00191690558461091e5761271061090f7f000000000000000000000000000000000000000000000000000000000000000085610e11565b6109199190610e2e565b610920565b5f5b90505f61092d8285610e4d565b90505f836001600160a01b0316826040515f6040518083038185875af1925050503d805f8114610978576040519150601f19603f3d011682016040523d82523d5f602084013e61097d565b606091505b505090508061099f576040516312171d8360e31b815260040160405180910390fd5b8215610a18576002546040515f916001600160a01b03169085908381818185875af1925050503d805f81146109ef576040519150601f19603f3d011682016040523d82523d5f602084013e6109f4565b606091505b5050905080610a16576040516312171d8360e31b815260040160405180910390fd5b505b836001600160a01b03167ff396531fad1d4dc2cd92386cdea800c92a507f8fe1bdb80313eb32a57481b1dc83604051610a5391815260200190565b60405180910390a250505050505050565b60018060055462010000900460ff166004811115610a8457610a84610d9d565b14610aa25760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff1615610acb57604051637c9d2ba760e01b815260040160405180910390fd5b7f0000000000000000000000000000000000000000000000000000000000000000600454610af99190610e60565b4210156105a8576040516326c015ab60e21b815260040160405180910390fd5b5f546001600160a01b03163314610b42576040516282b42960e81b815260040160405180910390fd5b5f8060055462010000900460ff166004811115610b6157610b61610d9d565b14610b7f5760405163baf3f0f760e01b815260040160405180910390fd5b345f03610b9f5760405163162908e360e11b815260040160405180910390fd5b3460035542600455600580546001919062ff00001916620100008302179055505f54604080513481524260208201526001600160a01b03909216917f73a19dd210f1a7f902193214c0ee91dd35ee5b4d920cba8d519eca65a7b488ca91016104c1565b6002546001600160a01b03163314610c2c576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff166004811115610c4c57610c4c610d9d565b14610c6a5760405163baf3f0f760e01b815260040160405180910390fd5b600380546005805462ff00001916620300001790555f9182905581546040519192916001600160a01b039091169083908381818185875af1925050503d805f8114610cd0576040519150601f19603f3d011682016040523d82523d5f602084013e610cd5565b606091505b5050905080610cf7576040516312171d8360e31b815260040160405180910390fd5b5f546040518381526001600160a01b03909116907fa171b6942063c6f2800ce40a780edce37baa2b618571b11eedd1e69e626e7d769060200160405180910390a2505050565b5f600160055462010000900460ff166004811115610d5d57610d5d610d9d565b14610d6757505f90565b7f0000000000000000000000000000000000000000000000000000000000000000600454610d959190610e60565b421015905090565b634e487b7160e01b5f52602160045260245ffd5b6020810160058310610dd157634e487b7160e01b5f52602160045260245ffd5b91905290565b5f60208284031215610de7575f5ffd5b81358015158114610df6575f5ffd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b8082028115828204841417610e2857610e28610dfd565b92915050565b5f82610e4857634e487b7160e01b5f52601260045260245ffd5b500490565b81810381811115610e2857610e28610dfd565b80820180821115610e2857610e28610dfd56fea264697066735822122088b2f8f45703bd2a0453aa74e4716302520c8eaff83d54e14aa9bef6becbd9e364736f6c634300081c0033", + "deployedBytecode": "0x608060405260043610610117575f3560e01c80637150d8ae1161009f578063c04c22a411610063578063c04c22a414610365578063d0e30db014610398578063e1f6c098146103a0578063e8a61cc8146103bf578063e908154e146103d357610162565b80637150d8ae146102e657806389e1e82a146103045780639317190014610323578063a36693b11461033c578063a58541231461035057610162565b806347a7846f116100e657806347a7846f146102605780635e10177b1461027557806369d895751461028b5780636a5b428b1461029f5780636daa2d44146102d257610162565b80630695c46c146101ae57806308551a53146101e157806312065fe0146102185780631b6701911461023457610162565b366101625760405162461bcd60e51b81526020600482015260166024820152752ab9b2903232b837b9b4ba141490333ab731ba34b7b760511b60448201526064015b60405180910390fd5b34801561016d575f5ffd5b5060405162461bcd60e51b8152602060048201526015602482015274125b9d985b1a5908199d5b98dd1a5bdb8818d85b1b605a1b6044820152606401610159565b3480156101b9575f5ffd5b506005546101cc90610100900460ff1681565b60405190151581526020015b60405180910390f35b3480156101ec575f5ffd5b50600154610200906001600160a01b031681565b6040516001600160a01b0390911681526020016101d8565b348015610223575f5ffd5b50475b6040519081526020016101d8565b34801561023f575f5ffd5b506005546102539062010000900460ff1681565b6040516101d89190610db1565b34801561026b575f5ffd5b5061022660035481565b348015610280575f5ffd5b506102896103e7565b005b348015610296575f5ffd5b506102896104cc565b3480156102aa575f5ffd5b506102267f000000000000000000000000000000000000000000000000000000000000000081565b3480156102dd575f5ffd5b50610289610745565b3480156102f1575f5ffd5b505f54610200906001600160a01b031681565b34801561030f575f5ffd5b5061028961031e366004610dd7565b610808565b34801561032e575f5ffd5b506005546101cc9060ff1681565b348015610347575f5ffd5b50610289610a64565b34801561035b575f5ffd5b5061022660045481565b348015610370575f5ffd5b506102267f000000000000000000000000000000000000000000000000000000000000000081565b610289610b19565b3480156103ab575f5ffd5b50600254610200906001600160a01b031681565b3480156103ca575f5ffd5b50610289610c02565b3480156103de575f5ffd5b506101cc610d3d565b6001546001600160a01b03163314610411576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff16600481111561043157610431610d9d565b1461044f5760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff161561047857604051637c9d2ba760e01b815260040160405180910390fd5b6005805460ff19166001908117909155546040514281526001600160a01b03909116907f28720c586a6e736146008f9bf260b09f29b8dcc37314fed637613ea3aec614f8906020015b60405180910390a250565b6002546001600160a01b031633146104f6576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff16600481111561051657610516610d9d565b146105345760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff161561055d57604051637c9d2ba760e01b815260040160405180910390fd5b60055460ff166105a85760405162461bcd60e51b815260206004820152601660248201527511195b1a5d995c9e481b9bdd0818dbdb999a5c9b595960521b6044820152606401610159565b6003545f6127106105d97f000000000000000000000000000000000000000000000000000000000000000084610e11565b6105e39190610e2e565b90505f6105f08284610e4d565b6005805462ff00001916620200001790555f600381905560015460405192935090916001600160a01b039091169083908381818185875af1925050503d805f8114610656576040519150601f19603f3d011682016040523d82523d5f602084013e61065b565b606091505b505090508061067d576040516312171d8360e31b815260040160405180910390fd5b82156106f6576002546040515f916001600160a01b03169085908381818185875af1925050503d805f81146106cd576040519150601f19603f3d011682016040523d82523d5f602084013e6106d2565b606091505b50509050806106f4576040516312171d8360e31b815260040160405180910390fd5b505b60015460408051848152602081018690526001600160a01b03909216917fc90c76155d237557f9162f07cd0da2ff40418cd82028c0e89535270fb9de5104910160405180910390a25050505050565b60018060055462010000900460ff16600481111561076557610765610d9d565b146107835760405163baf3f0f760e01b815260040160405180910390fd5b5f546001600160a01b031633148015906107a857506001546001600160a01b03163314155b156107c5576040516282b42960e81b815260040160405180910390fd5b6005805462ffff0019166204010017905560405142815233907fbd189a57edd98c9c61d4dd3cf0d28749a2cdda08f9bc354f8864830e04520824906020016104c1565b6002546001600160a01b03163314610832576040516282b42960e81b815260040160405180910390fd5b60048060055462010000900460ff16600481111561085257610852610d9d565b146108705760405163baf3f0f760e01b815260040160405180910390fd5b6003545f8361088a576001546001600160a01b0316610896565b5f546001600160a01b03165b9050836108a45760026108a7565b60035b6005805462ff00001916620100008360048111156108c7576108c7610d9d565b02179055505f60038190556005805461ff00191690558461091e5761271061090f7f000000000000000000000000000000000000000000000000000000000000000085610e11565b6109199190610e2e565b610920565b5f5b90505f61092d8285610e4d565b90505f836001600160a01b0316826040515f6040518083038185875af1925050503d805f8114610978576040519150601f19603f3d011682016040523d82523d5f602084013e61097d565b606091505b505090508061099f576040516312171d8360e31b815260040160405180910390fd5b8215610a18576002546040515f916001600160a01b03169085908381818185875af1925050503d805f81146109ef576040519150601f19603f3d011682016040523d82523d5f602084013e6109f4565b606091505b5050905080610a16576040516312171d8360e31b815260040160405180910390fd5b505b836001600160a01b03167ff396531fad1d4dc2cd92386cdea800c92a507f8fe1bdb80313eb32a57481b1dc83604051610a5391815260200190565b60405180910390a250505050505050565b60018060055462010000900460ff166004811115610a8457610a84610d9d565b14610aa25760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff1615610acb57604051637c9d2ba760e01b815260040160405180910390fd5b7f0000000000000000000000000000000000000000000000000000000000000000600454610af99190610e60565b4210156105a8576040516326c015ab60e21b815260040160405180910390fd5b5f546001600160a01b03163314610b42576040516282b42960e81b815260040160405180910390fd5b5f8060055462010000900460ff166004811115610b6157610b61610d9d565b14610b7f5760405163baf3f0f760e01b815260040160405180910390fd5b345f03610b9f5760405163162908e360e11b815260040160405180910390fd5b3460035542600455600580546001919062ff00001916620100008302179055505f54604080513481524260208201526001600160a01b03909216917f73a19dd210f1a7f902193214c0ee91dd35ee5b4d920cba8d519eca65a7b488ca91016104c1565b6002546001600160a01b03163314610c2c576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff166004811115610c4c57610c4c610d9d565b14610c6a5760405163baf3f0f760e01b815260040160405180910390fd5b600380546005805462ff00001916620300001790555f9182905581546040519192916001600160a01b039091169083908381818185875af1925050503d805f8114610cd0576040519150601f19603f3d011682016040523d82523d5f602084013e610cd5565b606091505b5050905080610cf7576040516312171d8360e31b815260040160405180910390fd5b5f546040518381526001600160a01b03909116907fa171b6942063c6f2800ce40a780edce37baa2b618571b11eedd1e69e626e7d769060200160405180910390a2505050565b5f600160055462010000900460ff166004811115610d5d57610d5d610d9d565b14610d6757505f90565b7f0000000000000000000000000000000000000000000000000000000000000000600454610d959190610e60565b421015905090565b634e487b7160e01b5f52602160045260245ffd5b6020810160058310610dd157634e487b7160e01b5f52602160045260245ffd5b91905290565b5f60208284031215610de7575f5ffd5b81358015158114610df6575f5ffd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b8082028115828204841417610e2857610e28610dfd565b92915050565b5f82610e4857634e487b7160e01b5f52601260045260245ffd5b500490565b81810381811115610e2857610e28610dfd565b80820180821115610e2857610e28610dfd56fea264697066735822122088b2f8f45703bd2a0453aa74e4716302520c8eaff83d54e14aa9bef6becbd9e364736f6c634300081c0033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": { + "11": [ + { + "length": 32, + "start": 688 + }, + { + "length": 32, + "start": 1460 + }, + { + "length": 32, + "start": 2282 + } + ], + "13": [ + { + "length": 32, + "start": 886 + }, + { + "length": 32, + "start": 2765 + }, + { + "length": 32, + "start": 3433 + } + ] + }, + "inputSourceName": "project/contracts/Escrow.sol", + "buildInfoId": "solc-0_8_28-6032aaa0c3542719223e32aa44963126d1e0fc2b" +} \ No newline at end of file diff --git a/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/deployed_addresses.json b/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/deployed_addresses.json new file mode 100644 index 00000000..6a99c518 --- /dev/null +++ b/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/deployed_addresses.json @@ -0,0 +1,3 @@ +{ + "EscrowModule#Escrow": "0xf99919a45050f413A7BdbEF9D1E6450973E0935d" +} diff --git a/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/journal.jsonl b/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/journal.jsonl new file mode 100644 index 00000000..7bc499a6 --- /dev/null +++ b/assignments/EscrowAssigmnent/ignition/deployments/chain-11155111/journal.jsonl @@ -0,0 +1,8 @@ + +{"chainId":11155111,"type":"DEPLOYMENT_INITIALIZE"} +{"artifactId":"EscrowModule#Escrow","constructorArgs":["0x4E1B1d9Af926e7e0FBCb9C5b23EEda9d80642b99","0x910e783EaCbdC2453827B443Bf125Bac4B76E5C2",{"_kind":"bigint","value":"100"},7200],"contractName":"Escrow","dependencies":[],"from":"0x4e1b1d9af926e7e0fbcb9c5b23eeda9d80642b99","futureId":"EscrowModule#Escrow","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"0"}} +{"futureId":"EscrowModule#Escrow","networkInteraction":{"data":"0x60c060405234801561000f575f5ffd5b506040516110c73803806110c783398101604081905261002e916101a1565b6001600160a01b0384161580159061004e57506001600160a01b03831615155b6100915760405162461bcd60e51b815260206004820152600f60248201526e496e76616c6964206164647265737360881b60448201526064015b60405180910390fd5b826001600160a01b0316846001600160a01b0316036100f25760405162461bcd60e51b815260206004820152601c60248201527f427579657220616e642073656c6c6572206d75737420646966666572000000006044820152606401610088565b6103e88211156101335760405162461bcd60e51b815260206004820152600c60248201526b08ccaca40e8dede40d0d2ced60a31b6044820152606401610088565b5f80546001600160a01b039586166001600160a01b03199182161790915560018054949095169381169390931790935560028054909216331790915560805260a0526005805462ff0000191690556101e1565b80516001600160a01b038116811461019c575f5ffd5b919050565b5f5f5f5f608085870312156101b4575f5ffd5b6101bd85610186565b93506101cb60208601610186565b6040860151606090960151949790965092505050565b60805160a051610ea961021e5f395f818161037601528181610acd0152610d6901525f81816102b0015281816105b401526108ea0152610ea95ff3fe608060405260043610610117575f3560e01c80637150d8ae1161009f578063c04c22a411610063578063c04c22a414610365578063d0e30db014610398578063e1f6c098146103a0578063e8a61cc8146103bf578063e908154e146103d357610162565b80637150d8ae146102e657806389e1e82a146103045780639317190014610323578063a36693b11461033c578063a58541231461035057610162565b806347a7846f116100e657806347a7846f146102605780635e10177b1461027557806369d895751461028b5780636a5b428b1461029f5780636daa2d44146102d257610162565b80630695c46c146101ae57806308551a53146101e157806312065fe0146102185780631b6701911461023457610162565b366101625760405162461bcd60e51b81526020600482015260166024820152752ab9b2903232b837b9b4ba141490333ab731ba34b7b760511b60448201526064015b60405180910390fd5b34801561016d575f5ffd5b5060405162461bcd60e51b8152602060048201526015602482015274125b9d985b1a5908199d5b98dd1a5bdb8818d85b1b605a1b6044820152606401610159565b3480156101b9575f5ffd5b506005546101cc90610100900460ff1681565b60405190151581526020015b60405180910390f35b3480156101ec575f5ffd5b50600154610200906001600160a01b031681565b6040516001600160a01b0390911681526020016101d8565b348015610223575f5ffd5b50475b6040519081526020016101d8565b34801561023f575f5ffd5b506005546102539062010000900460ff1681565b6040516101d89190610db1565b34801561026b575f5ffd5b5061022660035481565b348015610280575f5ffd5b506102896103e7565b005b348015610296575f5ffd5b506102896104cc565b3480156102aa575f5ffd5b506102267f000000000000000000000000000000000000000000000000000000000000000081565b3480156102dd575f5ffd5b50610289610745565b3480156102f1575f5ffd5b505f54610200906001600160a01b031681565b34801561030f575f5ffd5b5061028961031e366004610dd7565b610808565b34801561032e575f5ffd5b506005546101cc9060ff1681565b348015610347575f5ffd5b50610289610a64565b34801561035b575f5ffd5b5061022660045481565b348015610370575f5ffd5b506102267f000000000000000000000000000000000000000000000000000000000000000081565b610289610b19565b3480156103ab575f5ffd5b50600254610200906001600160a01b031681565b3480156103ca575f5ffd5b50610289610c02565b3480156103de575f5ffd5b506101cc610d3d565b6001546001600160a01b03163314610411576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff16600481111561043157610431610d9d565b1461044f5760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff161561047857604051637c9d2ba760e01b815260040160405180910390fd5b6005805460ff19166001908117909155546040514281526001600160a01b03909116907f28720c586a6e736146008f9bf260b09f29b8dcc37314fed637613ea3aec614f8906020015b60405180910390a250565b6002546001600160a01b031633146104f6576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff16600481111561051657610516610d9d565b146105345760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff161561055d57604051637c9d2ba760e01b815260040160405180910390fd5b60055460ff166105a85760405162461bcd60e51b815260206004820152601660248201527511195b1a5d995c9e481b9bdd0818dbdb999a5c9b595960521b6044820152606401610159565b6003545f6127106105d97f000000000000000000000000000000000000000000000000000000000000000084610e11565b6105e39190610e2e565b90505f6105f08284610e4d565b6005805462ff00001916620200001790555f600381905560015460405192935090916001600160a01b039091169083908381818185875af1925050503d805f8114610656576040519150601f19603f3d011682016040523d82523d5f602084013e61065b565b606091505b505090508061067d576040516312171d8360e31b815260040160405180910390fd5b82156106f6576002546040515f916001600160a01b03169085908381818185875af1925050503d805f81146106cd576040519150601f19603f3d011682016040523d82523d5f602084013e6106d2565b606091505b50509050806106f4576040516312171d8360e31b815260040160405180910390fd5b505b60015460408051848152602081018690526001600160a01b03909216917fc90c76155d237557f9162f07cd0da2ff40418cd82028c0e89535270fb9de5104910160405180910390a25050505050565b60018060055462010000900460ff16600481111561076557610765610d9d565b146107835760405163baf3f0f760e01b815260040160405180910390fd5b5f546001600160a01b031633148015906107a857506001546001600160a01b03163314155b156107c5576040516282b42960e81b815260040160405180910390fd5b6005805462ffff0019166204010017905560405142815233907fbd189a57edd98c9c61d4dd3cf0d28749a2cdda08f9bc354f8864830e04520824906020016104c1565b6002546001600160a01b03163314610832576040516282b42960e81b815260040160405180910390fd5b60048060055462010000900460ff16600481111561085257610852610d9d565b146108705760405163baf3f0f760e01b815260040160405180910390fd5b6003545f8361088a576001546001600160a01b0316610896565b5f546001600160a01b03165b9050836108a45760026108a7565b60035b6005805462ff00001916620100008360048111156108c7576108c7610d9d565b02179055505f60038190556005805461ff00191690558461091e5761271061090f7f000000000000000000000000000000000000000000000000000000000000000085610e11565b6109199190610e2e565b610920565b5f5b90505f61092d8285610e4d565b90505f836001600160a01b0316826040515f6040518083038185875af1925050503d805f8114610978576040519150601f19603f3d011682016040523d82523d5f602084013e61097d565b606091505b505090508061099f576040516312171d8360e31b815260040160405180910390fd5b8215610a18576002546040515f916001600160a01b03169085908381818185875af1925050503d805f81146109ef576040519150601f19603f3d011682016040523d82523d5f602084013e6109f4565b606091505b5050905080610a16576040516312171d8360e31b815260040160405180910390fd5b505b836001600160a01b03167ff396531fad1d4dc2cd92386cdea800c92a507f8fe1bdb80313eb32a57481b1dc83604051610a5391815260200190565b60405180910390a250505050505050565b60018060055462010000900460ff166004811115610a8457610a84610d9d565b14610aa25760405163baf3f0f760e01b815260040160405180910390fd5b600554610100900460ff1615610acb57604051637c9d2ba760e01b815260040160405180910390fd5b7f0000000000000000000000000000000000000000000000000000000000000000600454610af99190610e60565b4210156105a8576040516326c015ab60e21b815260040160405180910390fd5b5f546001600160a01b03163314610b42576040516282b42960e81b815260040160405180910390fd5b5f8060055462010000900460ff166004811115610b6157610b61610d9d565b14610b7f5760405163baf3f0f760e01b815260040160405180910390fd5b345f03610b9f5760405163162908e360e11b815260040160405180910390fd5b3460035542600455600580546001919062ff00001916620100008302179055505f54604080513481524260208201526001600160a01b03909216917f73a19dd210f1a7f902193214c0ee91dd35ee5b4d920cba8d519eca65a7b488ca91016104c1565b6002546001600160a01b03163314610c2c576040516282b42960e81b815260040160405180910390fd5b60018060055462010000900460ff166004811115610c4c57610c4c610d9d565b14610c6a5760405163baf3f0f760e01b815260040160405180910390fd5b600380546005805462ff00001916620300001790555f9182905581546040519192916001600160a01b039091169083908381818185875af1925050503d805f8114610cd0576040519150601f19603f3d011682016040523d82523d5f602084013e610cd5565b606091505b5050905080610cf7576040516312171d8360e31b815260040160405180910390fd5b5f546040518381526001600160a01b03909116907fa171b6942063c6f2800ce40a780edce37baa2b618571b11eedd1e69e626e7d769060200160405180910390a2505050565b5f600160055462010000900460ff166004811115610d5d57610d5d610d9d565b14610d6757505f90565b7f0000000000000000000000000000000000000000000000000000000000000000600454610d959190610e60565b421015905090565b634e487b7160e01b5f52602160045260245ffd5b6020810160058310610dd157634e487b7160e01b5f52602160045260245ffd5b91905290565b5f60208284031215610de7575f5ffd5b81358015158114610df6575f5ffd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b8082028115828204841417610e2857610e28610dfd565b92915050565b5f82610e4857634e487b7160e01b5f52601260045260245ffd5b500490565b81810381811115610e2857610e28610dfd565b80820180821115610e2857610e28610dfd56fea264697066735822122088b2f8f45703bd2a0453aa74e4716302520c8eaff83d54e14aa9bef6becbd9e364736f6c634300081c00330000000000000000000000004e1b1d9af926e7e0fbcb9c5b23eeda9d80642b99000000000000000000000000910e783eacbdc2453827b443bf125bac4b76e5c200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000001c20","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"0"}},"type":"NETWORK_INTERACTION_REQUEST"} +{"futureId":"EscrowModule#Escrow","networkInteractionId":1,"nonce":17,"type":"TRANSACTION_PREPARE_SEND"} +{"futureId":"EscrowModule#Escrow","networkInteractionId":1,"nonce":17,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"4233255382"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000"}},"hash":"0x2bc2fa006bac3f2b5482257aa82e266750c96eb18ec5a61ee74d07d7aa6315ed"},"type":"TRANSACTION_SEND"} +{"futureId":"EscrowModule#Escrow","hash":"0x2bc2fa006bac3f2b5482257aa82e266750c96eb18ec5a61ee74d07d7aa6315ed","networkInteractionId":1,"receipt":{"blockHash":"0xde557a021b8be6653dd121df73a1ee63a1594cc6ffbe6b557d137a743d40cdd2","blockNumber":10285127,"contractAddress":"0xf99919a45050f413A7BdbEF9D1E6450973E0935d","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} +{"futureId":"EscrowModule#Escrow","result":{"address":"0xf99919a45050f413A7BdbEF9D1E6450973E0935d","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} \ No newline at end of file diff --git a/assignments/EscrowAssigmnent/ignition/modules/Escrow.ts b/assignments/EscrowAssigmnent/ignition/modules/Escrow.ts new file mode 100644 index 00000000..12f55ee6 --- /dev/null +++ b/assignments/EscrowAssigmnent/ignition/modules/Escrow.ts @@ -0,0 +1,12 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; + +export default buildModule('EscrowModule', (m) => { + const escrow = m.contract('Escrow', [ + '0x4E1B1d9Af926e7e0FBCb9C5b23EEda9d80642b99', + '0x910e783EaCbdC2453827B443Bf125Bac4B76E5C2', + BigInt(100), + 7200, + ]); + + return { escrow }; +}); diff --git a/assignments/EscrowAssigmnent/package.json b/assignments/EscrowAssigmnent/package.json new file mode 100644 index 00000000..fdef36e8 --- /dev/null +++ b/assignments/EscrowAssigmnent/package.json @@ -0,0 +1,23 @@ +{ + "name": "EscrowAssigmnent", + "version": "1.0.0", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.8", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.6", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + }, + "type": "module", + "dependencies": { + "dotenv": "^17.3.1" + } +} diff --git a/assignments/EscrowAssigmnent/scripts/send-op-tx.ts b/assignments/EscrowAssigmnent/scripts/send-op-tx.ts new file mode 100644 index 00000000..681a76e0 --- /dev/null +++ b/assignments/EscrowAssigmnent/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from 'hardhat'; + +const { ethers } = await network.connect({ + network: 'hardhatOp', + chainType: 'op', +}); + +console.log('Sending transaction using the OP chain type'); + +const [sender] = await ethers.getSigners(); + +console.log('Sending 1 wei from', sender.address, 'to itself'); + +console.log('Sending L2 transaction'); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log('Transaction sent successfully'); diff --git a/assignments/EscrowAssigmnent/test/Counter.ts b/assignments/EscrowAssigmnent/test/Counter.ts new file mode 100644 index 00000000..3da6ea94 --- /dev/null +++ b/assignments/EscrowAssigmnent/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; + +const { ethers } = await network.connect(); + +describe('Counter', function () { + it('Should emit the Increment event when calling the inc() function', async function () { + const counter = await ethers.deployContract('Counter'); + + await expect(counter.inc()).to.emit(counter, 'Increment').withArgs(1n); + }); + + it('The sum of the Increment events should match the current value', async function () { + const counter = await ethers.deployContract('Counter'); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + 'latest' + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/assignments/EscrowAssigmnent/tsconfig.json b/assignments/EscrowAssigmnent/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/EscrowAssigmnent/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} diff --git a/assignments/Timelock/.gitignore b/assignments/Timelock/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/Timelock/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/assignments/Timelock/README.md b/assignments/Timelock/README.md new file mode 100644 index 00000000..068533e2 --- /dev/null +++ b/assignments/Timelock/README.md @@ -0,0 +1,23 @@ +## TimeLock Smart Contract + +A simple on-chain time-locked vault system that allows users to deposit ETH and lock it until a chosen future timestamp. Each user can create multiple independent vaults with different unlock times. +This contract is useful for: + +1. Personal savings locks +2. Vesting simulations +3. Delayed payments +4. Self-custody time-based funds management + +### Overview + +The TimeLock contract lets users: +Deposit ETH into time-locked vaults +Withdraw funds only after unlock time +Manage multiple vaults per address +Withdraw all unlocked vaults at once +View vault balances and statuses +Each deposit creates a new vault tied to the sender. + +contract address: 0x9566b30F3B5177596A729544Ca127474baa3692C + +(etherscan_link)[https://eth-sepolia.blockscout.com/address/0x9566b30F3B5177596A729544Ca127474baa3692C] diff --git a/assignments/Timelock/contracts/TimeLock.sol b/assignments/Timelock/contracts/TimeLock.sol new file mode 100644 index 00000000..34ccb235 --- /dev/null +++ b/assignments/Timelock/contracts/TimeLock.sol @@ -0,0 +1,152 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract TimeLock { + struct Vault { + uint balance; + uint unlockTime; + bool active; + } + + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint vaultId, uint amount, uint unlockTime); + event Withdrawn(address indexed user, uint vaultId, uint amount); + + function deposit(uint _unlockTime) external payable returns (uint) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + // Create new vault + vaults[msg.sender].push(Vault({ + balance: msg.value, + unlockTime: _unlockTime, + active: true + })); + + uint vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } +function withdraw(uint _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint amount = userVault.balance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.active = false; + + (bool success, ) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + function withdrawAll() external returns (uint) { + uint totalWithdrawn = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && + userVaults[i].balance > 0 && + block.timestamp >= userVaults[i].unlockTime) { + + uint amount = userVaults[i].balance; + userVaults[i].balance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + (bool success, ) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + function getVaultCount(address _user) external view returns (uint) { + return vaults[_user].length; + } + function getVault(address _user, uint _vaultId) external view returns ( + uint balance, + uint unlockTime, + bool active, + bool isUnlocked + ) { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return ( + vault.balance, + vault.unlockTime, + vault.active, + block.timestamp >= vault.unlockTime + ); + } + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + function getActiveVaults(address _user) external view returns ( + uint[] memory activeVaults, + uint[] memory balances, + uint[] memory unlockTimes + ) { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint activeCount = 0; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint[](activeCount); + balances = new uint[](activeCount); + unlockTimes = new uint[](activeCount); + + // Populate arrays + uint index = 0; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, unlockTimes); + } + function getTotalBalance(address _user) external view returns (uint total) { + Vault[] storage userVaults = vaults[_user]; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + function getUnlockedBalance(address _user) external view returns (uint unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && + userVaults[i].balance > 0 && + block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/assignments/Timelock/hardhat.config.ts b/assignments/Timelock/hardhat.config.ts new file mode 100644 index 00000000..64f25861 --- /dev/null +++ b/assignments/Timelock/hardhat.config.ts @@ -0,0 +1,44 @@ +import hardhatToolboxMochaEthersPlugin from '@nomicfoundation/hardhat-toolbox-mocha-ethers'; +import { configVariable, defineConfig } from 'hardhat/config'; +import 'dotenv/config'; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: '0.8.28', + }, + production: { + version: '0.8.28', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: 'edr-simulated', + chainType: 'l1', + }, + hardhatOp: { + type: 'edr-simulated', + chainType: 'op', + }, + sepolia: { + type: 'http', + chainType: 'l1', + url: configVariable('SEPOLIA_RPC_URL'), + accounts: [configVariable('SEPOLIA_PRIVATE_KEY')], + }, + }, + verify: { + etherscan: { + apiKey: configVariable('ETHERSCAN_API_KEY'), + }, + }, +}); diff --git a/assignments/Timelock/ignition/deployments/chain-11155111/artifacts/TimeLockModule#TimeLock.json b/assignments/Timelock/ignition/deployments/chain-11155111/artifacts/TimeLockModule#TimeLock.json new file mode 100644 index 00000000..fa9a9078 --- /dev/null +++ b/assignments/Timelock/ignition/deployments/chain-11155111/artifacts/TimeLockModule#TimeLock.json @@ -0,0 +1,276 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "TimeLock", + "sourceName": "contracts/TimeLock.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "vaultId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "unlockTime", + "type": "uint256" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "vaultId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_unlockTime", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getActiveVaults", + "outputs": [ + { + "internalType": "uint256[]", + "name": "activeVaults", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "unlockTimes", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getAllVaults", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "unlockTime", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "internalType": "struct TimeLock.Vault[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getTotalBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "total", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getUnlockedBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "unlocked", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_vaultId", + "type": "uint256" + } + ], + "name": "getVault", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "unlockTime", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isUnlocked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getVaultCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_vaultId", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawAll", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600e575f5ffd5b50610f3b8061001c5f395ff3fe608060405260043610610084575f3560e01c8063853828b611610057578063853828b61461013d578063a2e5437c14610151578063b6b55f251461017d578063d3d3819314610190578063d99d13f5146101af575f5ffd5b8063129de5bf146100885780632e1a7d4d146100ba57806357c57f72146100db57806377e1296e14610109575b5f5ffd5b348015610093575f5ffd5b506100a76100a2366004610d4a565b6101f6565b6040519081526020015b60405180910390f35b3480156100c5575f5ffd5b506100d96100d4366004610d6a565b6102db565b005b3480156100e6575f5ffd5b506100fa6100f5366004610d4a565b610518565b6040516100b193929190610dbb565b348015610114575f5ffd5b506100a7610123366004610d4a565b6001600160a01b03165f9081526020819052604090205490565b348015610148575f5ffd5b506100a76107a1565b34801561015c575f5ffd5b5061017061016b366004610d4a565b6109ef565b6040516100b19190610dfd565b6100a761018b366004610d6a565b610a7f565b34801561019b575f5ffd5b506100a76101aa366004610d4a565b610bee565b3480156101ba575f5ffd5b506101ce6101c9366004610e5d565b610c74565b60408051948552602085019390935290151591830191909152151560608201526080016100b1565b6001600160a01b0381165f908152602081905260408120815b81548110156102d45781818154811061022a5761022a610e85565b5f91825260209091206002600390920201015460ff16801561026b57505f82828154811061025a5761025a610e85565b905f5260205f2090600302015f0154115b8015610298575081818154811061028457610284610e85565b905f5260205f209060030201600101544210155b156102cc578181815481106102af576102af610e85565b905f5260205f2090600302015f0154836102c99190610ead565b92505b60010161020f565b5050919050565b335f9081526020819052604090205481106103305760405162461bcd60e51b815260206004820152601060248201526f125b9d985b1a59081d985d5b1d08125160821b60448201526064015b60405180910390fd5b335f90815260208190526040812080548390811061035057610350610e85565b5f9182526020909120600390910201600281015490915060ff166103ac5760405162461bcd60e51b81526020600482015260136024820152725661756c74206973206e6f742061637469766560681b6044820152606401610327565b80546103f35760405162461bcd60e51b81526020600482015260166024820152755661756c7420686173207a65726f2062616c616e636560501b6044820152606401610327565b80600101544210156104405760405162461bcd60e51b8152602060048201526016602482015275119d5b991cc8185c99481cdd1a5b1b081b1bd8dad95960521b6044820152606401610327565b80545f80835560028301805460ff19169055604051339083908381818185875af1925050503d805f811461048f576040519150601f19603f3d011682016040523d82523d5f602084013e610494565b606091505b50509050806104d75760405162461bcd60e51b815260206004820152600f60248201526e151c985b9cd9995c8819985a5b1959608a1b6044820152606401610327565b604080518581526020810184905233917f92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6910160405180910390a250505050565b6001600160a01b0381165f90815260208190526040812060609182918291805b82548110156105af5782818154811061055357610553610e85565b5f91825260209091206002600390920201015460ff16801561059457505f83828154811061058357610583610e85565b905f5260205f2090600302015f0154115b156105a757816105a381610ec6565b9250505b600101610538565b508067ffffffffffffffff8111156105c9576105c9610ede565b6040519080825280602002602001820160405280156105f2578160200160208202803683370190505b5094508067ffffffffffffffff81111561060e5761060e610ede565b604051908082528060200260200182016040528015610637578160200160208202803683370190505b5093508067ffffffffffffffff81111561065357610653610ede565b60405190808252806020026020018201604052801561067c578160200160208202803683370190505b5092505f805b83548110156107965783818154811061069d5761069d610e85565b5f91825260209091206002600390920201015460ff1680156106de57505f8482815481106106cd576106cd610e85565b905f5260205f2090600302015f0154115b1561078e57808783815181106106f6576106f6610e85565b60200260200101818152505083818154811061071457610714610e85565b905f5260205f2090600302015f015486838151811061073557610735610e85565b60200260200101818152505083818154811061075357610753610e85565b905f5260205f2090600302016001015485838151811061077557610775610e85565b60209081029190910101528161078a81610ec6565b9250505b600101610682565b505050509193909250565b335f9081526020819052604081208190815b8154811015610910578181815481106107ce576107ce610e85565b5f91825260209091206002600390920201015460ff16801561080f57505f8282815481106107fe576107fe610e85565b905f5260205f2090600302015f0154115b801561083c575081818154811061082857610828610e85565b905f5260205f209060030201600101544210155b15610908575f82828154811061085457610854610e85565b905f5260205f2090600302015f015490505f83838154811061087857610878610e85565b905f5260205f2090600302015f01819055505f83838154811061089d5761089d610e85565b5f9182526020909120600390910201600201805460ff19169115159190911790556108c88185610ead565b604080518481526020810184905291955033917f92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6910160405180910390a2505b6001016107b3565b505f82116109605760405162461bcd60e51b815260206004820152601b60248201527f4e6f20756e6c6f636b65642066756e647320617661696c61626c6500000000006044820152606401610327565b6040515f90339084908381818185875af1925050503d805f811461099f576040519150601f19603f3d011682016040523d82523d5f602084013e6109a4565b606091505b50509050806109e75760405162461bcd60e51b815260206004820152600f60248201526e151c985b9cd9995c8819985a5b1959608a1b6044820152606401610327565b509092915050565b6001600160a01b0381165f90815260208181526040808320805482518185028101850190935280835260609492939192909184015b82821015610a74575f8481526020908190206040805160608101825260038602909201805483526001808201548486015260029091015460ff161515918301919091529083529092019101610a24565b505050509050919050565b5f5f3411610ad95760405162461bcd60e51b815260206004820152602160248201527f4465706f736974206d7573742062652067726561746572207468616e207a65726044820152606f60f81b6064820152608401610327565b428211610b325760405162461bcd60e51b815260206004820152602160248201527f556e6c6f636b2074696d65206d75737420626520696e207468652066757475726044820152606560f81b6064820152608401610327565b335f81815260208181526040808320815160608101835234815280840188815260019382018481528354808601855584885286882093516003909102909301928355905182850155516002909101805460ff19169115159190911790559383529082905291549091610ba391610ef2565b6040805182815234602082015290810185905290915033907f91ede45f04a37a7c170f5c1207df3b6bc748dc1e04ad5e917a241d0f52feada39060600160405180910390a292915050565b6001600160a01b0381165f908152602081905260408120815b81548110156102d457818181548110610c2257610c22610e85565b5f91825260209091206002600390920201015460ff1615610c6c57818181548110610c4f57610c4f610e85565b905f5260205f2090600302015f015483610c699190610ead565b92505b600101610c07565b6001600160a01b0382165f908152602081905260408120548190819081908510610cd35760405162461bcd60e51b815260206004820152601060248201526f125b9d985b1a59081d985d5b1d08125160821b6044820152606401610327565b6001600160a01b0386165f908152602081905260408120805487908110610cfc57610cfc610e85565b5f918252602090912060039091020180546001820154600290920154909991985060ff1696504288111595509350505050565b80356001600160a01b0381168114610d45575f5ffd5b919050565b5f60208284031215610d5a575f5ffd5b610d6382610d2f565b9392505050565b5f60208284031215610d7a575f5ffd5b5035919050565b5f8151808452602084019350602083015f5b82811015610db1578151865260209586019590910190600101610d93565b5093949350505050565b606081525f610dcd6060830186610d81565b8281036020840152610ddf8186610d81565b90508281036040840152610df38185610d81565b9695505050505050565b602080825282518282018190525f918401906040840190835b81811015610e52578351805184526020810151602085015260408101511515604085015250606083019250602084019350600181019050610e16565b509095945050505050565b5f5f60408385031215610e6e575f5ffd5b610e7783610d2f565b946020939093013593505050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b80820180821115610ec057610ec0610e99565b92915050565b5f60018201610ed757610ed7610e99565b5060010190565b634e487b7160e01b5f52604160045260245ffd5b81810381811115610ec057610ec0610e9956fea2646970667358221220c9aacbde5cf327311162eecd155054f90be76550357d00f8e4b10929caf9884064736f6c634300081c0033", + "deployedBytecode": "0x608060405260043610610084575f3560e01c8063853828b611610057578063853828b61461013d578063a2e5437c14610151578063b6b55f251461017d578063d3d3819314610190578063d99d13f5146101af575f5ffd5b8063129de5bf146100885780632e1a7d4d146100ba57806357c57f72146100db57806377e1296e14610109575b5f5ffd5b348015610093575f5ffd5b506100a76100a2366004610d4a565b6101f6565b6040519081526020015b60405180910390f35b3480156100c5575f5ffd5b506100d96100d4366004610d6a565b6102db565b005b3480156100e6575f5ffd5b506100fa6100f5366004610d4a565b610518565b6040516100b193929190610dbb565b348015610114575f5ffd5b506100a7610123366004610d4a565b6001600160a01b03165f9081526020819052604090205490565b348015610148575f5ffd5b506100a76107a1565b34801561015c575f5ffd5b5061017061016b366004610d4a565b6109ef565b6040516100b19190610dfd565b6100a761018b366004610d6a565b610a7f565b34801561019b575f5ffd5b506100a76101aa366004610d4a565b610bee565b3480156101ba575f5ffd5b506101ce6101c9366004610e5d565b610c74565b60408051948552602085019390935290151591830191909152151560608201526080016100b1565b6001600160a01b0381165f908152602081905260408120815b81548110156102d45781818154811061022a5761022a610e85565b5f91825260209091206002600390920201015460ff16801561026b57505f82828154811061025a5761025a610e85565b905f5260205f2090600302015f0154115b8015610298575081818154811061028457610284610e85565b905f5260205f209060030201600101544210155b156102cc578181815481106102af576102af610e85565b905f5260205f2090600302015f0154836102c99190610ead565b92505b60010161020f565b5050919050565b335f9081526020819052604090205481106103305760405162461bcd60e51b815260206004820152601060248201526f125b9d985b1a59081d985d5b1d08125160821b60448201526064015b60405180910390fd5b335f90815260208190526040812080548390811061035057610350610e85565b5f9182526020909120600390910201600281015490915060ff166103ac5760405162461bcd60e51b81526020600482015260136024820152725661756c74206973206e6f742061637469766560681b6044820152606401610327565b80546103f35760405162461bcd60e51b81526020600482015260166024820152755661756c7420686173207a65726f2062616c616e636560501b6044820152606401610327565b80600101544210156104405760405162461bcd60e51b8152602060048201526016602482015275119d5b991cc8185c99481cdd1a5b1b081b1bd8dad95960521b6044820152606401610327565b80545f80835560028301805460ff19169055604051339083908381818185875af1925050503d805f811461048f576040519150601f19603f3d011682016040523d82523d5f602084013e610494565b606091505b50509050806104d75760405162461bcd60e51b815260206004820152600f60248201526e151c985b9cd9995c8819985a5b1959608a1b6044820152606401610327565b604080518581526020810184905233917f92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6910160405180910390a250505050565b6001600160a01b0381165f90815260208190526040812060609182918291805b82548110156105af5782818154811061055357610553610e85565b5f91825260209091206002600390920201015460ff16801561059457505f83828154811061058357610583610e85565b905f5260205f2090600302015f0154115b156105a757816105a381610ec6565b9250505b600101610538565b508067ffffffffffffffff8111156105c9576105c9610ede565b6040519080825280602002602001820160405280156105f2578160200160208202803683370190505b5094508067ffffffffffffffff81111561060e5761060e610ede565b604051908082528060200260200182016040528015610637578160200160208202803683370190505b5093508067ffffffffffffffff81111561065357610653610ede565b60405190808252806020026020018201604052801561067c578160200160208202803683370190505b5092505f805b83548110156107965783818154811061069d5761069d610e85565b5f91825260209091206002600390920201015460ff1680156106de57505f8482815481106106cd576106cd610e85565b905f5260205f2090600302015f0154115b1561078e57808783815181106106f6576106f6610e85565b60200260200101818152505083818154811061071457610714610e85565b905f5260205f2090600302015f015486838151811061073557610735610e85565b60200260200101818152505083818154811061075357610753610e85565b905f5260205f2090600302016001015485838151811061077557610775610e85565b60209081029190910101528161078a81610ec6565b9250505b600101610682565b505050509193909250565b335f9081526020819052604081208190815b8154811015610910578181815481106107ce576107ce610e85565b5f91825260209091206002600390920201015460ff16801561080f57505f8282815481106107fe576107fe610e85565b905f5260205f2090600302015f0154115b801561083c575081818154811061082857610828610e85565b905f5260205f209060030201600101544210155b15610908575f82828154811061085457610854610e85565b905f5260205f2090600302015f015490505f83838154811061087857610878610e85565b905f5260205f2090600302015f01819055505f83838154811061089d5761089d610e85565b5f9182526020909120600390910201600201805460ff19169115159190911790556108c88185610ead565b604080518481526020810184905291955033917f92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6910160405180910390a2505b6001016107b3565b505f82116109605760405162461bcd60e51b815260206004820152601b60248201527f4e6f20756e6c6f636b65642066756e647320617661696c61626c6500000000006044820152606401610327565b6040515f90339084908381818185875af1925050503d805f811461099f576040519150601f19603f3d011682016040523d82523d5f602084013e6109a4565b606091505b50509050806109e75760405162461bcd60e51b815260206004820152600f60248201526e151c985b9cd9995c8819985a5b1959608a1b6044820152606401610327565b509092915050565b6001600160a01b0381165f90815260208181526040808320805482518185028101850190935280835260609492939192909184015b82821015610a74575f8481526020908190206040805160608101825260038602909201805483526001808201548486015260029091015460ff161515918301919091529083529092019101610a24565b505050509050919050565b5f5f3411610ad95760405162461bcd60e51b815260206004820152602160248201527f4465706f736974206d7573742062652067726561746572207468616e207a65726044820152606f60f81b6064820152608401610327565b428211610b325760405162461bcd60e51b815260206004820152602160248201527f556e6c6f636b2074696d65206d75737420626520696e207468652066757475726044820152606560f81b6064820152608401610327565b335f81815260208181526040808320815160608101835234815280840188815260019382018481528354808601855584885286882093516003909102909301928355905182850155516002909101805460ff19169115159190911790559383529082905291549091610ba391610ef2565b6040805182815234602082015290810185905290915033907f91ede45f04a37a7c170f5c1207df3b6bc748dc1e04ad5e917a241d0f52feada39060600160405180910390a292915050565b6001600160a01b0381165f908152602081905260408120815b81548110156102d457818181548110610c2257610c22610e85565b5f91825260209091206002600390920201015460ff1615610c6c57818181548110610c4f57610c4f610e85565b905f5260205f2090600302015f015483610c699190610ead565b92505b600101610c07565b6001600160a01b0382165f908152602081905260408120548190819081908510610cd35760405162461bcd60e51b815260206004820152601060248201526f125b9d985b1a59081d985d5b1d08125160821b6044820152606401610327565b6001600160a01b0386165f908152602081905260408120805487908110610cfc57610cfc610e85565b5f918252602090912060039091020180546001820154600290920154909991985060ff1696504288111595509350505050565b80356001600160a01b0381168114610d45575f5ffd5b919050565b5f60208284031215610d5a575f5ffd5b610d6382610d2f565b9392505050565b5f60208284031215610d7a575f5ffd5b5035919050565b5f8151808452602084019350602083015f5b82811015610db1578151865260209586019590910190600101610d93565b5093949350505050565b606081525f610dcd6060830186610d81565b8281036020840152610ddf8186610d81565b90508281036040840152610df38185610d81565b9695505050505050565b602080825282518282018190525f918401906040840190835b81811015610e52578351805184526020810151602085015260408101511515604085015250606083019250602084019350600181019050610e16565b509095945050505050565b5f5f60408385031215610e6e575f5ffd5b610e7783610d2f565b946020939093013593505050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b80820180821115610ec057610ec0610e99565b92915050565b5f60018201610ed757610ed7610e99565b5060010190565b634e487b7160e01b5f52604160045260245ffd5b81810381811115610ec057610ec0610e9956fea2646970667358221220c9aacbde5cf327311162eecd155054f90be76550357d00f8e4b10929caf9884064736f6c634300081c0033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/TimeLock.sol", + "buildInfoId": "solc-0_8_28-fc407e03e0399ada8b13f1aefb2e259ced5d9be9" +} \ No newline at end of file diff --git a/assignments/Timelock/ignition/deployments/chain-11155111/deployed_addresses.json b/assignments/Timelock/ignition/deployments/chain-11155111/deployed_addresses.json new file mode 100644 index 00000000..30d719ef --- /dev/null +++ b/assignments/Timelock/ignition/deployments/chain-11155111/deployed_addresses.json @@ -0,0 +1,3 @@ +{ + "TimeLockModule#TimeLock": "0x9566b30F3B5177596A729544Ca127474baa3692C" +} diff --git a/assignments/Timelock/ignition/deployments/chain-11155111/journal.jsonl b/assignments/Timelock/ignition/deployments/chain-11155111/journal.jsonl new file mode 100644 index 00000000..b770b668 --- /dev/null +++ b/assignments/Timelock/ignition/deployments/chain-11155111/journal.jsonl @@ -0,0 +1,8 @@ + +{"chainId":11155111,"type":"DEPLOYMENT_INITIALIZE"} +{"artifactId":"TimeLockModule#TimeLock","constructorArgs":[],"contractName":"TimeLock","dependencies":[],"from":"0x4e1b1d9af926e7e0fbcb9c5b23eeda9d80642b99","futureId":"TimeLockModule#TimeLock","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"0"}} +{"futureId":"TimeLockModule#TimeLock","networkInteraction":{"data":"0x6080604052348015600e575f5ffd5b50610f3b8061001c5f395ff3fe608060405260043610610084575f3560e01c8063853828b611610057578063853828b61461013d578063a2e5437c14610151578063b6b55f251461017d578063d3d3819314610190578063d99d13f5146101af575f5ffd5b8063129de5bf146100885780632e1a7d4d146100ba57806357c57f72146100db57806377e1296e14610109575b5f5ffd5b348015610093575f5ffd5b506100a76100a2366004610d4a565b6101f6565b6040519081526020015b60405180910390f35b3480156100c5575f5ffd5b506100d96100d4366004610d6a565b6102db565b005b3480156100e6575f5ffd5b506100fa6100f5366004610d4a565b610518565b6040516100b193929190610dbb565b348015610114575f5ffd5b506100a7610123366004610d4a565b6001600160a01b03165f9081526020819052604090205490565b348015610148575f5ffd5b506100a76107a1565b34801561015c575f5ffd5b5061017061016b366004610d4a565b6109ef565b6040516100b19190610dfd565b6100a761018b366004610d6a565b610a7f565b34801561019b575f5ffd5b506100a76101aa366004610d4a565b610bee565b3480156101ba575f5ffd5b506101ce6101c9366004610e5d565b610c74565b60408051948552602085019390935290151591830191909152151560608201526080016100b1565b6001600160a01b0381165f908152602081905260408120815b81548110156102d45781818154811061022a5761022a610e85565b5f91825260209091206002600390920201015460ff16801561026b57505f82828154811061025a5761025a610e85565b905f5260205f2090600302015f0154115b8015610298575081818154811061028457610284610e85565b905f5260205f209060030201600101544210155b156102cc578181815481106102af576102af610e85565b905f5260205f2090600302015f0154836102c99190610ead565b92505b60010161020f565b5050919050565b335f9081526020819052604090205481106103305760405162461bcd60e51b815260206004820152601060248201526f125b9d985b1a59081d985d5b1d08125160821b60448201526064015b60405180910390fd5b335f90815260208190526040812080548390811061035057610350610e85565b5f9182526020909120600390910201600281015490915060ff166103ac5760405162461bcd60e51b81526020600482015260136024820152725661756c74206973206e6f742061637469766560681b6044820152606401610327565b80546103f35760405162461bcd60e51b81526020600482015260166024820152755661756c7420686173207a65726f2062616c616e636560501b6044820152606401610327565b80600101544210156104405760405162461bcd60e51b8152602060048201526016602482015275119d5b991cc8185c99481cdd1a5b1b081b1bd8dad95960521b6044820152606401610327565b80545f80835560028301805460ff19169055604051339083908381818185875af1925050503d805f811461048f576040519150601f19603f3d011682016040523d82523d5f602084013e610494565b606091505b50509050806104d75760405162461bcd60e51b815260206004820152600f60248201526e151c985b9cd9995c8819985a5b1959608a1b6044820152606401610327565b604080518581526020810184905233917f92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6910160405180910390a250505050565b6001600160a01b0381165f90815260208190526040812060609182918291805b82548110156105af5782818154811061055357610553610e85565b5f91825260209091206002600390920201015460ff16801561059457505f83828154811061058357610583610e85565b905f5260205f2090600302015f0154115b156105a757816105a381610ec6565b9250505b600101610538565b508067ffffffffffffffff8111156105c9576105c9610ede565b6040519080825280602002602001820160405280156105f2578160200160208202803683370190505b5094508067ffffffffffffffff81111561060e5761060e610ede565b604051908082528060200260200182016040528015610637578160200160208202803683370190505b5093508067ffffffffffffffff81111561065357610653610ede565b60405190808252806020026020018201604052801561067c578160200160208202803683370190505b5092505f805b83548110156107965783818154811061069d5761069d610e85565b5f91825260209091206002600390920201015460ff1680156106de57505f8482815481106106cd576106cd610e85565b905f5260205f2090600302015f0154115b1561078e57808783815181106106f6576106f6610e85565b60200260200101818152505083818154811061071457610714610e85565b905f5260205f2090600302015f015486838151811061073557610735610e85565b60200260200101818152505083818154811061075357610753610e85565b905f5260205f2090600302016001015485838151811061077557610775610e85565b60209081029190910101528161078a81610ec6565b9250505b600101610682565b505050509193909250565b335f9081526020819052604081208190815b8154811015610910578181815481106107ce576107ce610e85565b5f91825260209091206002600390920201015460ff16801561080f57505f8282815481106107fe576107fe610e85565b905f5260205f2090600302015f0154115b801561083c575081818154811061082857610828610e85565b905f5260205f209060030201600101544210155b15610908575f82828154811061085457610854610e85565b905f5260205f2090600302015f015490505f83838154811061087857610878610e85565b905f5260205f2090600302015f01819055505f83838154811061089d5761089d610e85565b5f9182526020909120600390910201600201805460ff19169115159190911790556108c88185610ead565b604080518481526020810184905291955033917f92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6910160405180910390a2505b6001016107b3565b505f82116109605760405162461bcd60e51b815260206004820152601b60248201527f4e6f20756e6c6f636b65642066756e647320617661696c61626c6500000000006044820152606401610327565b6040515f90339084908381818185875af1925050503d805f811461099f576040519150601f19603f3d011682016040523d82523d5f602084013e6109a4565b606091505b50509050806109e75760405162461bcd60e51b815260206004820152600f60248201526e151c985b9cd9995c8819985a5b1959608a1b6044820152606401610327565b509092915050565b6001600160a01b0381165f90815260208181526040808320805482518185028101850190935280835260609492939192909184015b82821015610a74575f8481526020908190206040805160608101825260038602909201805483526001808201548486015260029091015460ff161515918301919091529083529092019101610a24565b505050509050919050565b5f5f3411610ad95760405162461bcd60e51b815260206004820152602160248201527f4465706f736974206d7573742062652067726561746572207468616e207a65726044820152606f60f81b6064820152608401610327565b428211610b325760405162461bcd60e51b815260206004820152602160248201527f556e6c6f636b2074696d65206d75737420626520696e207468652066757475726044820152606560f81b6064820152608401610327565b335f81815260208181526040808320815160608101835234815280840188815260019382018481528354808601855584885286882093516003909102909301928355905182850155516002909101805460ff19169115159190911790559383529082905291549091610ba391610ef2565b6040805182815234602082015290810185905290915033907f91ede45f04a37a7c170f5c1207df3b6bc748dc1e04ad5e917a241d0f52feada39060600160405180910390a292915050565b6001600160a01b0381165f908152602081905260408120815b81548110156102d457818181548110610c2257610c22610e85565b5f91825260209091206002600390920201015460ff1615610c6c57818181548110610c4f57610c4f610e85565b905f5260205f2090600302015f015483610c699190610ead565b92505b600101610c07565b6001600160a01b0382165f908152602081905260408120548190819081908510610cd35760405162461bcd60e51b815260206004820152601060248201526f125b9d985b1a59081d985d5b1d08125160821b6044820152606401610327565b6001600160a01b0386165f908152602081905260408120805487908110610cfc57610cfc610e85565b5f918252602090912060039091020180546001820154600290920154909991985060ff1696504288111595509350505050565b80356001600160a01b0381168114610d45575f5ffd5b919050565b5f60208284031215610d5a575f5ffd5b610d6382610d2f565b9392505050565b5f60208284031215610d7a575f5ffd5b5035919050565b5f8151808452602084019350602083015f5b82811015610db1578151865260209586019590910190600101610d93565b5093949350505050565b606081525f610dcd6060830186610d81565b8281036020840152610ddf8186610d81565b90508281036040840152610df38185610d81565b9695505050505050565b602080825282518282018190525f918401906040840190835b81811015610e52578351805184526020810151602085015260408101511515604085015250606083019250602084019350600181019050610e16565b509095945050505050565b5f5f60408385031215610e6e575f5ffd5b610e7783610d2f565b946020939093013593505050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b80820180821115610ec057610ec0610e99565b92915050565b5f60018201610ed757610ed7610e99565b5060010190565b634e487b7160e01b5f52604160045260245ffd5b81810381811115610ec057610ec0610e9956fea2646970667358221220c9aacbde5cf327311162eecd155054f90be76550357d00f8e4b10929caf9884064736f6c634300081c0033","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"0"}},"type":"NETWORK_INTERACTION_REQUEST"} +{"futureId":"TimeLockModule#TimeLock","networkInteractionId":1,"nonce":16,"type":"TRANSACTION_PREPARE_SEND"} +{"futureId":"TimeLockModule#TimeLock","networkInteractionId":1,"nonce":16,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"5731974928"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000"}},"hash":"0x6e97fbbcf98c862f219c0a7d6811757262df63ef5997ada62b225de0fd289bb6"},"type":"TRANSACTION_SEND"} +{"futureId":"TimeLockModule#TimeLock","hash":"0x6e97fbbcf98c862f219c0a7d6811757262df63ef5997ada62b225de0fd289bb6","networkInteractionId":1,"receipt":{"blockHash":"0x71faf45d797130d787f6ba6d5b7788ac24b5b641e0b979f79100216a8968ccc3","blockNumber":10284998,"contractAddress":"0x9566b30F3B5177596A729544Ca127474baa3692C","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} +{"futureId":"TimeLockModule#TimeLock","result":{"address":"0x9566b30F3B5177596A729544Ca127474baa3692C","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} \ No newline at end of file diff --git a/assignments/Timelock/ignition/modules/TimeLock.ts b/assignments/Timelock/ignition/modules/TimeLock.ts new file mode 100644 index 00000000..93fa6175 --- /dev/null +++ b/assignments/Timelock/ignition/modules/TimeLock.ts @@ -0,0 +1,6 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; + +export default buildModule('TimeLockModule', (m) => { + const timelock = m.contract('TimeLock'); + return { timelock }; +}); diff --git a/assignments/Timelock/package.json b/assignments/Timelock/package.json new file mode 100644 index 00000000..8e425dd8 --- /dev/null +++ b/assignments/Timelock/package.json @@ -0,0 +1,23 @@ +{ + "name": "Timelock", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.11", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.8", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + }, + "dependencies": { + "dotenv": "^17.3.1" + } +} diff --git a/assignments/Timelock/scripts/send-op-tx.ts b/assignments/Timelock/scripts/send-op-tx.ts new file mode 100644 index 00000000..681a76e0 --- /dev/null +++ b/assignments/Timelock/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from 'hardhat'; + +const { ethers } = await network.connect({ + network: 'hardhatOp', + chainType: 'op', +}); + +console.log('Sending transaction using the OP chain type'); + +const [sender] = await ethers.getSigners(); + +console.log('Sending 1 wei from', sender.address, 'to itself'); + +console.log('Sending L2 transaction'); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log('Transaction sent successfully'); diff --git a/assignments/Timelock/test/TimeLock.ts b/assignments/Timelock/test/TimeLock.ts new file mode 100644 index 00000000..99f0b5bd --- /dev/null +++ b/assignments/Timelock/test/TimeLock.ts @@ -0,0 +1,173 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; + +const { ethers, networkHelpers } = await network.connect(); + +const { time, loadFixture } = networkHelpers; + +describe('TimeLock', function () { + const ONE_DAY_IN_SECS = 24 * 60 * 60; + const DEPOSIT_AMOUNT = ethers.parseEther('1.0'); + + async function deployTimeLockFixture() { + const [owner, otherAccount] = await ethers.getSigners(); + const TimeLock = await ethers.getContractFactory('TimeLock'); + const timelock = await TimeLock.deploy(); + + return { timelock, owner, otherAccount }; + } + + describe('Deposits', function () { + it('Should create a vault with correct balance and unlock time', async function () { + const { timelock, owner } = await loadFixture(deployTimeLockFixture); + const unlockTime = (await time.latest()) + ONE_DAY_IN_SECS; + + await expect(timelock.deposit(unlockTime, { value: DEPOSIT_AMOUNT })) + .to.emit(timelock, 'Deposited') + .withArgs(owner.address, 0, DEPOSIT_AMOUNT, unlockTime); + + const vault = await timelock.getVault(owner.address, 0); + expect(vault.balance).to.equal(DEPOSIT_AMOUNT); + expect(vault.unlockTime).to.equal(unlockTime); + expect(vault.active).to.be.true; + }); + + it('Should revert if deposit is 0', async function () { + const { timelock } = await loadFixture(deployTimeLockFixture); + const unlockTime = (await time.latest()) + ONE_DAY_IN_SECS; + + await expect( + timelock.deposit(unlockTime, { value: 0 }) + ).to.be.revertedWith('Deposit must be greater than zero'); + }); + + it('Should revert if unlock time is in the past', async function () { + const { timelock } = await loadFixture(deployTimeLockFixture); + const pastTime = (await time.latest()) - 100; + + await expect( + timelock.deposit(pastTime, { value: DEPOSIT_AMOUNT }) + ).to.be.revertedWith('Unlock time must be in the future'); + }); + }); + + describe.only('Withdrawals', function () { + it('Should fail if funds are still locked', async function () { + const { timelock } = await loadFixture(deployTimeLockFixture); + const unlockTime = (await time.latest()) + ONE_DAY_IN_SECS; + + await timelock.deposit(unlockTime, { value: DEPOSIT_AMOUNT }); + await expect(timelock.withdraw(0)).to.be.revertedWith( + 'Funds are still locked' + ); + }); + + it('Should succeed if unlock time has passed', async function () { + const { timelock, owner } = await loadFixture(deployTimeLockFixture); + const unlockTime = (await time.latest()) + ONE_DAY_IN_SECS; + + await timelock.deposit(unlockTime, { value: DEPOSIT_AMOUNT }); + + // Move time forward + await time.increaseTo(unlockTime); + + await expect(timelock.withdraw(0)) + .to.emit(timelock, 'Withdrawn') + .withArgs(owner.address, 0, DEPOSIT_AMOUNT); + + const vault = await timelock.getVault(owner.address, 0); + expect(vault.active).to.be.false; + expect(vault.balance).to.equal(0); + }); + + it('Should fail if trying to withdraw from an inactive vault', async function () { + const { timelock } = await loadFixture(deployTimeLockFixture); + const unlockTime = (await time.latest()) + ONE_DAY_IN_SECS; + + await timelock.deposit(unlockTime, { value: DEPOSIT_AMOUNT }); + await time.increaseTo(unlockTime); + await timelock.withdraw(0); + + await expect(timelock.withdraw(0)).to.be.revertedWith( + 'Vault is not active' + ); + }); + }); + + describe('WithdrawAll', function () { + it('Should withdraw from multiple unlocked vaults at once', async function () { + const { timelock, owner } = await loadFixture(deployTimeLockFixture); + const now = await time.latest(); + + // Deposit into 3 vaults with different times + await timelock.deposit(now + ONE_DAY_IN_SECS, { value: DEPOSIT_AMOUNT }); // Vault 0 + await timelock.deposit(now + ONE_DAY_IN_SECS * 2, { + value: DEPOSIT_AMOUNT, + }); // Vault 1 + await timelock.deposit(now + ONE_DAY_IN_SECS * 10, { + value: DEPOSIT_AMOUNT, + }); // Vault 2 (remains locked) + + // Move time to unlock first two vaults + await time.increase(ONE_DAY_IN_SECS * 3); + + const expectedTransfer = DEPOSIT_AMOUNT * BigInt(2); + + await expect(timelock.withdrawAll()).to.changeEtherBalance( + ethers, + owner, + expectedTransfer + ); + + expect(await timelock.getTotalBalance(owner.address)).to.equal( + DEPOSIT_AMOUNT + ); // Only Vault 2 remains + }); + + it('Should revert if no vaults are ready for withdrawal', async function () { + const { timelock } = await loadFixture(deployTimeLockFixture); + const unlockTime = (await time.latest()) + ONE_DAY_IN_SECS; + + await timelock.deposit(unlockTime, { value: DEPOSIT_AMOUNT }); + await expect(timelock.withdrawAll()).to.be.revertedWith( + 'No unlocked funds available' + ); + }); + }); + + describe('View Functions', function () { + it('Should correctly track total and unlocked balances', async function () { + const { timelock, owner } = await loadFixture(deployTimeLockFixture); + const now = await time.latest(); + + await timelock.deposit(now + 100, { value: ethers.parseEther('1') }); + await timelock.deposit(now + 1000, { value: ethers.parseEther('2') }); + + expect(await timelock.getTotalBalance(owner.address)).to.equal( + ethers.parseEther('3') + ); + expect(await timelock.getUnlockedBalance(owner.address)).to.equal(0); + + await time.increase(200); + expect(await timelock.getUnlockedBalance(owner.address)).to.equal( + ethers.parseEther('1') + ); + }); + + it('Should return only active vaults data', async function () { + const { timelock, owner } = await loadFixture(deployTimeLockFixture); + const now = await time.latest(); + + await timelock.deposit(now + 100, { value: DEPOSIT_AMOUNT }); + await timelock.deposit(now + 200, { value: DEPOSIT_AMOUNT }); + + // Withdraw the first one + await time.increase(150); + await timelock.withdraw(0); + + const activeData = await timelock.getActiveVaults(owner.address); + expect(activeData.activeVaults.length).to.equal(1); + expect(activeData.activeVaults[0]).to.equal(1); // Index of the second vault + }); + }); +}); diff --git a/assignments/Timelock/tsconfig.json b/assignments/Timelock/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/Timelock/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} diff --git a/assignments/Todo/.gitignore b/assignments/Todo/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/Todo/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/assignments/Todo/README.md b/assignments/Todo/README.md new file mode 100644 index 00000000..081c64cb --- /dev/null +++ b/assignments/Todo/README.md @@ -0,0 +1,160 @@ +# ✅ TodoList + +> A decentralized todo list application built on Ethereum — create, complete, and manage tasks entirely on-chain. + +--- + +## Overview + +TodoList is a simple smart contract project that demonstrates on-chain task management. Users can add todo items, mark them as complete, and query their task list directly from the blockchain. Built with Hardhat 3 and deployed on Ethereum, it serves as a clean reference project for learning Solidity state management, event emission, and contract interaction patterns. + +--- + +## Tech Stack + +- [Hardhat 3](https://hardhat.org/) — Development environment and task runner +- [Solidity](https://soliditylang.org/) — Smart contract language +- [Ethers.js v6](https://docs.ethers.org/v6/) — Ethereum library +- [TypeScript](https://www.typescriptlang.org/) — Typed scripting + +--- + +## Folder Structure + +``` +. +├── contracts/ # Solidity source files +│ ├── TodoList.sol # Core todo list contract +│ +│ +│ +├── ignition/ # Hardhat Ignition deployment modules +│ └── modules/ +│ └── TodoList.ts # Deployment module definitions +│ +├── test/ # Test suite +│ ├── unit/ # Unit tests (TodoList.test.ts) +│ └── integration/ # End-to-end and fork tests +│ +├── scripts/ # Standalone utility scripts +│ +├── deployments/ # Auto-generated deployment artifacts +│ └── / +│ └── deployed_addresses.json +│ +├── typechain-types/ # Auto-generated TypeScript typings +│ +├── hardhat.config.ts # Hardhat configuration +├── package.json +└── README.md +``` + +--- + +## Getting Started + +### Prerequisites + +- Node.js `>= 20.x` +- npm or pnpm + +### Install Dependencies + +```bash +npm install +``` + +### Compile Contracts + +```bash +npx hardhat compile +``` + +### Run Tests + +```bash +npx hardhat test +``` + +### Run Tests with Gas Report + +```bash +REPORT_GAS=true npx hardhat test +``` + +--- + +## Deployment + +Deployments are managed via [Hardhat Ignition](https://hardhat.org/ignition/docs/getting-started). + +### Deploy to a Network + +```bash +npx hardhat ignition deploy ignition/modules/TodoList.ts --network +``` + +### Supported Networks + +| Network | Chain ID | +| ---------------- | -------- | +| Ethereum Mainnet | 1 | +| Sepolia Testnet | 11155111 | +| Localhost | 31337 | + +--- + +## Deployed Contracts + +> ✅ All contracts are **verified on Etherscan**. + +### Ethereum Mainnet + +| Contract | Address | Etherscan | +| ---------- | --------------------------- | --------------------------------------------------------------------------- | +| `TodoList` | `0xYourContractAddressHere` | [View on Etherscan](https://etherscan.io/address/0xYourContractAddressHere) | + +### Sepolia Testnet + +| Contract | Address | Etherscan | +| ---------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| `TodoList` | 0xaAee9Be4E1F57cde21976adacF502D2349054506 | [View on Etherscan] (https://eth-sepolia.blockscout.com/address/0xaAee9Be4E1F57cde21976adacF502D2349054506#code) | +| | + +--- + +## Contract Verification + +Contracts are verified using Hardhat's built-in Etherscan verification. To manually verify a contract run: + +```bash +npx hardhat verify --network +``` + +Ensure your `hardhat.config.ts` includes a valid `ETHERSCAN_API_KEY` set via your `.env` file. + +--- + +## Environment Variables + +Create a `.env` file in the root of the project and populate it with the following: + +```env +# RPC URLs +MAINNET_RPC_URL=https://mainnet.infura.io/v3/YOUR_KEY +SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY + +# Deployer wallet +PRIVATE_KEY=your_private_key_here + +# Etherscan verification +ETHERSCAN_API_KEY=your_etherscan_api_key_here +``` + +> ⚠️ Never commit your `.env` file. It is included in `.gitignore` by default. + +--- + +## License + +This project is licensed under the [MIT License](LICENSE). diff --git a/assignments/Todo/contracts/Todo.sol b/assignments/Todo/contracts/Todo.sol new file mode 100644 index 00000000..b4684bb7 --- /dev/null +++ b/assignments/Todo/contracts/Todo.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract TodoContract { + enum Status { + Done, + Defaulted, + Pending + } + + struct Todo { + uint256 id; + address owner; + string text; + Status status; + uint256 deadline; + } + + // State Variables + uint256 private todoCounter; + mapping(uint256 => Todo) public todos; + // Maps user address to a list of their Todo IDs + mapping(address => uint256[]) private userTodoIds; + + // Events + event CreatedTodo(uint256 indexed id, address indexed owner, string text, uint256 deadline); + event StatusUpdated(uint256 indexed id, Status newStatus); + event TodoEdited(uint256 indexed id, string newText, uint256 newDeadline); + event TodoDeleted(uint256 indexed id, address indexed owner); + + // Modifiers + modifier onlyTodoOwner(uint256 _id) { + require(todos[_id].owner == msg.sender, "Not the owner"); + _; + } + + modifier exists(uint256 _id) { + require(_id > 0 && _id <= todoCounter, "Todo does not exist"); + require(todos[_id].owner != address(0), "Todo was deleted"); + _; + } + + function createTodo(string calldata _text, uint256 _deadline) external returns (uint256) { + require(bytes(_text).length > 0, "Empty text"); + require(_deadline > block.timestamp + 600, "Deadline must be at least 10 mins away"); + + todoCounter++; + + todos[todoCounter] = Todo({ + id: todoCounter, + owner: msg.sender, + text: _text, + status: Status.Pending, + deadline: _deadline + }); + + userTodoIds[msg.sender].push(todoCounter); + + emit CreatedTodo(todoCounter, msg.sender, _text, _deadline); + return todoCounter; + } + function completeTodo(uint256 _id) external exists(_id) onlyTodoOwner(_id) { + Todo storage todo = todos[_id]; + require(todo.status == Status.Pending, "Todo is already finalized"); + + if (block.timestamp > todo.deadline) { + todo.status = Status.Defaulted; + } else { + todo.status = Status.Done; + } + + emit StatusUpdated(_id, todo.status); + } + function editTodo(uint256 _id, string calldata _newText, uint256 _newDeadline) + external + exists(_id) + onlyTodoOwner(_id) + { + Todo storage todo = todos[_id]; + require(todo.status == Status.Pending, "Cannot edit completed todos"); + require(bytes(_newText).length > 0, "Text cannot be empty"); + require(_newDeadline > block.timestamp + 600, "New deadline too soon"); + + todo.text = _newText; + todo.deadline = _newDeadline; + + emit TodoEdited(_id, _newText, _newDeadline); + } + function getMyTodoIds() external view returns (uint256[] memory) { + return userTodoIds[msg.sender]; + } + function getTodo(uint256 _id) external view exists(_id) returns (Todo memory) { + return todos[_id]; + } + function getTotalTodoCount() external view returns (uint256) { + return todoCounter; + } +} diff --git a/assignments/Todo/contracts/Todo.t.sol b/assignments/Todo/contracts/Todo.t.sol new file mode 100644 index 00000000..e69de29b diff --git a/assignments/Todo/hardhat.config.ts b/assignments/Todo/hardhat.config.ts new file mode 100644 index 00000000..288983fd --- /dev/null +++ b/assignments/Todo/hardhat.config.ts @@ -0,0 +1,43 @@ +import hardhatToolboxMochaEthersPlugin from '@nomicfoundation/hardhat-toolbox-mocha-ethers'; +import { configVariable, defineConfig } from 'hardhat/config'; +import 'dotenv/config'; +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: '0.8.28', + }, + production: { + version: '0.8.28', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: 'edr-simulated', + chainType: 'l1', + }, + hardhatOp: { + type: 'edr-simulated', + chainType: 'op', + }, + sepolia: { + type: 'http', + chainType: 'l1', + url: configVariable('SEPOLIA_RPC_URL'), + accounts: [configVariable('SEPOLIA_PRIVATE_KEY')], + }, + }, + verify: { + etherscan: { + apiKey: configVariable('ETHERSCAN_API_KEY'), + }, + }, +}); diff --git a/assignments/Todo/ignition/deployments/chain-11155111/artifacts/TodoModule#TodoContract.json b/assignments/Todo/ignition/deployments/chain-11155111/artifacts/TodoModule#TodoContract.json new file mode 100644 index 00000000..64b67ad6 --- /dev/null +++ b/assignments/Todo/ignition/deployments/chain-11155111/artifacts/TodoModule#TodoContract.json @@ -0,0 +1,279 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "TodoContract", + "sourceName": "contracts/Todo.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "text", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "CreatedTodo", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum TodoContract.Status", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "StatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "TodoDeleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "newText", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newDeadline", + "type": "uint256" + } + ], + "name": "TodoEdited", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "completeTodo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_text", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_deadline", + "type": "uint256" + } + ], + "name": "createTodo", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_newText", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_newDeadline", + "type": "uint256" + } + ], + "name": "editTodo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getMyTodoIds", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getTodo", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "string", + "name": "text", + "type": "string" + }, + { + "internalType": "enum TodoContract.Status", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct TodoContract.Todo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTotalTodoCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "todos", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "string", + "name": "text", + "type": "string" + }, + { + "internalType": "enum TodoContract.Status", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600e575f5ffd5b50610f4a8061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061007a575f3560e01c806381a95e641161005857806381a95e64146100be578063bc8bc2b4146100d1578063dd68afb6146100f5578063ece28c6c14610115575f5ffd5b80631a6fd31b1461007e57806320fe214414610094578063409f0ef6146100a9575b5f5ffd5b5f545b6040519081526020015b60405180910390f35b6100a76100a23660046109ed565b610128565b005b6100b16102d9565b60405161008b9190610a04565b6100a76100cc366004610a8b565b610337565b6100e46100df3660046109ed565b61055f565b60405161008b959493929190610b3c565b6101086101033660046109ed565b610622565b60405161008b9190610b84565b610081610123366004610be4565b61079c565b805f8111801561013957505f548111155b61015e5760405162461bcd60e51b815260040161015590610c2c565b60405180910390fd5b5f81815260016020819052604090912001546001600160a01b03166101955760405162461bcd60e51b815260040161015590610c59565b5f828152600160208190526040909120015482906001600160a01b031633146101f05760405162461bcd60e51b815260206004820152600d60248201526c2737ba103a34329037bbb732b960991b6044820152606401610155565b5f8381526001602052604090206002600382015460ff16600281111561021857610218610b08565b146102655760405162461bcd60e51b815260206004820152601960248201527f546f646f20697320616c72656164792066696e616c697a6564000000000000006044820152606401610155565b80600401544211156102855760038101805460ff19166001179055610292565b60038101805460ff191690555b600381015460405185917f2da7b23ca63c1eb969eee5fae4acb98186abecf5358b0354a82a5183ebca6b2a916102cb9160ff1690610c83565b60405180910390a250505050565b335f9081526002602090815260409182902080548351818402810184019094528084526060939283018282801561032d57602002820191905f5260205f20905b815481526020019060010190808311610319575b5050505050905090565b835f8111801561034857505f548111155b6103645760405162461bcd60e51b815260040161015590610c2c565b5f81815260016020819052604090912001546001600160a01b031661039b5760405162461bcd60e51b815260040161015590610c59565b5f858152600160208190526040909120015485906001600160a01b031633146103f65760405162461bcd60e51b815260206004820152600d60248201526c2737ba103a34329037bbb732b960991b6044820152606401610155565b5f8681526001602052604090206002600382015460ff16600281111561041e5761041e610b08565b1461046b5760405162461bcd60e51b815260206004820152601b60248201527f43616e6e6f74206564697420636f6d706c6574656420746f646f7300000000006044820152606401610155565b846104af5760405162461bcd60e51b8152602060048201526014602482015273546578742063616e6e6f7420626520656d70747960601b6044820152606401610155565b6104bb42610258610cab565b84116105015760405162461bcd60e51b81526020600482015260156024820152742732bb903232b0b23634b732903a37b79039b7b7b760591b6044820152606401610155565b60028101610510868883610d50565b50838160040181905550867fa50a580af3fc8fb6e11279758762d8cf594b77ba6e0d024231d4421b7fad094987878760405161054e93929190610e0a565b60405180910390a250505050505050565b600160208190525f91825260409091208054918101546002820180546001600160a01b03909216929161059190610cd2565b80601f01602080910402602001604051908101604052809291908181526020018280546105bd90610cd2565b80156106085780601f106105df57610100808354040283529160200191610608565b820191905f5260205f20905b8154815290600101906020018083116105eb57829003601f168201915b505050506003830154600490930154919260ff1691905085565b61062a6109a8565b815f8111801561063b57505f548111155b6106575760405162461bcd60e51b815260040161015590610c2c565b5f81815260016020819052604090912001546001600160a01b031661068e5760405162461bcd60e51b815260040161015590610c59565b5f83815260016020818152604092839020835160a08101855281548152928101546001600160a01b03169183019190915260028101805492939192918401916106d690610cd2565b80601f016020809104026020016040519081016040528092919081815260200182805461070290610cd2565b801561074d5780601f106107245761010080835404028352916020019161074d565b820191905f5260205f20905b81548152906001019060200180831161073057829003601f168201915b5050509183525050600382015460209091019060ff16600281111561077457610774610b08565b600281111561078557610785610b08565b815260200160048201548152505091505b50919050565b5f826107d75760405162461bcd60e51b815260206004820152600a602482015269115b5c1d1e481d195e1d60b21b6044820152606401610155565b6107e342610258610cab565b82116108405760405162461bcd60e51b815260206004820152602660248201527f446561646c696e65206d757374206265206174206c65617374203130206d696e60448201526573206177617960d01b6064820152608401610155565b5f8054908061084e83610e41565b91905055506040518060a001604052805f548152602001336001600160a01b0316815260200185858080601f0160208091040260200160405190810160405280939291908181526020018383808284375f920191909152505050908252506020016002815260209081018490525f805481526001808352604091829020845181559284015190830180546001600160a01b0319166001600160a01b0390921691909117905582015160028201906109059082610e59565b50606082015160038201805460ff1916600183600281111561092957610929610b08565b021790555060809190910151600490910155335f818152600260209081526040808320835481546001810183559185529284200191909155905490517f6cb4a8fa4444e5f73f72e217a9bedb4ba086638bf9cf22f32bab1dd5e393424f9061099690889088908890610e0a565b60405180910390a3505f549392505050565b6040518060a001604052805f81526020015f6001600160a01b03168152602001606081526020015f60028111156109e1576109e1610b08565b81526020015f81525090565b5f602082840312156109fd575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b81811015610a3b578351835260209384019390920191600101610a1d565b509095945050505050565b5f5f83601f840112610a56575f5ffd5b50813567ffffffffffffffff811115610a6d575f5ffd5b602083019150836020828501011115610a84575f5ffd5b9250929050565b5f5f5f5f60608587031215610a9e575f5ffd5b84359350602085013567ffffffffffffffff811115610abb575f5ffd5b610ac787828801610a46565b9598909750949560400135949350505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b634e487b7160e01b5f52602160045260245ffd5b60038110610b3857634e487b7160e01b5f52602160045260245ffd5b9052565b8581526001600160a01b038516602082015260a0604082018190525f90610b6590830186610ada565b9050610b746060830185610b1c565b8260808301529695505050505050565b602081528151602082015260018060a01b0360208301511660408201525f604083015160a06060840152610bbb60c0840182610ada565b90506060840151610bcf6080850182610b1c565b50608084015160a08401528091505092915050565b5f5f5f60408486031215610bf6575f5ffd5b833567ffffffffffffffff811115610c0c575f5ffd5b610c1886828701610a46565b909790965060209590950135949350505050565b602080825260139082015272151bd91bc8191bd95cc81b9bdd08195e1a5cdd606a1b604082015260600190565b60208082526010908201526f151bd91bc81dd85cc819195b195d195960821b604082015260600190565b60208101610c918284610b1c565b92915050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610c9157610c91610c97565b634e487b7160e01b5f52604160045260245ffd5b600181811c90821680610ce657607f821691505b60208210810361079657634e487b7160e01b5f52602260045260245ffd5b601f821115610d4b57805f5260205f20601f840160051c81016020851015610d295750805b601f840160051c820191505b81811015610d48575f8155600101610d35565b50505b505050565b67ffffffffffffffff831115610d6857610d68610cbe565b610d7c83610d768354610cd2565b83610d04565b5f601f841160018114610dad575f8515610d965750838201355b5f19600387901b1c1916600186901b178355610d48565b5f83815260208120601f198716915b82811015610ddc5786850135825560209485019460019092019101610dbc565b5086821015610df8575f1960f88860031b161c19848701351681555b505060018560011b0183555050505050565b60408152826040820152828460608301375f606084830101525f6060601f19601f8601168301019050826020830152949350505050565b5f60018201610e5257610e52610c97565b5060010190565b815167ffffffffffffffff811115610e7357610e73610cbe565b610e8781610e818454610cd2565b84610d04565b6020601f821160018114610eb9575f8315610ea25750848201515b5f19600385901b1c1916600184901b178455610d48565b5f84815260208120601f198516915b82811015610ee85787850151825560209485019460019092019101610ec8565b5084821015610f0557868401515f19600387901b60f8161c191681555b50505050600190811b0190555056fea2646970667358221220c1fe6fbf47cd16c51a4bacc86a60850e2ae6ad32b8ae5be3f1ce8b1a81ecc5ca64736f6c634300081c0033", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b506004361061007a575f3560e01c806381a95e641161005857806381a95e64146100be578063bc8bc2b4146100d1578063dd68afb6146100f5578063ece28c6c14610115575f5ffd5b80631a6fd31b1461007e57806320fe214414610094578063409f0ef6146100a9575b5f5ffd5b5f545b6040519081526020015b60405180910390f35b6100a76100a23660046109ed565b610128565b005b6100b16102d9565b60405161008b9190610a04565b6100a76100cc366004610a8b565b610337565b6100e46100df3660046109ed565b61055f565b60405161008b959493929190610b3c565b6101086101033660046109ed565b610622565b60405161008b9190610b84565b610081610123366004610be4565b61079c565b805f8111801561013957505f548111155b61015e5760405162461bcd60e51b815260040161015590610c2c565b60405180910390fd5b5f81815260016020819052604090912001546001600160a01b03166101955760405162461bcd60e51b815260040161015590610c59565b5f828152600160208190526040909120015482906001600160a01b031633146101f05760405162461bcd60e51b815260206004820152600d60248201526c2737ba103a34329037bbb732b960991b6044820152606401610155565b5f8381526001602052604090206002600382015460ff16600281111561021857610218610b08565b146102655760405162461bcd60e51b815260206004820152601960248201527f546f646f20697320616c72656164792066696e616c697a6564000000000000006044820152606401610155565b80600401544211156102855760038101805460ff19166001179055610292565b60038101805460ff191690555b600381015460405185917f2da7b23ca63c1eb969eee5fae4acb98186abecf5358b0354a82a5183ebca6b2a916102cb9160ff1690610c83565b60405180910390a250505050565b335f9081526002602090815260409182902080548351818402810184019094528084526060939283018282801561032d57602002820191905f5260205f20905b815481526020019060010190808311610319575b5050505050905090565b835f8111801561034857505f548111155b6103645760405162461bcd60e51b815260040161015590610c2c565b5f81815260016020819052604090912001546001600160a01b031661039b5760405162461bcd60e51b815260040161015590610c59565b5f858152600160208190526040909120015485906001600160a01b031633146103f65760405162461bcd60e51b815260206004820152600d60248201526c2737ba103a34329037bbb732b960991b6044820152606401610155565b5f8681526001602052604090206002600382015460ff16600281111561041e5761041e610b08565b1461046b5760405162461bcd60e51b815260206004820152601b60248201527f43616e6e6f74206564697420636f6d706c6574656420746f646f7300000000006044820152606401610155565b846104af5760405162461bcd60e51b8152602060048201526014602482015273546578742063616e6e6f7420626520656d70747960601b6044820152606401610155565b6104bb42610258610cab565b84116105015760405162461bcd60e51b81526020600482015260156024820152742732bb903232b0b23634b732903a37b79039b7b7b760591b6044820152606401610155565b60028101610510868883610d50565b50838160040181905550867fa50a580af3fc8fb6e11279758762d8cf594b77ba6e0d024231d4421b7fad094987878760405161054e93929190610e0a565b60405180910390a250505050505050565b600160208190525f91825260409091208054918101546002820180546001600160a01b03909216929161059190610cd2565b80601f01602080910402602001604051908101604052809291908181526020018280546105bd90610cd2565b80156106085780601f106105df57610100808354040283529160200191610608565b820191905f5260205f20905b8154815290600101906020018083116105eb57829003601f168201915b505050506003830154600490930154919260ff1691905085565b61062a6109a8565b815f8111801561063b57505f548111155b6106575760405162461bcd60e51b815260040161015590610c2c565b5f81815260016020819052604090912001546001600160a01b031661068e5760405162461bcd60e51b815260040161015590610c59565b5f83815260016020818152604092839020835160a08101855281548152928101546001600160a01b03169183019190915260028101805492939192918401916106d690610cd2565b80601f016020809104026020016040519081016040528092919081815260200182805461070290610cd2565b801561074d5780601f106107245761010080835404028352916020019161074d565b820191905f5260205f20905b81548152906001019060200180831161073057829003601f168201915b5050509183525050600382015460209091019060ff16600281111561077457610774610b08565b600281111561078557610785610b08565b815260200160048201548152505091505b50919050565b5f826107d75760405162461bcd60e51b815260206004820152600a602482015269115b5c1d1e481d195e1d60b21b6044820152606401610155565b6107e342610258610cab565b82116108405760405162461bcd60e51b815260206004820152602660248201527f446561646c696e65206d757374206265206174206c65617374203130206d696e60448201526573206177617960d01b6064820152608401610155565b5f8054908061084e83610e41565b91905055506040518060a001604052805f548152602001336001600160a01b0316815260200185858080601f0160208091040260200160405190810160405280939291908181526020018383808284375f920191909152505050908252506020016002815260209081018490525f805481526001808352604091829020845181559284015190830180546001600160a01b0319166001600160a01b0390921691909117905582015160028201906109059082610e59565b50606082015160038201805460ff1916600183600281111561092957610929610b08565b021790555060809190910151600490910155335f818152600260209081526040808320835481546001810183559185529284200191909155905490517f6cb4a8fa4444e5f73f72e217a9bedb4ba086638bf9cf22f32bab1dd5e393424f9061099690889088908890610e0a565b60405180910390a3505f549392505050565b6040518060a001604052805f81526020015f6001600160a01b03168152602001606081526020015f60028111156109e1576109e1610b08565b81526020015f81525090565b5f602082840312156109fd575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b81811015610a3b578351835260209384019390920191600101610a1d565b509095945050505050565b5f5f83601f840112610a56575f5ffd5b50813567ffffffffffffffff811115610a6d575f5ffd5b602083019150836020828501011115610a84575f5ffd5b9250929050565b5f5f5f5f60608587031215610a9e575f5ffd5b84359350602085013567ffffffffffffffff811115610abb575f5ffd5b610ac787828801610a46565b9598909750949560400135949350505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b634e487b7160e01b5f52602160045260245ffd5b60038110610b3857634e487b7160e01b5f52602160045260245ffd5b9052565b8581526001600160a01b038516602082015260a0604082018190525f90610b6590830186610ada565b9050610b746060830185610b1c565b8260808301529695505050505050565b602081528151602082015260018060a01b0360208301511660408201525f604083015160a06060840152610bbb60c0840182610ada565b90506060840151610bcf6080850182610b1c565b50608084015160a08401528091505092915050565b5f5f5f60408486031215610bf6575f5ffd5b833567ffffffffffffffff811115610c0c575f5ffd5b610c1886828701610a46565b909790965060209590950135949350505050565b602080825260139082015272151bd91bc8191bd95cc81b9bdd08195e1a5cdd606a1b604082015260600190565b60208082526010908201526f151bd91bc81dd85cc819195b195d195960821b604082015260600190565b60208101610c918284610b1c565b92915050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610c9157610c91610c97565b634e487b7160e01b5f52604160045260245ffd5b600181811c90821680610ce657607f821691505b60208210810361079657634e487b7160e01b5f52602260045260245ffd5b601f821115610d4b57805f5260205f20601f840160051c81016020851015610d295750805b601f840160051c820191505b81811015610d48575f8155600101610d35565b50505b505050565b67ffffffffffffffff831115610d6857610d68610cbe565b610d7c83610d768354610cd2565b83610d04565b5f601f841160018114610dad575f8515610d965750838201355b5f19600387901b1c1916600186901b178355610d48565b5f83815260208120601f198716915b82811015610ddc5786850135825560209485019460019092019101610dbc565b5086821015610df8575f1960f88860031b161c19848701351681555b505060018560011b0183555050505050565b60408152826040820152828460608301375f606084830101525f6060601f19601f8601168301019050826020830152949350505050565b5f60018201610e5257610e52610c97565b5060010190565b815167ffffffffffffffff811115610e7357610e73610cbe565b610e8781610e818454610cd2565b84610d04565b6020601f821160018114610eb9575f8315610ea25750848201515b5f19600385901b1c1916600184901b178455610d48565b5f84815260208120601f198516915b82811015610ee85787850151825560209485019460019092019101610ec8565b5084821015610f0557868401515f19600387901b60f8161c191681555b50505050600190811b0190555056fea2646970667358221220c1fe6fbf47cd16c51a4bacc86a60850e2ae6ad32b8ae5be3f1ce8b1a81ecc5ca64736f6c634300081c0033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/Todo.sol", + "buildInfoId": "solc-0_8_28-1fd892f99d99f6ca68aad2e2334b2f514f512bd1" +} \ No newline at end of file diff --git a/assignments/Todo/ignition/deployments/chain-11155111/deployed_addresses.json b/assignments/Todo/ignition/deployments/chain-11155111/deployed_addresses.json new file mode 100644 index 00000000..2e869c23 --- /dev/null +++ b/assignments/Todo/ignition/deployments/chain-11155111/deployed_addresses.json @@ -0,0 +1,3 @@ +{ + "TodoModule#TodoContract": "0xaAee9Be4E1F57cde21976adacF502D2349054506" +} diff --git a/assignments/Todo/ignition/deployments/chain-11155111/journal.jsonl b/assignments/Todo/ignition/deployments/chain-11155111/journal.jsonl new file mode 100644 index 00000000..5d2306d6 --- /dev/null +++ b/assignments/Todo/ignition/deployments/chain-11155111/journal.jsonl @@ -0,0 +1,8 @@ + +{"chainId":11155111,"type":"DEPLOYMENT_INITIALIZE"} +{"artifactId":"TodoModule#TodoContract","constructorArgs":[],"contractName":"TodoContract","dependencies":[],"from":"0x4e1b1d9af926e7e0fbcb9c5b23eeda9d80642b99","futureId":"TodoModule#TodoContract","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"0"}} +{"futureId":"TodoModule#TodoContract","networkInteraction":{"data":"0x6080604052348015600e575f5ffd5b50610f4a8061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061007a575f3560e01c806381a95e641161005857806381a95e64146100be578063bc8bc2b4146100d1578063dd68afb6146100f5578063ece28c6c14610115575f5ffd5b80631a6fd31b1461007e57806320fe214414610094578063409f0ef6146100a9575b5f5ffd5b5f545b6040519081526020015b60405180910390f35b6100a76100a23660046109ed565b610128565b005b6100b16102d9565b60405161008b9190610a04565b6100a76100cc366004610a8b565b610337565b6100e46100df3660046109ed565b61055f565b60405161008b959493929190610b3c565b6101086101033660046109ed565b610622565b60405161008b9190610b84565b610081610123366004610be4565b61079c565b805f8111801561013957505f548111155b61015e5760405162461bcd60e51b815260040161015590610c2c565b60405180910390fd5b5f81815260016020819052604090912001546001600160a01b03166101955760405162461bcd60e51b815260040161015590610c59565b5f828152600160208190526040909120015482906001600160a01b031633146101f05760405162461bcd60e51b815260206004820152600d60248201526c2737ba103a34329037bbb732b960991b6044820152606401610155565b5f8381526001602052604090206002600382015460ff16600281111561021857610218610b08565b146102655760405162461bcd60e51b815260206004820152601960248201527f546f646f20697320616c72656164792066696e616c697a6564000000000000006044820152606401610155565b80600401544211156102855760038101805460ff19166001179055610292565b60038101805460ff191690555b600381015460405185917f2da7b23ca63c1eb969eee5fae4acb98186abecf5358b0354a82a5183ebca6b2a916102cb9160ff1690610c83565b60405180910390a250505050565b335f9081526002602090815260409182902080548351818402810184019094528084526060939283018282801561032d57602002820191905f5260205f20905b815481526020019060010190808311610319575b5050505050905090565b835f8111801561034857505f548111155b6103645760405162461bcd60e51b815260040161015590610c2c565b5f81815260016020819052604090912001546001600160a01b031661039b5760405162461bcd60e51b815260040161015590610c59565b5f858152600160208190526040909120015485906001600160a01b031633146103f65760405162461bcd60e51b815260206004820152600d60248201526c2737ba103a34329037bbb732b960991b6044820152606401610155565b5f8681526001602052604090206002600382015460ff16600281111561041e5761041e610b08565b1461046b5760405162461bcd60e51b815260206004820152601b60248201527f43616e6e6f74206564697420636f6d706c6574656420746f646f7300000000006044820152606401610155565b846104af5760405162461bcd60e51b8152602060048201526014602482015273546578742063616e6e6f7420626520656d70747960601b6044820152606401610155565b6104bb42610258610cab565b84116105015760405162461bcd60e51b81526020600482015260156024820152742732bb903232b0b23634b732903a37b79039b7b7b760591b6044820152606401610155565b60028101610510868883610d50565b50838160040181905550867fa50a580af3fc8fb6e11279758762d8cf594b77ba6e0d024231d4421b7fad094987878760405161054e93929190610e0a565b60405180910390a250505050505050565b600160208190525f91825260409091208054918101546002820180546001600160a01b03909216929161059190610cd2565b80601f01602080910402602001604051908101604052809291908181526020018280546105bd90610cd2565b80156106085780601f106105df57610100808354040283529160200191610608565b820191905f5260205f20905b8154815290600101906020018083116105eb57829003601f168201915b505050506003830154600490930154919260ff1691905085565b61062a6109a8565b815f8111801561063b57505f548111155b6106575760405162461bcd60e51b815260040161015590610c2c565b5f81815260016020819052604090912001546001600160a01b031661068e5760405162461bcd60e51b815260040161015590610c59565b5f83815260016020818152604092839020835160a08101855281548152928101546001600160a01b03169183019190915260028101805492939192918401916106d690610cd2565b80601f016020809104026020016040519081016040528092919081815260200182805461070290610cd2565b801561074d5780601f106107245761010080835404028352916020019161074d565b820191905f5260205f20905b81548152906001019060200180831161073057829003601f168201915b5050509183525050600382015460209091019060ff16600281111561077457610774610b08565b600281111561078557610785610b08565b815260200160048201548152505091505b50919050565b5f826107d75760405162461bcd60e51b815260206004820152600a602482015269115b5c1d1e481d195e1d60b21b6044820152606401610155565b6107e342610258610cab565b82116108405760405162461bcd60e51b815260206004820152602660248201527f446561646c696e65206d757374206265206174206c65617374203130206d696e60448201526573206177617960d01b6064820152608401610155565b5f8054908061084e83610e41565b91905055506040518060a001604052805f548152602001336001600160a01b0316815260200185858080601f0160208091040260200160405190810160405280939291908181526020018383808284375f920191909152505050908252506020016002815260209081018490525f805481526001808352604091829020845181559284015190830180546001600160a01b0319166001600160a01b0390921691909117905582015160028201906109059082610e59565b50606082015160038201805460ff1916600183600281111561092957610929610b08565b021790555060809190910151600490910155335f818152600260209081526040808320835481546001810183559185529284200191909155905490517f6cb4a8fa4444e5f73f72e217a9bedb4ba086638bf9cf22f32bab1dd5e393424f9061099690889088908890610e0a565b60405180910390a3505f549392505050565b6040518060a001604052805f81526020015f6001600160a01b03168152602001606081526020015f60028111156109e1576109e1610b08565b81526020015f81525090565b5f602082840312156109fd575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b81811015610a3b578351835260209384019390920191600101610a1d565b509095945050505050565b5f5f83601f840112610a56575f5ffd5b50813567ffffffffffffffff811115610a6d575f5ffd5b602083019150836020828501011115610a84575f5ffd5b9250929050565b5f5f5f5f60608587031215610a9e575f5ffd5b84359350602085013567ffffffffffffffff811115610abb575f5ffd5b610ac787828801610a46565b9598909750949560400135949350505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b634e487b7160e01b5f52602160045260245ffd5b60038110610b3857634e487b7160e01b5f52602160045260245ffd5b9052565b8581526001600160a01b038516602082015260a0604082018190525f90610b6590830186610ada565b9050610b746060830185610b1c565b8260808301529695505050505050565b602081528151602082015260018060a01b0360208301511660408201525f604083015160a06060840152610bbb60c0840182610ada565b90506060840151610bcf6080850182610b1c565b50608084015160a08401528091505092915050565b5f5f5f60408486031215610bf6575f5ffd5b833567ffffffffffffffff811115610c0c575f5ffd5b610c1886828701610a46565b909790965060209590950135949350505050565b602080825260139082015272151bd91bc8191bd95cc81b9bdd08195e1a5cdd606a1b604082015260600190565b60208082526010908201526f151bd91bc81dd85cc819195b195d195960821b604082015260600190565b60208101610c918284610b1c565b92915050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610c9157610c91610c97565b634e487b7160e01b5f52604160045260245ffd5b600181811c90821680610ce657607f821691505b60208210810361079657634e487b7160e01b5f52602260045260245ffd5b601f821115610d4b57805f5260205f20601f840160051c81016020851015610d295750805b601f840160051c820191505b81811015610d48575f8155600101610d35565b50505b505050565b67ffffffffffffffff831115610d6857610d68610cbe565b610d7c83610d768354610cd2565b83610d04565b5f601f841160018114610dad575f8515610d965750838201355b5f19600387901b1c1916600186901b178355610d48565b5f83815260208120601f198716915b82811015610ddc5786850135825560209485019460019092019101610dbc565b5086821015610df8575f1960f88860031b161c19848701351681555b505060018560011b0183555050505050565b60408152826040820152828460608301375f606084830101525f6060601f19601f8601168301019050826020830152949350505050565b5f60018201610e5257610e52610c97565b5060010190565b815167ffffffffffffffff811115610e7357610e73610cbe565b610e8781610e818454610cd2565b84610d04565b6020601f821160018114610eb9575f8315610ea25750848201515b5f19600385901b1c1916600184901b178455610d48565b5f84815260208120601f198516915b82811015610ee85787850151825560209485019460019092019101610ec8565b5084821015610f0557868401515f19600387901b60f8161c191681555b50505050600190811b0190555056fea2646970667358221220c1fe6fbf47cd16c51a4bacc86a60850e2ae6ad32b8ae5be3f1ce8b1a81ecc5ca64736f6c634300081c0033","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"0"}},"type":"NETWORK_INTERACTION_REQUEST"} +{"futureId":"TodoModule#TodoContract","networkInteractionId":1,"nonce":4,"type":"TRANSACTION_PREPARE_SEND"} +{"futureId":"TodoModule#TodoContract","networkInteractionId":1,"nonce":4,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"8223202906"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000"}},"hash":"0x998799bd60a8dea4f59f418c79add98e8a989647b8699d7f2d24da6028b71cc7"},"type":"TRANSACTION_SEND"} +{"futureId":"TodoModule#TodoContract","hash":"0x998799bd60a8dea4f59f418c79add98e8a989647b8699d7f2d24da6028b71cc7","networkInteractionId":1,"receipt":{"blockHash":"0xe0b520b9181d82729772ece944b6f1306c057400f9861deb7dec3ed25958c86f","blockNumber":10271183,"contractAddress":"0xaAee9Be4E1F57cde21976adacF502D2349054506","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} +{"futureId":"TodoModule#TodoContract","result":{"address":"0xaAee9Be4E1F57cde21976adacF502D2349054506","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} \ No newline at end of file diff --git a/assignments/Todo/ignition/modules/Todo.ts b/assignments/Todo/ignition/modules/Todo.ts new file mode 100644 index 00000000..e77a73b6 --- /dev/null +++ b/assignments/Todo/ignition/modules/Todo.ts @@ -0,0 +1,7 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; + +export default buildModule('TodoModule', (m) => { + const todo = m.contract('TodoContract'); + + return { todo }; +}); diff --git a/assignments/Todo/package.json b/assignments/Todo/package.json new file mode 100644 index 00000000..39a827f0 --- /dev/null +++ b/assignments/Todo/package.json @@ -0,0 +1,23 @@ +{ + "name": "Todo", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.8", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.6", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + }, + "dependencies": { + "dotenv": "^17.3.1" + } +} diff --git a/assignments/Todo/scripts/send-op-tx.ts b/assignments/Todo/scripts/send-op-tx.ts new file mode 100644 index 00000000..681a76e0 --- /dev/null +++ b/assignments/Todo/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from 'hardhat'; + +const { ethers } = await network.connect({ + network: 'hardhatOp', + chainType: 'op', +}); + +console.log('Sending transaction using the OP chain type'); + +const [sender] = await ethers.getSigners(); + +console.log('Sending 1 wei from', sender.address, 'to itself'); + +console.log('Sending L2 transaction'); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log('Transaction sent successfully'); diff --git a/assignments/Todo/test/Todo.ts b/assignments/Todo/test/Todo.ts new file mode 100644 index 00000000..87d0d97f --- /dev/null +++ b/assignments/Todo/test/Todo.ts @@ -0,0 +1,126 @@ +//import { loadFixture, time } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from 'chai'; +import { network } from 'hardhat'; + +const { ethers, networkHelpers } = await network.connect(); + +const { time, loadFixture } = networkHelpers; + +describe('TodoContract', function () { + const TODO_TEXT = 'Finish Solidity Project'; + const TEN_MINUTES = 600; + + async function deployTodoFixture() { + const [owner, otherAccount] = await ethers.getSigners(); + const TodoContract = await ethers.getContractFactory('TodoContract'); + const todoContract = (await TodoContract.deploy()) as any; + + return { todoContract, owner, otherAccount }; + } + + describe('Creation', function () { + it('Should create a todo with Pending status', async function () { + const { todoContract, owner } = await loadFixture(deployTodoFixture); + const deadline = (await time.latest()) + TEN_MINUTES + 100; + + await expect(todoContract.createTodo(TODO_TEXT, deadline)) + .to.emit(todoContract, 'CreatedTodo') + .withArgs(1, owner.address, TODO_TEXT, deadline); + + const todo = await todoContract.getTodo(1); + expect(todo.text).to.equal(TODO_TEXT); + expect(todo.status).to.equal(2); // Status.Pending + }); + + it('Should fail if deadline is too soon', async function () { + const { todoContract } = await loadFixture(deployTodoFixture); + const invalidDeadline = (await time.latest()) + 100; // Less than 600s + + await expect( + todoContract.createTodo(TODO_TEXT, invalidDeadline) + ).to.be.revertedWith('Deadline must be at least 10 mins away'); + }); + }); + + describe('Completion Logic (The State Machine)', function () { + it('Should mark status as Done if completed before deadline', async function () { + const { todoContract } = await loadFixture(deployTodoFixture); + const deadline = (await time.latest()) + TEN_MINUTES + 1000; + + await todoContract.createTodo(TODO_TEXT, deadline); + + // Complete it immediately + await todoContract.completeTodo(1); + + const todo = await todoContract.getTodo(1); + expect(todo.status).to.equal(0); // Status.Done + }); + + it('Should mark status as Defaulted if completed after deadline', async function () { + const { todoContract } = await loadFixture(deployTodoFixture); + const deadline = (await time.latest()) + TEN_MINUTES + 500; + + await todoContract.createTodo(TODO_TEXT, deadline); + + // Fast forward time past the deadline + await time.increaseTo(deadline + 1); + + await todoContract.completeTodo(1); + + const todo = await todoContract.getTodo(1); + expect(todo.status).to.equal(1); // Status.Defaulted + }); + }); + + describe('Editing and Permissions', function () { + it('Should allow the owner to edit a pending todo', async function () { + const { todoContract } = await loadFixture(deployTodoFixture); + const deadline = (await time.latest()) + TEN_MINUTES + 1000; + await todoContract.createTodo(TODO_TEXT, deadline); + + const newText = 'Updated Task'; + await todoContract.editTodo(1, newText, deadline + 100); + + const todo = await todoContract.getTodo(1); + expect(todo.text).to.equal(newText); + }); + + it('Should fail if a non-owner tries to complete a todo', async function () { + const { todoContract, otherAccount } = + await loadFixture(deployTodoFixture); + const deadline = (await time.latest()) + TEN_MINUTES + 1000; + await todoContract.createTodo(TODO_TEXT, deadline); + + await expect( + todoContract.connect(otherAccount).completeTodo(1) + ).to.be.revertedWith('Not the owner'); + }); + + it('Should prevent editing a todo that is already completed', async function () { + const { todoContract } = await loadFixture(deployTodoFixture); + const deadline = (await time.latest()) + TEN_MINUTES + 1000; + await todoContract.createTodo(TODO_TEXT, deadline); + + await todoContract.completeTodo(1); + + await expect( + todoContract.editTodo(1, 'New Text', deadline + 2000) + ).to.be.revertedWith('Cannot edit completed todos'); + }); + }); + + describe('Getters', function () { + it('Should correctly track user todo IDs', async function () { + const { todoContract, owner } = await loadFixture(deployTodoFixture); + const deadline = (await time.latest()) + TEN_MINUTES + 1000; + + await todoContract.createTodo('Task 1', deadline); + await todoContract.createTodo('Task 2', deadline); + + const myIds = await todoContract.getMyTodoIds(); + expect(myIds.length).to.equal(2); + expect(myIds[0]).to.equal(1n); + expect(myIds[1]).to.equal(2n); + }); + }); +}); diff --git a/assignments/Todo/tsconfig.json b/assignments/Todo/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/Todo/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} diff --git a/assignments/foundry-test-assignment/.github/workflows/test.yml b/assignments/foundry-test-assignment/.github/workflows/test.yml new file mode 100644 index 00000000..b79c8d4f --- /dev/null +++ b/assignments/foundry-test-assignment/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: CI + +permissions: {} + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: forge --version + + - name: Run Forge fmt + run: forge fmt --check + + - name: Run Forge build + run: forge build --sizes + + - name: Run Forge tests + run: forge test -vvv diff --git a/assignments/foundry-test-assignment/.gitignore b/assignments/foundry-test-assignment/.gitignore new file mode 100644 index 00000000..18d49a69 --- /dev/null +++ b/assignments/foundry-test-assignment/.gitignore @@ -0,0 +1,17 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + + +lib/ \ No newline at end of file diff --git a/assignments/foundry-test-assignment/.gitmodules b/assignments/foundry-test-assignment/.gitmodules new file mode 100644 index 00000000..888d42dc --- /dev/null +++ b/assignments/foundry-test-assignment/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/assignments/foundry-test-assignment/README.md b/assignments/foundry-test-assignment/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/assignments/foundry-test-assignment/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/assignments/foundry-test-assignment/foundry.lock b/assignments/foundry-test-assignment/foundry.lock new file mode 100644 index 00000000..ab2c6d82 --- /dev/null +++ b/assignments/foundry-test-assignment/foundry.lock @@ -0,0 +1,14 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.15.0", + "rev": "0844d7e1fc5e60d77b68e469bff60265f236c398" + } + }, + "lib/openzeppelin-contracts": { + "tag": { + "name": "v5.6.1", + "rev": "5fd1781b1454fd1ef8e722282f86f9293cacf256" + } + } +} \ No newline at end of file diff --git a/assignments/foundry-test-assignment/foundry.toml b/assignments/foundry-test-assignment/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/assignments/foundry-test-assignment/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/assignments/foundry-test-assignment/lib/openzeppelin-contracts b/assignments/foundry-test-assignment/lib/openzeppelin-contracts new file mode 160000 index 00000000..5fd1781b --- /dev/null +++ b/assignments/foundry-test-assignment/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 5fd1781b1454fd1ef8e722282f86f9293cacf256 diff --git a/assignments/foundry-test-assignment/remappings.txt b/assignments/foundry-test-assignment/remappings.txt new file mode 100644 index 00000000..918ed315 --- /dev/null +++ b/assignments/foundry-test-assignment/remappings.txt @@ -0,0 +1,5 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ diff --git a/assignments/foundry-test-assignment/src/CounterV3.sol b/assignments/foundry-test-assignment/src/CounterV3.sol new file mode 100644 index 00000000..269dfb0c --- /dev/null +++ b/assignments/foundry-test-assignment/src/CounterV3.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract CounterV3 { + uint256 public number; + address public owner; + + mapping(address => bool) public approvedCallers; + mapping(address => bool) public pendingApproval; + + event ApprovalRequested(address indexed caller); + event ApprovalGranted(address indexed caller); + event ApprovalRevoked(address indexed caller); + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + modifier onlyAuthorized() { + if (msg.sender != owner) { + require(approvedCallers[msg.sender], "Owner consent required"); + } + _; + } + + function requestPrivilege() external { + require(msg.sender != owner, "Owner does not need consent"); + + pendingApproval[msg.sender] = true; + emit ApprovalRequested(msg.sender); + } + + function grantPrivilege(address caller) external onlyOwner { + require(pendingApproval[caller], "No pending request"); + + approvedCallers[caller] = true; + pendingApproval[caller] = false; + emit ApprovalGranted(caller); + } + + function revokePrivilege(address caller) external onlyOwner { + require(approvedCallers[caller], "Caller not approved"); + + approvedCallers[caller] = false; + emit ApprovalRevoked(caller); + } + + function setNumber(uint256 newNumber) public onlyAuthorized { + number = newNumber; + } + + function increment() public onlyAuthorized { + number++; + } +} diff --git a/assignments/foundry-test-assignment/src/Token.sol b/assignments/foundry-test-assignment/src/Token.sol new file mode 100644 index 00000000..50f223e5 --- /dev/null +++ b/assignments/foundry-test-assignment/src/Token.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.6.0 +pragma solidity ^0.8.27; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; + +contract JasonToken is ERC20, ERC20Burnable, Ownable { + constructor(address recipient, address initialOwner) ERC20("JasonToken", "JKB") Ownable(initialOwner) { + _mint(recipient, 1000 * 10 ** decimals()); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/assignments/foundry-test-assignment/src/Vault.sol b/assignments/foundry-test-assignment/src/Vault.sol new file mode 100644 index 00000000..4500dd36 --- /dev/null +++ b/assignments/foundry-test-assignment/src/Vault.sol @@ -0,0 +1,144 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract TimeLock { + struct Vault { + uint256 balance; + uint256 unlockTime; + bool active; + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + + function deposit(uint256 _unlockTime) external payable returns (uint256) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + // Create new vault + vaults[msg.sender].push(Vault({balance: msg.value, unlockTime: _unlockTime, active: true})); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.active = false; + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + userVaults[i].balance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns (uint256[] memory activeVaults, uint256[] memory balances, uint256[] memory unlockTimes) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/assignments/foundry-test-assignment/src/VaultV2.sol b/assignments/foundry-test-assignment/src/VaultV2.sol new file mode 100644 index 00000000..41a6c350 --- /dev/null +++ b/assignments/foundry-test-assignment/src/VaultV2.sol @@ -0,0 +1,217 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IErc20} from "./interfaces/IERC20.sol"; + +contract TimeLockV2 { + IErc20 public immutable token; + address public owner; + address public pendingOwner; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + struct Vault { + uint256 balance; + uint256 tokenBalance; + uint256 unlockTime; + bool active; + } + + constructor(address _token) { + token = IErc20(_token); + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "New owner is zero address"); + require(newOwner != owner, "New owner is current owner"); + pendingOwner = newOwner; + emit OwnershipTransferStarted(owner, newOwner); + } + + function acceptOwnership() external { + require(msg.sender == pendingOwner, "Not pending owner"); + address previousOwner = owner; + owner = msg.sender; + pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, msg.sender); + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + event EmergencyWithdrawn(address indexed owner, uint256 amount); + + //INTERNAL FUNCTIONS + function _depositRatio(uint256 _totalDeposit) internal pure returns (uint256 _tokenAmount) { + _tokenAmount = _totalDeposit * 10; // Token Ratio + } + + function deposit(uint256 _unlockTime) external payable returns (uint256) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + uint256 tokenBal = _depositRatio(msg.value); + require(token.transfer(msg.sender, tokenBal), "Token transfer failed"); + + // Create new vault + vaults[msg.sender].push( + Vault({balance: msg.value, unlockTime: _unlockTime, tokenBalance: tokenBal, active: true}) + ); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + uint256 tokenAmount = userVault.tokenBalance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.tokenBalance = 0; + userVault.active = false; + + require(token.transferFrom(msg.sender, address(this), tokenAmount), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + uint256 totalTokens = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + uint256 tokenAmount = userVaults[i].tokenBalance; + + userVaults[i].balance = 0; + userVaults[i].tokenBalance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + totalTokens += tokenAmount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + require(token.transfer(msg.sender, totalTokens), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function emergencyWithdraw() external onlyOwner returns (uint256 amount) { + amount = address(this).balance; + require(amount > 0, "No funds available"); + + (bool success,) = payable(owner).call{value: amount}(""); + if (!success) revert(); + + emit EmergencyWithdrawn(owner, amount); + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 tokenBalance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.tokenBalance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + tokenBalances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + tokenBalances[index] = userVaults[i].tokenBalance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, tokenBalances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/assignments/foundry-test-assignment/src/interfaces/IERC20.sol b/assignments/foundry-test-assignment/src/interfaces/IERC20.sol new file mode 100644 index 00000000..92e32621 --- /dev/null +++ b/assignments/foundry-test-assignment/src/interfaces/IERC20.sol @@ -0,0 +1,19 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IErc20 is IERC20 { + function decimals() external view returns (uint8); + + function totalSupply() external view returns (uint256); + + function balanceOf(address _owner) external view returns (uint256 balance); + + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); + + function mint(address to, uint256 amount) external; + + function burn(uint256 amount) external; + + function burnFrom(address account, uint256 amount) external; +} diff --git a/assignments/foundry-test-assignment/test/CounterV3.t.sol b/assignments/foundry-test-assignment/test/CounterV3.t.sol new file mode 100644 index 00000000..e3384801 --- /dev/null +++ b/assignments/foundry-test-assignment/test/CounterV3.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {CounterV3} from "../src/CounterV3.sol"; + +contract CounterV3Test is Test { + CounterV3 public counterv3; + address public addr1; + address public addr2; + + function setUp() public { + counterv3 = new CounterV3(); + addr1 = makeAddr("addr1"); + addr2 = makeAddr("addr2"); + } + + function test_setNumber() public { + counterv3.setNumber(3); + assertEq(counterv3.number(), 3); + } + + function test_increment() public { + counterv3.increment(); + assertEq(counterv3.number(), 1); + } + + function test_revert_increment_withoutPrivilege() public { + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.increment(); + + assertEq(counterv3.number(), 0); + } + + function test_revert_setNumber_withoutPrivilege() public { + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.setNumber(7); + + assertEq(counterv3.number(), 0); + } + + function test_grantPrivilege() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + assertTrue(counterv3.pendingApproval(addr1)); + + counterv3.grantPrivilege(addr1); + + assertTrue(counterv3.approvedCallers(addr1)); + assertFalse(counterv3.pendingApproval(addr1)); + } + + function test_approvedCaller_increment() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + + vm.prank(addr1); + counterv3.increment(); + + assertEq(counterv3.number(), 1); + } + + function test_revert_grantPrivilege_withoutRequest() public { + vm.expectRevert("No pending request"); + counterv3.grantPrivilege(addr1); + } + + function test_revokePrivilege() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + counterv3.revokePrivilege(addr1); + + assertFalse(counterv3.approvedCallers(addr1)); + } + + function test_revert_revokedCaller_setNumber() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + counterv3.revokePrivilege(addr1); + + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.setNumber(7); + } + + function test_revert_grantPrivilege_notOwner() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + vm.prank(addr2); + vm.expectRevert("Not owner"); + counterv3.grantPrivilege(addr1); + } + + function test_revert_revokePrivilege_notOwner() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + + vm.prank(addr2); + vm.expectRevert("Not owner"); + counterv3.revokePrivilege(addr1); + } + + function test_revert_revokePrivilege_withoutApproval() public { + vm.expectRevert("Caller not approved"); + counterv3.revokePrivilege(addr1); + } + + function test_revert_requestPrivilege_owner() public { + vm.expectRevert("Owner does not need consent"); + counterv3.requestPrivilege(); + } +} diff --git a/assignments/foundry-test-assignment/test/Vault.t.sol b/assignments/foundry-test-assignment/test/Vault.t.sol new file mode 100644 index 00000000..bdf2655b --- /dev/null +++ b/assignments/foundry-test-assignment/test/Vault.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {TimeLock} from "../src/Vault.sol"; + +contract VaultTest is Test { + TimeLock public vault; + address public user; + address public attacker; + + function setUp() public { + vault = new TimeLock(); + user = makeAddr("user"); + attacker = makeAddr("attacker"); + + assertEq(address(vault).balance, 0); + + vm.deal(user, 10 ether); + vm.deal(attacker, 10 ether); + } + + function test_deposit() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + assertEq(user.balance, userBalanceBefore - 1 ether); + assertEq(address(vault).balance, vaultBalanceBefore + 1 ether); + assertEq(vault.getVaultCount(user), 1); + + (uint256 balance, uint256 savedUnlockTime, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 1 ether); + assertEq(savedUnlockTime, unlockTime); + assertTrue(active); + assertFalse(isUnlocked); + } + + function test_revert_deposit_withZeroValue() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vm.expectRevert("Deposit must be greater than zero"); + vault.deposit{value: 0}(unlockTime); + } + + function test_revert_deposit_withNonFutureUnlockTime() public { + vm.prank(user); + vm.expectRevert("Unlock time must be in the future"); + vault.deposit{value: 1 ether}(block.timestamp); + } + + function test_revert_getVault_withInvalidVaultId() public { + vm.expectRevert("Invalid vault ID"); + vault.getVault(user, 0); + } + + function test_getAllVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + TimeLock.Vault[] memory vaults = vault.getAllVaults(user); + + assertEq(vaults.length, 2); + assertEq(vaults[0].balance, 1 ether); + assertEq(vaults[0].unlockTime, firstUnlockTime); + assertTrue(vaults[0].active); + assertEq(vaults[1].balance, 2 ether); + assertEq(vaults[1].unlockTime, secondUnlockTime); + assertTrue(vaults[1].active); + } + + function test_getActiveVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + (uint256[] memory activeVaults, uint256[] memory balances, uint256[] memory unlockTimes) = + vault.getActiveVaults(user); + + assertEq(activeVaults.length, 1); + assertEq(balances.length, 1); + assertEq(unlockTimes.length, 1); + assertEq(activeVaults[0], 1); + assertEq(balances[0], 2 ether); + assertEq(unlockTimes[0], secondUnlockTime); + } + + function test_getTotalBalance() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + assertEq(vault.getTotalBalance(user), 3 ether); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + assertEq(vault.getTotalBalance(user), 2 ether); + } + + function test_getUnlockedBalance() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + assertEq(vault.getUnlockedBalance(user), 0); + + vm.warp(firstUnlockTime); + assertEq(vault.getUnlockedBalance(user), 1 ether); + + vm.warp(secondUnlockTime); + assertEq(vault.getUnlockedBalance(user), 3 ether); + } + + function test_withdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.warp(unlockTime); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + vault.withdraw(0); + + assertEq(user.balance, userBalanceBefore + 1 ether); + assertEq(address(vault).balance, vaultBalanceBefore - 1 ether); + + (uint256 balance,, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 0); + assertFalse(active); + assertTrue(isUnlocked); + } + + function test_revert_withdraw_whenStillLocked() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(user); + vm.expectRevert("Funds are still locked"); + vault.withdraw(0); + } + + function test_revert_withdraw_fromAnotherUsersVault() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("Invalid vault ID"); + vault.withdraw(0); + } + + function test_withdrawAll() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(secondUnlockTime); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + uint256 amount = vault.withdrawAll(); + + assertEq(amount, 3 ether); + assertEq(user.balance, userBalanceBefore + 3 ether); + assertEq(address(vault).balance, vaultBalanceBefore - 3 ether); + assertEq(vault.getTotalBalance(user), 0); + } + + function test_revert_withdrawAll_fromUserWithoutUnlockedFunds() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("No unlocked funds available"); + vault.withdrawAll(); + } +} diff --git a/assignments/foundry-test-assignment/test/VaultV2.t.sol b/assignments/foundry-test-assignment/test/VaultV2.t.sol new file mode 100644 index 00000000..16f36564 --- /dev/null +++ b/assignments/foundry-test-assignment/test/VaultV2.t.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {TimeLockV2} from "../src/VaultV2.sol"; +import {JasonToken} from "../src/Token.sol"; + +contract VaultV2Test is Test { + TimeLockV2 public vault; + JasonToken public token; + address public owner; + address public user; + address public attacker; + address public newOwner; + + uint256 oneDay = 86400; + + function setUp() public { + owner = makeAddr("owner"); + vm.startPrank(owner); + token = new JasonToken(address(this), address(this)); + vault = new TimeLockV2(address(token)); + vm.stopPrank(); + user = makeAddr("user"); + attacker = makeAddr("attacker"); + newOwner = makeAddr("newOwner"); + + token.transfer(address(vault), 500 * 10 ** token.decimals()); + + vm.deal(user, 10 ether); + vm.deal(attacker, 10 ether); + } + + function test_ownerIsSetOnDeployment() public view { + assertEq(vault.owner(), owner); + console.log("Owner: ", vault.owner()); + console.log("This: ", owner); + } + + function test_emergencyWithdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 ownerBalanceBefore = owner.balance; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(owner); + uint256 withdrawnAmount = vault.emergencyWithdraw(); + + assertEq(withdrawnAmount, 1 ether); + assertEq(address(vault).balance, 0); + assertEq(owner.balance, ownerBalanceBefore + 1 ether); + } + + function test_revert_emergencyWithdraw_notOwner() public { + uint256 unlockTime = block.timestamp + oneDay; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert(); + vault.emergencyWithdraw(); + } + + function test_transferOwnership_and_acceptOwnership() public { + vm.prank(owner); + vault.transferOwnership(newOwner); + + assertEq(vault.pendingOwner(), newOwner); + + vm.prank(newOwner); + vault.acceptOwnership(); + + assertEq(vault.owner(), newOwner); + assertEq(vault.pendingOwner(), address(0)); + } + + function test_revert_transferOwnership_notOwner() public { + vm.prank(attacker); + vm.expectRevert("Not owner"); + vault.transferOwnership(newOwner); + } + + function test_revert_acceptOwnership_notPendingOwner() public { + vm.prank(owner); + vault.transferOwnership(newOwner); + + vm.prank(attacker); + vm.expectRevert("Not pending owner"); + vault.acceptOwnership(); + } + + function test_emergencyWithdraw_accessAfterOwnershipTransfer() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(owner); + vault.transferOwnership(newOwner); + + vm.prank(newOwner); + vault.acceptOwnership(); + + vm.prank(owner); + vm.expectRevert("Not owner"); + vault.emergencyWithdraw(); + + uint256 newOwnerBalanceBefore = newOwner.balance; + vm.prank(newOwner); + uint256 withdrawnAmount = vault.emergencyWithdraw(); + + assertEq(withdrawnAmount, 1 ether); + assertEq(address(vault).balance, 0); + assertEq(newOwner.balance, newOwnerBalanceBefore + 1 ether); + } + + function test_deposit() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + uint256 userTokenBefore = token.balanceOf(user); + uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + assertEq(user.balance, userEthBefore - 1 ether); + assertEq(address(vault).balance, vaultEthBefore + 1 ether); + assertEq(token.balanceOf(user), userTokenBefore + 10 ether); + assertEq(token.balanceOf(address(vault)), vaultTokenBefore - 10 ether); + assertEq(vault.getVaultCount(user), 1); + + (uint256 balance, uint256 tokenBalance, uint256 savedUnlockTime, bool active, bool isUnlocked) = + vault.getVault(user, 0); + + assertEq(balance, 1 ether); + assertEq(tokenBalance, 10 ether); + assertEq(savedUnlockTime, unlockTime); + assertTrue(active); + assertFalse(isUnlocked); + } + + function test_revert_deposit_withZeroValue() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vm.expectRevert("Deposit must be greater than zero"); + vault.deposit{value: 0}(unlockTime); + } + + function test_getActiveVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + token.approve(address(vault), 10 ether); + vm.stopPrank(); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) = vault.getActiveVaults(user); + + assertEq(activeVaults.length, 1); + assertEq(balances.length, 1); + assertEq(tokenBalances.length, 1); + assertEq(unlockTimes.length, 1); + assertEq(activeVaults[0], 1); + assertEq(balances[0], 2 ether); + assertEq(tokenBalances[0], 20 ether); + assertEq(unlockTimes[0], secondUnlockTime); + } + + function test_withdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(user); + token.approve(address(vault), 10 ether); + + vm.warp(unlockTime); + + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + uint256 userTokenBefore = token.balanceOf(user); + uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.withdraw(0); + + assertEq(user.balance, userEthBefore + 1 ether); + assertEq(address(vault).balance, vaultEthBefore - 1 ether); + assertEq(token.balanceOf(user), userTokenBefore - 10 ether); + assertEq(token.balanceOf(address(vault)), vaultTokenBefore + 10 ether); + + (uint256 balance, uint256 tokenBalance,, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 0); + assertEq(tokenBalance, 0); + assertFalse(active); + assertTrue(isUnlocked); + } + + function test_revert_withdraw_withoutApproval() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.warp(unlockTime); + + vm.prank(user); + vm.expectRevert(); + vault.withdraw(0); + } + + function test_withdrawAll() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(secondUnlockTime); + + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + + vm.prank(user); + uint256 amount = vault.withdrawAll(); + + assertEq(amount, 3 ether); + assertEq(user.balance, userEthBefore + 3 ether); + assertEq(address(vault).balance, vaultEthBefore - 3 ether); + assertEq(vault.getTotalBalance(user), 0); + } + + function test_revert_withdraw_fromAnotherUsersVault() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("Invalid vault ID"); + vault.withdraw(0); + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..efe1ce8b --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "dev": "node assignments/3-private-key/generate_address.js" + }, + "dependencies": { + "bip32": "^5.0.0", + "bip39": "^3.1.0", + "bitcoinjs-lib": "^7.0.1", + "crypto": "^1.0.1", + "js-sha3": "^0.9.3", + "tiny-secp256k1": "^2.2.4" + } +} diff --git a/rust_session/Cargo.lock b/rust_session/Cargo.lock new file mode 100644 index 00000000..bf268c9a --- /dev/null +++ b/rust_session/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "rust_session" +version = "0.1.0" diff --git a/rust_session/Cargo.toml b/rust_session/Cargo.toml new file mode 100644 index 00000000..c7d4ffab --- /dev/null +++ b/rust_session/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rust_session" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/rust_session/axum_server/Cargo.lock b/rust_session/axum_server/Cargo.lock new file mode 100644 index 00000000..272d9dec --- /dev/null +++ b/rust_session/axum_server/Cargo.lock @@ -0,0 +1,704 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum_server" +version = "0.1.0" +dependencies = [ + "axum", + "serde", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust_session/axum_server/Cargo.toml b/rust_session/axum_server/Cargo.toml new file mode 100644 index 00000000..2f818f62 --- /dev/null +++ b/rust_session/axum_server/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "axum_server" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = "0.8.9" +tokio = { version = "1", features = ["full"] } +serde = {version = "1.0.228", features = ["derive"]} +tracing = "0.1" +# env-filter lets us control log levels per crate, e.g. +# "tower_http=debug,info" — debug for tower_http, info for everything else +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + + +# tower-http = ready-made middleware for axum (it's built on tower). +# The "trace" feature gives us TraceLayer — production-grade +# request/response logging with timings. +tower-http = { version = "0.6", features = ["trace"] } \ No newline at end of file diff --git a/rust_session/axum_server/src/handlers/mod.rs b/rust_session/axum_server/src/handlers/mod.rs new file mode 100644 index 00000000..0bb9d16d --- /dev/null +++ b/rust_session/axum_server/src/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod root; +pub mod user_handler; diff --git a/rust_session/axum_server/src/handlers/root.rs b/rust_session/axum_server/src/handlers/root.rs new file mode 100644 index 00000000..a122633b --- /dev/null +++ b/rust_session/axum_server/src/handlers/root.rs @@ -0,0 +1,4 @@ +// basic handler that responds with a static string +pub async fn root() -> &'static str { + "Axum Server Live: Rust Class 🚀" +} diff --git a/rust_session/axum_server/src/handlers/todo_hander.rs b/rust_session/axum_server/src/handlers/todo_hander.rs new file mode 100644 index 00000000..f1593ab9 --- /dev/null +++ b/rust_session/axum_server/src/handlers/todo_hander.rs @@ -0,0 +1,19 @@ +use crate::schema::users::{CreateUserPayload, User}; + +use axum::{Json, http::StatusCode}; +pub async fn create_todo( + // this argument tells axum to parse the request body + // as JSON into a `CreateUserPayload` type + Json(payload): Json, +) -> (StatusCode, Json) { + // insert your application logic here + let user = User { + id: 1337, + username: payload.username, + age: payload.age, + }; + + // this will be converted into a JSON response + // with a status code of `201 Created` + (StatusCode::CREATED, Json(user)) +} diff --git a/rust_session/axum_server/src/handlers/user_handler.rs b/rust_session/axum_server/src/handlers/user_handler.rs new file mode 100644 index 00000000..975edfb2 --- /dev/null +++ b/rust_session/axum_server/src/handlers/user_handler.rs @@ -0,0 +1,19 @@ +use crate::schema::users::{CreateUserPayload, User}; + +use axum::{Json, http::StatusCode}; +pub async fn create_user( + // this argument tells axum to parse the request body + // as JSON into a `CreateUserPayload` type + Json(payload): Json, +) -> (StatusCode, Json) { + // insert your application logic here + let user = User { + id: 1337, + username: payload.username, + age: payload.age, + }; + + // this will be converted into a JSON response + // with a status code of `201 Created` + (StatusCode::CREATED, Json(user)) +} diff --git a/rust_session/axum_server/src/main.rs b/rust_session/axum_server/src/main.rs new file mode 100644 index 00000000..e2e24ed5 --- /dev/null +++ b/rust_session/axum_server/src/main.rs @@ -0,0 +1,18 @@ +mod handlers; +mod routes; +mod schema; + +const PORT: &str = "0.0.0.0:4440"; + +#[tokio::main] +async fn main() { + // initialize tracing + tracing_subscriber::fmt::init(); + + let app = routes::axum_router().await; + // run our app with hyper, listening globally on port 3000 + let listener = tokio::net::TcpListener::bind(PORT).await.unwrap(); + + tracing::info!("server running on {PORT}"); + axum::serve(listener, app).await.unwrap(); +} diff --git a/rust_session/axum_server/src/routes/mod.rs b/rust_session/axum_server/src/routes/mod.rs new file mode 100644 index 00000000..ffed60d6 --- /dev/null +++ b/rust_session/axum_server/src/routes/mod.rs @@ -0,0 +1,13 @@ +use crate::handlers::{root::root, user_handler::create_user}; +use axum::{ + Router, + routing::{get, post}, +}; +pub async fn axum_router() -> Router { + let app = Router::new() + // `GET /` goes to `root` + .route("/", get(root)) + // `POST /users` goes to `create_user` + .route("/users", post(create_user)); + app +} diff --git a/rust_session/axum_server/src/schema/mod.rs b/rust_session/axum_server/src/schema/mod.rs new file mode 100644 index 00000000..1056a828 --- /dev/null +++ b/rust_session/axum_server/src/schema/mod.rs @@ -0,0 +1,2 @@ +pub mod todo; +pub mod users; diff --git a/rust_session/axum_server/src/schema/todo.rs b/rust_session/axum_server/src/schema/todo.rs new file mode 100644 index 00000000..236b49cc --- /dev/null +++ b/rust_session/axum_server/src/schema/todo.rs @@ -0,0 +1,32 @@ +// use serde::{Deserialize, Serialize}; +// use uuid::Uuid; + +// #[derive(Serialize, Deserialize)] +// pub struct TodoPayload { +// pub name: String, + +// } + +// #[derive(Serialize, Deserialize)] +// pub struct Todo { +// pub id: Uuid, +// pub name: String, +// pub status: Status, +// } + +// pub enum Status { +// Started, +// Completed, +// InvalidEntry + +// } + +// impl Status { +// pub fn map_id_to_status(&self, x: u8) -> Self { +// match x { +// 1 => Status::Started, +// 2 => Status::Completed, +// _ => Status::InvalidEntry +// } +// } +// } diff --git a/rust_session/axum_server/src/schema/users.rs b/rust_session/axum_server/src/schema/users.rs new file mode 100644 index 00000000..751c7004 --- /dev/null +++ b/rust_session/axum_server/src/schema/users.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +// the input to our `create_user` handler +#[derive(Deserialize)] +pub struct CreateUserPayload { + pub username: String, + pub age: u8, +} + +// the output to our `create_user` handler +#[derive(Serialize)] +pub struct User { + pub id: u64, + pub username: String, + pub age: u8, +} diff --git a/rust_session/notes/borrowing.md b/rust_session/notes/borrowing.md new file mode 100644 index 00000000..5c4a011b --- /dev/null +++ b/rust_session/notes/borrowing.md @@ -0,0 +1,31 @@ +## 2. Borrowing + +Constantly moving ownership in and out of functions is awkward. **Borrowing** lets you give a function access to a value *without* transferring ownership. + +You borrow a value by passing a **reference** to it. + +```rust +fn greet(s: &String) { // `s` is a reference to a String + println!("Hello, {}!", s); +} // reference ends here; nothing is dropped + +fn main() { + let name = String::from("Carol"); + greet(&name); // pass a reference — `name` keeps ownership + + println!("Still have: {}", name); // ✅ name is still valid +} +``` + +The `&` means "borrow this, don't take it". The function can use the value but cannot drop it. + +### The two kinds of borrow + +| Kind | Syntax | Can read? | Can modify? | How many at once? | +|---|---|---|---|---| +| Shared (immutable) | `&T` | ✅ | ❌ | Unlimited | +| Exclusive (mutable) | `&mut T` | ✅ | ✅ | Exactly one | + +These rules together mean: **you can never have a mutable and an immutable reference to the same value at the same time.** + +--- diff --git a/rust_session/notes/overall-notes.md b/rust_session/notes/overall-notes.md new file mode 100644 index 00000000..5f5d7a9f --- /dev/null +++ b/rust_session/notes/overall-notes.md @@ -0,0 +1,479 @@ +# Rust Ownership, Borrowing & References +### A beginner's guide with annotated code + +--- + +## Why these concepts exist + +Most languages either make *you* manage memory (C, C++) or use a garbage collector to do it (Python, Java, Go). Rust takes a third path: the **compiler** enforces memory safety at compile time through three rules. No runtime cost, no GC pauses, no dangling pointers. + +The three concepts build on each other: + +``` +Ownership → who is responsible for a value +Borrowing → letting someone else use it temporarily +References → the actual mechanism for borrowing +``` + +--- + +## 1. Ownership + +Every value in Rust has exactly **one owner**. When the owner goes out of scope, the value is dropped (freed). + +```rust +fn main() { + let name = String::from("Alice"); // `name` is the owner of this String + println!("{}", name); +} // `name` goes out of scope here → String is dropped automatically +``` + +### Ownership moves on assignment + +When you assign a heap value to another variable, ownership **moves** — the original variable becomes invalid. + +```rust +fn main() { + let a = String::from("hello"); + let b = a; // ownership moves from `a` to `b` + + // println!("{}", a); // ❌ compile error: `a` was moved + println!("{}", b); // ✅ `b` is the owner now +} +``` + +> **Why?** If both `a` and `b` owned the same heap memory, Rust would try to free it twice — a classic bug. Moving prevents this. + +### Ownership moves into functions too + +```rust +fn greet(s: String) { // `s` takes ownership + println!("Hello, {}!", s); +} // `s` is dropped here + +fn main() { + let name = String::from("Bob"); + greet(name); // ownership moves into `greet` + + // println!("{}", name); // ❌ compile error: `name` was moved +} +``` + +### Copy types are different + +Primitive types like `i32`, `bool`, `f64`, and `char` implement the `Copy` trait — they are copied, not moved, because they live entirely on the stack. + +```rust +fn main() { + let x = 5; + let y = x; // `x` is copied, not moved + + println!("{} {}", x, y); // ✅ both are valid — x is still usable +} +``` + +--- + +## 2. Borrowing + +Constantly moving ownership in and out of functions is awkward. **Borrowing** lets you give a function access to a value *without* transferring ownership. + +You borrow a value by passing a **reference** to it. + +```rust +fn greet(s: &String) { // `s` is a reference to a String + println!("Hello, {}!", s); +} // reference ends here; nothing is dropped + +fn main() { + let name = String::from("Carol"); + greet(&name); // pass a reference — `name` keeps ownership + + println!("Still have: {}", name); // ✅ name is still valid +} +``` + +The `&` means "borrow this, don't take it". The function can use the value but cannot drop it. + +### The two kinds of borrow + +| Kind | Syntax | Can read? | Can modify? | How many at once? | +|---|---|---|---|---| +| Shared (immutable) | `&T` | ✅ | ❌ | Unlimited | +| Exclusive (mutable) | `&mut T` | ✅ | ✅ | Exactly one | + +These rules together mean: **you can never have a mutable and an immutable reference to the same value at the same time.** + +--- + +## 3. References + +A reference is a pointer that is *guaranteed by the compiler* to be valid. No null pointers, no dangling pointers — ever. + +### Immutable reference (`&T`) + +```rust +fn print_length(s: &String) { + println!("Length: {}", s.len()); // can read + // s.push_str(" world"); // ❌ can't modify through &String +} + +fn main() { + let message = String::from("hello"); + + let r1 = &message; // first shared borrow + let r2 = &message; // second shared borrow — totally fine + + println!("{} and {}", r1, r2); // ✅ multiple readers are safe +} +``` + +### Mutable reference (`&mut T`) + +```rust +fn append_world(s: &mut String) { + s.push_str(", world"); // ✅ can modify through &mut +} + +fn main() { + let mut message = String::from("hello"); // must be `mut` to borrow mutably + append_world(&mut message); + println!("{}", message); // "hello, world" +} +``` + +### The exclusivity rule in action + +```rust +fn main() { + let mut s = String::from("hello"); + + let r1 = &s; // immutable borrow + let r2 = &s; // another immutable borrow — fine + // let r3 = &mut s; // ❌ can't have &mut while &s exists + + println!("{} {}", r1, r2); + // r1 and r2 are no longer used after this point + + let r3 = &mut s; // ✅ now fine — r1 and r2 are done + r3.push_str("!"); + println!("{}", r3); +} +``` + +> **The rule in plain English:** You can have as many readers as you like *or* exactly one writer — never both at the same time. + +--- + +## 4. The slice — a special reference + +A **slice** is a reference to a _part_ of a collection. It borrows a window into the data without copying it. + +```rust +fn first_word(s: &str) -> &str { + let bytes = s.as_bytes(); + for (i, &byte) in bytes.iter().enumerate() { + if byte == b' ' { + return &s[0..i]; // return a slice up to the first space + } + } + &s[..] // whole string is one word +} + +fn main() { + let sentence = String::from("hello world"); + let word = first_word(&sentence); + println!("First word: {}", word); // "hello" +} +``` + +--- + +## 5. Putting it all together + +```rust +struct Player { + name: String, + score: u32, +} + +// Takes ownership of `player` — caller loses it +fn destroy(player: Player) { + println!("{} removed.", player.name); +} + +// Borrows immutably — caller keeps ownership, can't modify +fn describe(player: &Player) { + println!("{} has {} points.", player.name, player.score); +} + +// Borrows mutably — caller keeps ownership, function can modify +fn award_points(player: &mut Player, points: u32) { + player.score += points; +} + +fn main() { + let mut p = Player { + name: String::from("Dave"), + score: 0, + }; + + award_points(&mut p, 10); // mutably borrow + describe(&p); // immutably borrow — "Dave has 10 points." + award_points(&mut p, 5); // mutably borrow again + describe(&p); // "Dave has 15 points." + + destroy(p); // move ownership in — `p` is invalid after this + // describe(&p); // ❌ compile error: `p` was moved +} +``` + +--- + +## 6. Stack frames + +Think of the stack like a stack of trays in a cafeteria. Every time you call a function, a new tray is placed on top. Every local variable in that function lives on that tray. When the function returns, the tray is removed and everything on it disappears instantly. + +```rust +fn add(a: i32, b: i32) -> i32 { + // When `add` is called, a NEW frame is pushed onto the stack. + // `a`, `b`, and `result` all live inside this frame. + let result = a + b; + result + // Frame is popped here — `a`, `b`, `result` are gone. +} + +fn main() { + // main() has its own frame on the stack. + let x = 10; // lives in main's frame + let y = 20; // lives in main's frame + + let z = add(x, y); // a new frame for `add` is pushed on top of main's frame + // when add() returns, its frame is popped + // main's frame (x, y, z) is still there + + println!("{} + {} = {}", x, y, z); // 10 + 20 = 30 +} // main's frame is popped — program ends +``` + +### Each function call gets its own frame — even recursive ones + +```rust +fn countdown(n: i32) { + // Each call to `countdown` gets its OWN frame with its OWN copy of `n`. + // They do not share `n` — each frame is independent. + if n == 0 { + println!("Go!"); + return; // this frame is popped + } + println!("{}...", n); + countdown(n - 1); // a new frame is pushed on top, with n-1 + // when it returns, we come back to THIS frame +} // this frame is popped + +fn main() { + countdown(3); + // Stack during execution (top = most recent): + // countdown(0) ← top + // countdown(1) + // countdown(2) + // countdown(3) + // main ← bottom +} +``` + +> **Key rule:** the stack frame only exists while the function is running. You cannot return a reference to a local variable — the frame (and the variable) will be gone. + +```rust +// ❌ This does NOT compile — and that's a good thing. +fn make_number() -> &i32 { + let n = 42; + &n // can't return a reference to `n` — `n` dies when this frame pops +} + +// ✅ Return the value itself instead (copy it out of the frame). +fn make_number() -> i32 { + let n = 42; + n // value is copied into the caller's frame +} +``` + +--- + +## 7. The memory allocator + +When you need memory whose size isn't known until runtime — like a `String` the user types in, or a `Vec` that grows — the **allocator** steps in. It manages a large region of memory called the **heap** and hands out chunks on demand. + +In Rust you rarely talk to the allocator directly. Types like `String`, `Vec`, and `Box` do it for you. + +```rust +fn main() { + // String::from calls the allocator: "give me enough heap space for 5 bytes" + let s = String::from("hello"); + // s on the stack looks like: [ ptr | len=5 | cap=5 ] + // | + // └──► [ h | e | l | l | o ] ← heap + println!("{}", s); +} // s goes out of scope → Rust calls the allocator: "take this memory back" + // No manual free(), no garbage collector needed. +``` + +### The allocator grows memory when needed + +```rust +fn main() { + let mut v: Vec = Vec::new(); // allocator gives a small block on the heap + println!("len={}, cap={}", v.len(), v.capacity()); // len=0, cap=0 + + v.push(1); // not enough space → allocator gives a bigger block, data is moved + v.push(2); + v.push(3); + println!("len={}, cap={}", v.len(), v.capacity()); // len=3, cap=4 (typical) + + v.push(4); + v.push(5); // exceeded cap=4 → allocator gives an even bigger block again + println!("len={}, cap={}", v.len(), v.capacity()); // len=5, cap=8 (typical) +} // allocator reclaims all heap memory here +``` + +### `Box` — putting a single value on the heap explicitly + +```rust +fn main() { + let x: i32 = 7; // 7 lives on the stack + let b: Box = Box::new(7); // 7 lives on the heap; `b` is a pointer on the stack + + println!("stack x = {}", x); + println!("heap b = {}", b); // Box auto-derefs, prints 7 + + // When `b` goes out of scope, Box calls the allocator to free that heap slot. + // You never need to call free() yourself. +} +``` + +> **One sentence summary:** the stack is fast but temporary; the heap is flexible but requires the allocator to track what's in use. Rust's ownership rules ensure the allocator is always called exactly once to free each allocation. + +--- + +## 8. Pointers and addresses + +Every byte in your computer's memory has a numbered address, like a house number on a street. A **pointer** is simply a variable that stores one of those addresses — it *points to* where some data lives. + +In Rust, references (`&T`) *are* pointers under the hood. The compiler just adds safety rules on top. + +```rust +fn main() { + let x: i32 = 42; + + // `addr` is a raw pointer to x's address in memory. + // We use `&raw const` to get it without creating a safe reference. + let addr = &raw const x as usize; // cast address to a plain number + + println!("value of x : {}", x); + println!("address of x: 0x{:x}", addr); // e.g. 0x7ffee3b2c4bc (will vary each run) +} +``` + +The address printed will be different every time — the OS places your program at a different spot in memory on each run. What matters is the *concept*: `x` is data sitting at some address, and a pointer holds that address. + +### A reference IS a pointer — with compiler guarantees + +```rust +fn main() { + let x: i32 = 100; + let r: &i32 = &x; // `r` is a reference (a safe pointer) to `x` + + // Print the value `r` points to + println!("value : {}", *r); // dereference with `*` to get the value → 100 + + // Print the address `r` holds + println!("address: {:p}", r); // {:p} formats a reference as an address + // e.g. 0x7ffee3b2c490 +} +``` + +`{:p}` is the "pointer" format — it prints the memory address rather than the value. + +### Following the pointer — dereferencing + +Dereferencing (`*`) means "go to the address this pointer holds and give me what's there". + +```rust +fn main() { + let mut score: i32 = 50; + let r: &mut i32 = &mut score; // mutable reference — a pointer that allows writing + + println!("before: {}", score); // 50 + + *r = 99; // dereference + assign: go to the address, write 99 there + + println!("after : {}", score); // 99 ← `score` changed because `r` pointed at it +} +``` + +### Pointers to heap data — what `Box` looks like + +```rust +fn main() { + let b: Box = Box::new(7); + // b on the stack: [ 0x... ] ← an address + // | + // └──► [ 7 ] ← heap, at that address + + println!("b points to address : {:p}", b); // address on the heap + println!("value at that address: {}", *b); // 7 + + // Box is 8 bytes on a 64-bit system (just the address). + // The i32 itself is 4 bytes, living on the heap at that address. + println!("size of Box on stack: {} bytes", std::mem::size_of::>()); // 8 + println!("size of i32 on heap : {} bytes", std::mem::size_of::()); // 4 +} +``` + +### Visualising the relationship + +``` +Stack Heap +───────────────────── ────────────────── +b │ 0x55a3f1c0 │──────────► │ 7 │ (4 bytes at 0x55a3f1c0) + └────────────┘ └─────┘ + (8 bytes — just an address) +``` + +> **One sentence summary:** a pointer is just a number — a memory address. A Rust reference is a pointer that the compiler has verified is always valid, always properly aligned, and never aliased unsafely. + +--- + +## Quick-reference summary + +``` +OWNERSHIP + let x = value; x owns value + let y = x; ownership moves to y (x invalid for heap types) + let y = x.clone(); deep copy — both valid + +BORROWING + fn f(x: &T) immutable borrow — read only + fn f(x: &mut T) mutable borrow — read + write + +REFERENCES + &value create an immutable reference + &mut value create a mutable reference + Rules enforced at compile time: + - unlimited & at the same time + - only one &mut, and never alongside & + +SLICES + &s[0..n] reference to part of a string or vec +``` + +--- + +## Common beginner errors and fixes + +| Error message | What happened | Fix | +| ------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------- | +| `value used after move` | You moved a value then tried to use it | Use `&` to borrow, or `.clone()` to copy | +| `cannot borrow as mutable` | You forgot `mut` on the variable | Change `let x` to `let mut x` | +| `cannot borrow as mutable because also borrowed as immutable` | You have `&` and `&mut` at the same time | End the immutable borrow before creating the mutable one | +| `does not live long enough` | A reference outlives the data it points to | Make sure the owned value lives at least as long as the reference | diff --git a/rust_session/notes/ownership.md b/rust_session/notes/ownership.md new file mode 100644 index 00000000..63c16e48 --- /dev/null +++ b/rust_session/notes/ownership.md @@ -0,0 +1,76 @@ +# Rust Ownership, Borrowing & References + +### A beginner's guide with annotated code + +--- + +## Why these concepts exist + +Most languages either make _you_ manage memory (C, C++) or use a garbage collector to do it (Python, Java, Go). Rust takes a third path: the **compiler** enforces memory safety at compile time through three rules. No runtime cost, no GC pauses, no dangling pointers. + +The three concepts build on each other: + +``` +Ownership → who is responsible for a value +Borrowing → letting someone else use it temporarily +References → the actual mechanism for borrowing +``` + +--- + +## 1. Ownership + +Every value in Rust has exactly **one owner**. When the owner goes out of scope, the value is dropped (freed). + +```rust +fn main() { + let name = String::from("Alice"); // `name` is the owner of this String + println!("{}", name); +} // `name` goes out of scope here → String is dropped automatically +``` + +### Ownership moves on assignment + +When you assign a heap value to another variable, ownership **moves** — the original variable becomes invalid. + +```rust +fn main() { + let a = String::from("hello"); + let b = a; // ownership moves from `a` to `b` + + // println!("{}", a); // ❌ compile error: `a` was moved + println!("{}", b); // ✅ `b` is the owner now +} +``` + +> **Why?** If both `a` and `b` owned the same heap memory, Rust would try to free it twice — a classic bug. Moving prevents this. + +### Ownership moves into functions too + +```rust +fn greet(s: String) { // `s` takes ownership + println!("Hello, {}!", s); +} // `s` is dropped here + +fn main() { + let name = String::from("Bob"); + greet(name); // ownership moves into `greet` + + // println!("{}", name); // ❌ compile error: `name` was moved +} +``` + +### Copy types are different + +Primitive types like `i32`, `bool`, `f64`, and `char` implement the `Copy` trait — they are copied, not moved, because they live entirely on the stack. + +```rust +fn main() { + let x = 5; + let y = x; // `x` is copied, not moved + + println!("{} {}", x, y); // ✅ both are valid — x is still usable +} +``` + +--- diff --git a/rust_session/notes/referencing.md b/rust_session/notes/referencing.md new file mode 100644 index 00000000..3f4f267c --- /dev/null +++ b/rust_session/notes/referencing.md @@ -0,0 +1,58 @@ +## 3. References + +A reference is a pointer that is _guaranteed by the compiler_ to be valid. No null pointers, no dangling pointers — ever. + +### Immutable reference (`&T`) + +```rust +fn print_length(s: &String) { + println!("Length: {}", s.len()); // can read + // s.push_str(" world"); // ❌ can't modify through &String +} + +fn main() { + let message = String::from("hello"); + + let r1 = &message; // first shared borrow + let r2 = &message; // second shared borrow — totally fine + + println!("{} and {}", r1, r2); // ✅ multiple readers are safe +} +``` + +### Mutable reference (`&mut T`) + +```rust +fn append_world(s: &mut String) { + s.push_str(", world"); // ✅ can modify through &mut +} + +fn main() { + let mut message = String::from("hello"); // must be `mut` to borrow mutably + append_world(&mut message); + println!("{}", message); // "hello, world" +} +``` + +### The exclusivity rule in action + +```rust +fn main() { + let mut s = String::from("hello"); + + let r1 = &s; // immutable borrow + let r2 = &s; // another immutable borrow — fine + // let r3 = &mut s; // ❌ can't have &mut while &s exists + + println!("{} {}", r1, r2); + // r1 and r2 are no longer used after this point + + let r3 = &mut s; // ✅ now fine — r1 and r2 are done + r3.push_str("!"); + println!("{}", r3); +} +``` + +> **The rule in plain English:** You can have as many readers as you like _or_ exactly one writer — never both at the same time. + +--- diff --git a/rust_session/src/array.rs b/rust_session/src/array.rs new file mode 100644 index 00000000..938c12f5 --- /dev/null +++ b/rust_session/src/array.rs @@ -0,0 +1,5 @@ +pub fn test_array() { + let arr: [u8; 3] = [10, 7, 10]; + println!("array here: {:?}", arr); + +} \ No newline at end of file diff --git a/rust_session/src/main.rs b/rust_session/src/main.rs new file mode 100644 index 00000000..c11cc20e --- /dev/null +++ b/rust_session/src/main.rs @@ -0,0 +1,17 @@ +mod sub; +mod sum; +mod ownership; +mod array; +mod mut_ex; + + +fn main() { + // use sub::sub; + // sub(10, 5); + // ownership::test_ownership(); + // ownership::call_name(); + // array::test_array(); + // ownership::test_move(); + // ownership::call_greet(); + mut_ex::test_mut(); +} diff --git a/rust_session/src/mut_ex.rs b/rust_session/src/mut_ex.rs new file mode 100644 index 00000000..8f344789 --- /dev/null +++ b/rust_session/src/mut_ex.rs @@ -0,0 +1,9 @@ +pub fn test_mut() { + let mut name: String = String::from("Isaa"); + name.push('c'); + println!("name here: {name}"); + + name.push_str("O"); + println!("name here: {name}"); + +} \ No newline at end of file diff --git a/rust_session/src/ownership.rs b/rust_session/src/ownership.rs new file mode 100644 index 00000000..0d4b7975 --- /dev/null +++ b/rust_session/src/ownership.rs @@ -0,0 +1,33 @@ +pub fn test_ownership() { + let name = String::from("Alice"); // `name` is the owner of this String + // name.push('r'); + println!("name is: {}", name); +} + +pub fn call_name() { + let name: &str = "Yusrah"; + // name. + + println!("name is: {}", name); +} + + +pub fn test_move() { + let a = String::from("Anagkazo"); + println!("{}", a.clone()); // ✅ compiles: another address has been created in memory + let b = a; // ownership moves from `a` to `b` + + // println!("{}", a); // ❌ compile error: `a` was moved + println!("{}", b); // ✅ `b` is the owner now +} + +pub fn greet(s: String) { // `s` takes ownership + println!("Hello, {}!", s); +} // `s` is dropped here + +pub fn call_greet() { + let name = String::from("Bob"); + greet(name); // ownership moves into `greet` + + // println!("{}", name); // ❌ compile error: `name` was moved +} \ No newline at end of file diff --git a/rust_session/src/sub.rs b/rust_session/src/sub.rs new file mode 100644 index 00000000..ea9141f9 --- /dev/null +++ b/rust_session/src/sub.rs @@ -0,0 +1,5 @@ +pub fn sub(x: u8, y: u8) -> u8 { + let result = x - y; + println!("result is: {result}"); + result +} diff --git a/rust_session/src/sum.rs b/rust_session/src/sum.rs new file mode 100644 index 00000000..78e40419 --- /dev/null +++ b/rust_session/src/sum.rs @@ -0,0 +1,4 @@ +fn sum(x: u8, y: u8) -> u8 { + x + y + // return x + y; +} diff --git a/rust_session/student_registry/Cargo.lock b/rust_session/student_registry/Cargo.lock new file mode 100644 index 00000000..e7f8ee38 --- /dev/null +++ b/rust_session/student_registry/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "student_registry" +version = "0.1.0" diff --git a/rust_session/student_registry/Cargo.toml b/rust_session/student_registry/Cargo.toml new file mode 100644 index 00000000..bde0b676 --- /dev/null +++ b/rust_session/student_registry/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "student_registry" +version = "0.1.0" +edition = "2021" diff --git a/rust_session/student_registry/README.md b/rust_session/student_registry/README.md new file mode 100644 index 00000000..9441b751 --- /dev/null +++ b/rust_session/student_registry/README.md @@ -0,0 +1,104 @@ +# Rust Student Registry +### A beginner project covering: `struct` · `Vec` · `enum` · `Option` · `impl` + +```bash +// ============================================================ +// STUDENT REGISTRY — Beginner Rust Project +// Concepts covered: +// • struct — grouping related data +// • enum — a value that can be one of several variants +// • Vec — a growable list +// • Option — a value that may or may not exist +// • impl block — adding methods to a struct +// • pattern matching (match, if let) +// • ownership & borrowing in practice +// ============================================================ +``` + +--- + +## How to run + +### Step 1 — Install Rust (if you haven't yet) +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` +Then restart your terminal, or run: +```bash +source "$HOME/.cargo/env" +``` + +### Step 2 — Verify the install +```bash +rustc --version # e.g. rustc 1.78.0 +cargo --version # e.g. cargo 1.78.0 +``` + +### Step 3 — Run the project +```bash +# Clone / copy this folder, then: +cd student_registry + +cargo run # compiles + runs in one command +``` + +### Other useful commands +```bash +cargo build # compile only (output goes to ./target/debug/) +cargo check # fast type-check without producing a binary (great while learning) +cargo run --release # compile with full optimisations (faster binary) +``` + +--- + +## What the project teaches + +| Concept | Where to look in main.rs | +|---|---| +| `struct` | `Student` and `Registry` definitions (~line 30, 90) | +| `enum` | `Grade` definition (~line 14) | +| `impl` block | `impl Grade`, `impl Student`, `impl Registry` | +| `Vec` | `Registry.students` field; `push`, `retain`, `iter` | +| `Option` | `find_by_name`, `find_by_id` return types; `demo_option()` | +| `match` | `Grade::as_str`, `letter_grade`, search demos | +| `if let` | `update_score`, `summary`, `demo_option` | +| Ownership / borrowing | `&self` vs `&mut self` on every method | +| Iterator methods | `summary()` — `.map()`, `.sum()`, `.max_by()` | + +--- + +## Expected output (abridged) + +``` + ╔══════════════════════════════════════╗ + ║ RUST STUDENT REGISTRY v1.0 ║ + ║ struct · Vec · enum · Option · impl ║ + ╚══════════════════════════════════════╝ + + ┌─ 1. Adding students (struct + Vec::push) + ✅ Added: Kay (ID 1) + ✅ Added: Yusrah (ID 2) + ... + + ┌─ 3. Search by name (Option<&Student>) + Found → ID: 2 Grade: 2nd Year Score: 64 + 'Yaw Owusu' not found in registry. + + ┌─ 8. Summary (iterators: map, sum, max_by) + Total students : 4 + Average score : 71.5 + Top student : Esi Boateng (91.0) +``` + +--- + +## Project structure + +``` +student_registry/ +├── Cargo.toml ← project metadata & dependencies +└── src/ + └── main.rs ← all code lives here (heavily annotated) +``` + +Everything is in one file intentionally — beginners don't need to navigate modules yet. diff --git a/rust_session/student_registry/notes/borrowing_deep_dive.md b/rust_session/student_registry/notes/borrowing_deep_dive.md new file mode 100644 index 00000000..76411372 --- /dev/null +++ b/rust_session/student_registry/notes/borrowing_deep_dive.md @@ -0,0 +1,167 @@ +# Borrowing Deep Dive +Borrowing is like lending something to a friend. + +--- + +## The core idea + +```rust +fn main() { + let book = String::from("Rust Programming"); + + read(&book); // lend the book to `read` + + // book is still yours after the function returns + println!("I still have: {}", book); +} + +fn read(b: &String) { // b is a borrowed reference — not the owner + println!("Reading: {}", b); +} // borrow ends here — nothing is dropped +``` + +You still own `book`. `read` just borrowed it temporarily. When `read` finishes, +the book comes back to you automatically. + +--- + +## Without borrowing — ownership moves away + +```rust +fn main() { + let book = String::from("Rust Programming"); + + read(book); // ownership MOVES into read — you no longer have it + + println!("{}", book); // ❌ compile error: book was moved +} + +fn read(b: String) { // b owns the book now + println!("Reading: {}", b); +} // b is dropped here — book is gone forever +``` + +This is why borrowing exists. Without `&`, the value moves in and never comes back. + +--- + +## Two kinds of borrow + +### 1. Immutable borrow `&T` — look but don't touch + +```rust +fn main() { + let score = 95; + + print_score(&score); // lend score for reading + + println!("score is still {}", score); // ✅ still usable +} + +fn print_score(s: &i32) { + println!("Score: {}", s); + // *s = 100; // ❌ can't change it — it's an immutable borrow +} +``` + +### 2. Mutable borrow `&mut T` — borrow AND change it + +```rust +fn main() { + let mut score = 95; // must be `mut` to lend mutably + + add_bonus(&mut score); // lend score for writing + + println!("new score: {}", score); // 100 +} + +fn add_bonus(s: &mut i32) { + *s += 5; // * means "go to what this points to and change it" +} +``` + +--- + +## The two rules Rust enforces + +### Rule 1 — as many readers as you like + +```rust +fn main() { + let name = String::from("Henry"); + + let r1 = &name; // borrow 1 — fine + let r2 = &name; // borrow 2 — also fine + let r3 = &name; // borrow 3 — still fine + + println!("{} {} {}", r1, r2, r3); // ✅ multiple readers are safe +} +``` + +Think of a book in a library — unlimited people can read the same book at the +same time because no one is changing it. + +### Rule 2 — only ONE writer, and no readers at the same time + +```rust +fn main() { + let mut name = String::from("Henry"); + + let r1 = &name; // immutable borrow + let r2 = &mut name; // ❌ compile error — can't have &mut while &name exists + + println!("{} {}", r1, r2); +} +``` + +```rust +fn main() { + let mut name = String::from("Henry"); + + let r1 = &name; + println!("{}", r1); // r1 last used here — borrow ends + + let r2 = &mut name; // ✅ fine now — r1 is done + r2.push_str(" Osei"); + println!("{}", r2); // "Henry Osei" +} +``` + +Think of a whiteboard — unlimited people can read it, but the moment someone +starts writing on it, everyone else has to step back. + +--- + +## In the student registry + +```rust +// &mut self — we need to CHANGE the registry (push a new student) +pub fn add(&mut self, name: &str, ...) { + // ^^^ + // mutable borrow — we will modify self.students + + self.students.push(student); // this is the change +} + +// &self — we only READ the registry (print students) +pub fn list_all(&self) { + // ^^^^^ + // immutable borrow — we never change anything + + for student in &self.students { // borrow each student for printing + println!("{}", student.name); + } +} +``` + +--- + +## One-line mental model + +``` +&T → "I want to look at it" +&mut T → "I want to look at it AND change it" +T → "I want to own it and take it with me" +``` + +Rust checks these rules at compile time — zero runtime cost, zero crashes. diff --git a/rust_session/student_registry/notes/core_logic.md b/rust_session/student_registry/notes/core_logic.md new file mode 100644 index 00000000..5383ef4e --- /dev/null +++ b/rust_session/student_registry/notes/core_logic.md @@ -0,0 +1,315 @@ +# Ownership, Borrowing & Referencing in the Student Registry + +This document walks through every place ownership, borrowing, and referencing +appear in the four files of the student registry project — +`grade.rs`, `student_struct.rs`, `registry.rs`, and `main.rs` — +and explains _why_ each one is used. + +--- + +## Quick reminder: the three concepts + +| Concept | Syntax | Meaning | +| -------------------------------- | --------------- | ------------------------------------------------------------------------------------------- | +| **Ownership** | `let x = value` | One variable is responsible for the value. When it goes out of scope, the value is dropped. | +| **Mutable borrow** | `&mut T` | Temporarily lend the value for reading AND writing. No ownership transfer. | +| **Immutable borrow / reference** | `&T` | Temporarily lend the value for reading only. No ownership transfer. | + +--- + +## 1. `grade.rs` + +```rust +pub fn as_str(&self) -> &str { + match self { + Grade::First => "1st Year", + Grade::Second => "2nd Year", + Grade::Third => "3rd Year", + } +} +``` + +### `&self` — immutable borrow of Grade + +`as_str` only needs to _read_ the variant — it never changes it. +`&self` is an immutable reference to the `Grade` value. + +- The caller keeps ownership of the `Grade`. +- `as_str` borrows it just long enough to run the `match` and return a string. +- When `as_str` returns, the borrow ends. The caller still owns the `Grade`. + +### `-> &str` — returning a reference to static memory + +The returned `&str` points to string literals like `"1st Year"` which live in +the program's static memory (baked into the binary). No heap allocation occurs. +The reference is valid for the entire lifetime of the program. + +--- + +## 2. `student_struct.rs` + +```rust +use crate::grade::Grade; + +pub struct Student { + pub id: u32, + pub name: String, + pub age: u8, + pub grade: Grade, + pub score: f32, +} + +impl Student { + pub fn new(id: u32, name: String, age: u8, grade: Grade, score: f32) -> Student { + Student { id, name, age, grade, score } + } +} +``` + +### `fn new(…) -> Student` — ownership transfer via return value + +`new` takes all five arguments **by value** — it takes ownership of each one: + +| Parameter | Type | What happens | +| --------- | -------- | ------------------------------------------------------ | +| `id` | `u32` | Copied (primitive, `Copy` trait) | +| `name` | `String` | **Moved** into the struct — heap memory is transferred | +| `age` | `u8` | Copied (primitive) | +| `grade` | `Grade` | **Moved** into the struct — enum value transferred | +| `score` | `f32` | Copied (primitive) | + +When `new` returns the `Student`, ownership of the entire struct — including +the `String` on the heap — moves to whoever called `new`. + +### Why `String` and not `&str`? + +`String` is an _owned_, heap-allocated string. The struct needs to _own_ its +`name` field so the name lives as long as the `Student` does. If we used `&str` +(a borrowed reference), we would have to track where the original string lives +and guarantee it outlives the struct — that is lifetime annotation territory, +which is more advanced. + +--- + +## 3. `registry.rs` + +```rust +pub struct Registry { + pub students: Vec, + next_id: u32, +} +``` + +### `Vec` — Registry owns all students + +`Registry` owns the `Vec`, and the `Vec` owns every `Student` inside it. +This is a chain of ownership: + +``` +Registry + └── Vec (heap) + ├── Student { id, name, age, grade, score } + ├── Student { … } + └── Student { … } +``` + +When `Registry` is dropped, the `Vec` is dropped, which drops every `Student`, +which drops every `String` inside each student. One owner, one cleanup — no +manual `free()` needed. + +--- + +### `pub fn new() -> Registry` + +```rust +pub fn new() -> Registry { + Registry { + students: Vec::new(), + next_id: 1, + } +} +``` + +Returns an owned `Registry` by value. The caller takes ownership. +`Vec::new()` creates an empty `Vec` — no heap allocation yet. +The heap only gets used when the first `push()` happens. + +--- + +### `pub fn add(&mut self, name: &str, age: u8, grade: Grade, score: f32)` + +```rust +pub fn add(&mut self, name: &str, age: u8, grade: Grade, score: f32) { + let id = self.next_id; + let student = Student::new(id, name.to_string(), age, grade, score); + println!(" ✅ Added: {} (ID {})", student.name, student.id); + self.students.push(student); + self.next_id += 1; +} +``` + +There are three distinct ownership/borrowing events here: + +#### `&mut self` — mutable borrow of Registry + +`add` needs to _change_ the registry — it pushes a new student and increments +`next_id`. So it takes a **mutable borrow** of the whole `Registry`. + +- The caller keeps ownership of the `Registry`. +- `add` can read and write any field on it. +- Only one `&mut` borrow can exist at a time — Rust enforces this. + +#### `name: &str` — immutable reference to a string + +`name` comes in as `&str` — a **borrowed reference** to a string slice. +`add` does not take ownership of the string. It only needs to read the +characters long enough to call `name.to_string()`. + +`name.to_string()` creates a brand-new owned `String` on the heap — that +owned copy is what gets moved into `Student::new`. The original string the +caller passed in is untouched and still owned by the caller. + +#### `grade: Grade` — ownership moves in + +`grade` is passed **by value** — ownership moves from the caller into `add`, +then immediately into `Student::new`, then into the `Student` struct. +After `reg.add(…, Grade::First, …)` in `main.rs`, the `Grade::First` value +lives inside the `Student` inside the `Vec`. The caller no longer holds it. + +#### `self.students.push(student)` — Vec takes ownership of Student + +After `println!`, `student` is moved into the `Vec` via `push`. From this +point: + +- `student` is no longer accessible as a local variable. +- The `Vec` is the owner. +- The memory for the student (including its `String name` on the heap) will be + freed only when the `Vec` itself is dropped. + +--- + +### `pub fn list_all(&self)` + +```rust +pub fn list_all(&self) { + for student in &self.students { + println!( + " {:>5} {:<20} {:>6} {:<10} {:.1}", + student.id, + student.name, + student.age, + student.grade.as_str(), + student.score, + ); + } +} +``` + +#### `&self` — immutable borrow of Registry + +`list_all` only reads — it never changes anything. `&self` is the correct +choice. The caller keeps ownership of the `Registry` and can use it again +after `list_all` returns. + +#### `for student in &self.students` — borrowing each element + +The `&` before `self.students` means we are iterating over **references** to +each `Student` — not moving them out of the `Vec`. + +- `student` inside the loop is `&Student` — a borrowed reference. +- The `Vec` still owns every student. +- Reading `student.id`, `student.name`, `student.age`, `student.score` is + fine because we are borrowing those fields through the reference. + +#### `student.grade.as_str()` — chained borrow + +`student` is `&Student`, so `student.grade` is accessing the `Grade` field +through a reference. Rust auto-derefs this for us. +`as_str` then takes `&self` on the `Grade` — another immutable borrow layered +on top. Both borrows exist only for the duration of the `println!` line, then +they end. + +--- + +## 4. `main.rs` + +```rust +fn main() { + let mut reg = Registry::new(); + + reg.add("Henry Osei", 20, Grade::First, 78.5); + reg.add("Kofi Mensah", 22, Grade::Second, 64.0); + reg.add("Esi Boateng", 21, Grade::First, 91.0); + + reg.list_all(); +} +``` + +### `let mut reg` — main owns Registry + +`main` owns `reg`. It must be `mut` because `add` takes `&mut self` — you +cannot mutably borrow something that was not declared mutable. + +### `reg.add(…)` — temporary mutable borrow + +Each call to `reg.add(…)` mutably borrows `reg` for the duration of that call. +When `add` returns, the mutable borrow ends and `reg` is available again for +the next call. + +### String literals as `&str` + +`"Henry Osei"` is a string literal — its type is `&str`. It is a reference +pointing into static memory. `add` accepts `name: &str`, so this matches +directly. No allocation happens. Inside `add`, `name.to_string()` is where +the heap allocation occurs. + +### `reg.list_all()` — temporary immutable borrow + +`list_all` takes `&self` — an immutable borrow of `reg`. After it returns, +`reg` is still owned by `main`. At the closing `}` of `main`, `reg` goes out +of scope and is dropped — which drops the `Vec`, which drops every `Student`, +which drops every `String name` on the heap. + +--- + +## Summary map + +``` +main.rs +│ +│ let mut reg = Registry::new(); +│ └── main owns reg (and its Vec) +│ +│ reg.add("Henry", 20, Grade::First, 78.5) +│ ├── &mut self — mutable borrow of reg (temporary) +│ ├── name: &str — immutable borrow of "Henry" (caller keeps it) +│ ├── grade: Grade — ownership of Grade::First moves into Student +│ └── push(student) — Vec takes ownership of Student +│ +│ reg.list_all() +│ ├── &self — immutable borrow of reg (temporary) +│ ├── &self.students — immutable borrow of Vec +│ └── &student — immutable borrow of each Student in the loop +│ └── student.grade.as_str() +│ └── &self on Grade — chained immutable borrow +│ +└── } ← reg dropped → Vec dropped → all Students dropped → all Strings freed +``` + +--- + +## The one pattern to remember + +``` +Does the function need to change the value? + YES → &mut self (mutable borrow) + NO → &self (immutable borrow) + +Does ownership need to leave the caller permanently? + YES → pass by value (e.g. grade: Grade into add()) + NO → pass by reference (e.g. name: &str into add()) +``` + +Rust enforces these choices at compile time. If you get them wrong, the +compiler tells you exactly which rule was broken and on which line — that is +the compiler errors you saw in the previous session. diff --git a/rust_session/student_registry/notes/referencing_deep_dive.md b/rust_session/student_registry/notes/referencing_deep_dive.md new file mode 100644 index 00000000..c422de29 --- /dev/null +++ b/rust_session/student_registry/notes/referencing_deep_dive.md @@ -0,0 +1,209 @@ +# Referencing in Rust — ELI5 + +A reference is just an address — it tells Rust _where_ a value lives in memory, +without taking the value away from its owner. + +If ownership is _having_ something, a reference is _knowing where it is_. + +--- + +## The core idea + +```rust +fn main() { + let score: i32 = 95; + + let r = &score; // r is a reference — it holds the address of `score` + + println!("score = {}", score); // use the original + println!("via r = {}", r); // use the reference — same value + println!("addr = {:p}", r); // {:p} prints the memory address e.g. 0x7ffee3b2c490 +} +``` + +`score` is the value sitting in memory. +`r` is a small variable that holds the _address_ of `score` — nothing more. +Both `score` and `r` print `95` because `r` points straight at `score`. + +--- + +## Dereferencing — following the address + +The `*` operator means "go to the address this reference holds and give me +what is there". + +```rust +fn main() { + let x = 10; + let r = &x; // r holds the address of x + + println!("{}", r); // Rust auto-derefs for println — prints 10 + println!("{}", *r); // explicit deref — also prints 10 + + // Think of it like: + // r = the street address of a house + // *r = walking to that address and looking inside the house +} +``` + +--- + +## Changing a value through a mutable reference + +```rust +fn main() { + let mut points = 50; + let r = &mut points; // mutable reference — can read AND write + + *r = 100; // go to the address and write 100 there + + println!("{}", points); // 100 — the original changed +} +``` + +`r` is not a copy of `points`. It is a direct window into the same memory slot. +Writing through `*r` changes `points` itself. + +--- + +## Printing the address with `{:p}` + +```rust +fn main() { + let name = String::from("Kofi"); + let r = &name; + + println!("value : {}", r); // Kofi + println!("address : {:p}", r); // e.g. 0x7ffee3b2c490 + + // The address will be different every time you run the program. + // The OS places things at different spots in RAM on each run. +} +``` + +`{:p}` is the "pointer" format specifier — it shows the raw memory address +the reference holds. + +--- + +## Multiple references to the same value + +```rust +fn main() { + let city = String::from("Accra"); + + let r1 = &city; + let r2 = &city; + let r3 = &city; + + // All three references point to the SAME memory — no copies made + println!("{:p}", r1); // same address + println!("{:p}", r2); // same address + println!("{:p}", r3); // same address + + println!("{} {} {}", r1, r2, r3); // Accra Accra Accra +} +``` + +References are cheap — they are just addresses (8 bytes on a 64-bit system). +No matter how large the original value is, the reference is always the same tiny size. + +--- + +## References vs owned values — size comparison + +```rust +use std::mem::size_of; +use std::mem::size_of_val; + +fn main() { + let name = String::from("Henry Osei"); // String on heap + let r = &name; + + // String = 3 words on the stack (ptr + len + cap) + println!("size of String : {} bytes", size_of_val(&name)); // 24 bytes + // Reference = 1 pointer + println!("size of &String : {} bytes", size_of::<&String>()); // 8 bytes + + // The reference is always 8 bytes regardless of how long the name is. +} +``` + +--- + +## References into a Vec — what the for loop actually does + +```rust +fn main() { + let students = vec![ + String::from("Henry"), + String::from("Kofi"), + String::from("Esi"), + ]; + + // &students.students borrows the Vec — gives references to each element + for name in &students { + // name is &String — a reference, not an owned String + println!("{:p} → {}", name, name); // address → value + } + + // students is still owned here — the loop only borrowed + println!("total: {}", students.len()); +} +``` + +Without the `&`, the loop would _move_ each `String` out of the `Vec` and the +`Vec` would be unusable afterwards. + +--- + +## In the student registry + +```rust +// list_all iterates with &self.students +// — each `student` is a &Student reference, not an owned Student + +pub fn list_all(&self) { + for student in &self.students { + // student : &Student + // student.name : &String (accessed through the reference) + // student.grade : &Grade (accessed through the reference) + + println!( + "{} {} {}", + student.name, // Rust auto-derefs &Student to reach .name + student.grade.as_str(), // chained reference: &Student → &Grade → &str + student.score, + ); + } + // Nothing was moved. Every Student is still inside the Vec. +} +``` + +--- + +## The difference between `&` and `&mut` in one picture + +``` +Memory slot: [ 95 ] ← the value lives here at address 0xABC + +&score → read-only window into 0xABC + many allowed at the same time + +&mut score → read-write window into 0xABC + only ONE allowed, and no &score at the same time +``` + +--- + +## One-line mental model + +``` +&T → "I know where it lives — I can look" +&mut T → "I know where it lives — I can look and change" +*r → "go to the address r holds and get what's there" +{:p} → "show me the raw address as a number" +``` + +Rust guarantees at compile time that every reference always points to valid +memory — no dangling pointers, no null, no use-after-free. Ever. diff --git a/rust_session/student_registry/notes/struct.md b/rust_session/student_registry/notes/struct.md new file mode 100644 index 00000000..162fa495 --- /dev/null +++ b/rust_session/student_registry/notes/struct.md @@ -0,0 +1,4 @@ +Grouping related data +A struct bundles related fields under one name — like a row in a database table, but typed. Each field has an explicit type: no surprises. + +name: `String` lives on the heap. id, age, score live on the stack. \ No newline at end of file diff --git a/rust_session/student_registry/src/grade.rs b/rust_session/student_registry/src/grade.rs new file mode 100644 index 00000000..10438b1e --- /dev/null +++ b/rust_session/student_registry/src/grade.rs @@ -0,0 +1,31 @@ +#[derive(Debug, PartialEq)] +pub enum Grade { + First, + Second, + Third, +} + +impl Grade { + pub fn as_str(&self) -> &str { + match self { + Grade::First => "Cohort 1", + Grade::Second => "Cohort 2", + Grade::Third => "Cohort 3", + } + } +} + +#[derive(Debug)] +pub enum Sex { + Male, + Female, +} + +impl Sex { + pub fn to_str(&self) { + match self { + Sex::Male => println!("male: 👨🏾"), + Sex::Female => println!("female: 👧🏾"), + } + } +} diff --git a/rust_session/student_registry/src/main.rs b/rust_session/student_registry/src/main.rs new file mode 100644 index 00000000..7ae18ff7 --- /dev/null +++ b/rust_session/student_registry/src/main.rs @@ -0,0 +1,43 @@ +mod grade; +mod registry; +mod student_struct; +mod utils; + +use grade::{Grade, Sex}; +use registry::Registry; +use student_struct::Student; + +fn main() { + // let g = Grade::Second; + // println!("{}", g.as_str()); // "2nd Year" + // println!("{:?}", g); + + // let ss = Student::new(); + // println!("stund") + + // let mut reg = Registry::new(); + + // reg.add("Victor", 20, Grade::First, 78.5); + // reg.add("Kosi", 22, Grade::Second, 64.0); + // reg.add("Yusrah", 21, Grade::First, 91.0); + + // reg.list_all();] + + let sex = Sex::Male; + println!("sex: {:?}", sex.to_str()); + + // let s: Student = Student::new(1, String::from("Testimony"), 16, Sex::Female, Grade::Third, 40.5); + let s: Student = Student::new( + 1, + "Testimony".to_string(), + 16, + Sex::Female, + Grade::Third, + 40.5, + ); + println!("student here: {:#?}", s); + + println!("student id: {}", s.id); + println!("student name: {}", s.name); + println!("student age: {}", s.age); +} diff --git a/rust_session/student_registry/src/registry.rs b/rust_session/student_registry/src/registry.rs new file mode 100644 index 00000000..146dc186 --- /dev/null +++ b/rust_session/student_registry/src/registry.rs @@ -0,0 +1,39 @@ +use crate::grade::{Grade, Sex}; +use crate::student_struct::Student; + +pub struct Registry { + pub students: Vec, + next_id: u32, +} + +impl Registry { + pub fn add(&mut self, name: &str, age: u8, sex: Sex, grade: Grade, score: f32) { + let id = self.next_id; + let student = Student::new(id, name.to_string(), age, sex, grade, score); + println!("Added: {} (ID {})", student.name, student.id); + self.students.push(student); + self.next_id += 1; + } + + pub fn list_all(&self) { + if self.students.is_empty() { + println!(" (no students enrolled yet)"); + return; + } + println!( + " {:>5} {:<20} {:<6} {:<10} {}", + "ID", "Name", "Age", "Grade", "Score" + ); + println!(" {}", "-".repeat(55)); + for student in &self.students { + println!( + " {:>5} {:<20} {:>6} {:<10} {:.1}", + student.id, + student.name, + student.age, + student.grade.as_str(), + student.score, + ); + } + } +} diff --git a/rust_session/student_registry/src/registry_struct.rs b/rust_session/student_registry/src/registry_struct.rs new file mode 100644 index 00000000..8b076a03 --- /dev/null +++ b/rust_session/student_registry/src/registry_struct.rs @@ -0,0 +1,6 @@ +use crate::student_struct; + +pub struct Registry { + students: Vec, // Vec = "a list of Student values" + next_id: u32, // auto-increment counter for IDs +} \ No newline at end of file diff --git a/rust_session/student_registry/src/student_struct.rs b/rust_session/student_registry/src/student_struct.rs new file mode 100644 index 00000000..9e4ead64 --- /dev/null +++ b/rust_session/student_registry/src/student_struct.rs @@ -0,0 +1,42 @@ +use crate::grade::{Grade, Sex}; + +// A struct groups related pieces of data under one name. +// Think of it as a custom data type you design yourself. + +#[derive(Debug)] +pub struct Student { + pub id: u32, // u32 = unsigned 32-bit integer (no negatives) + pub name: String, // String = heap-allocated, growable text + pub age: u8, + pub sex: Sex, + pub grade: Grade, // our own enum type from above + pub score: f32, +} + +// This is the implementation of the student struct with its corresponding methods +impl Student { + pub fn new(id: u32, name: String, age: u8, sex: Sex, grade: Grade, score: f32) -> Student { + Student { + id, + name, + age, + sex, + grade, + score, + } + } +} + +#[derive(Debug)] +pub enum Status { + Pending, + Ongoing, + Completed, +} + +pub struct Todo { + id: u8, + title: String, + description: String, + status: Status, +} diff --git a/rust_session/student_registry/src/utils.rs b/rust_session/student_registry/src/utils.rs new file mode 100644 index 00000000..ab2c8eeb --- /dev/null +++ b/rust_session/student_registry/src/utils.rs @@ -0,0 +1,3 @@ +// pub fn to_str(x: String) -> &'static str { +// x.as_str() +// } diff --git a/sessions/intro-to-testing/.gitignore b/sessions/intro-to-testing/.gitignore new file mode 100644 index 00000000..925ff281 --- /dev/null +++ b/sessions/intro-to-testing/.gitignore @@ -0,0 +1,21 @@ +# Node modules +/node_modules +node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/sessions/intro-to-testing/README.md b/sessions/intro-to-testing/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/sessions/intro-to-testing/README.md @@ -0,0 +1,57 @@ +# Sample Hardhat 3 Beta Project (`mocha` and `ethers`) + +This project showcases a Hardhat 3 Beta project using `mocha` for tests and the `ethers` library for Ethereum interactions. + +To learn more about the Hardhat 3 Beta, please visit the [Getting Started guide](https://hardhat.org/docs/getting-started#getting-started-with-hardhat-3). To share your feedback, join our [Hardhat 3 Beta](https://hardhat.org/hardhat3-beta-telegram-group) Telegram group or [open an issue](https://github.com/NomicFoundation/hardhat/issues/new) in our GitHub issue tracker. + +## Project Overview + +This example project includes: + +- A simple Hardhat configuration file. +- Foundry-compatible Solidity unit tests. +- TypeScript integration tests using `mocha` and ethers.js +- Examples demonstrating how to connect to different types of networks, including locally simulating OP mainnet. + +## Usage + +### Running Tests + +To run all the tests in the project, execute the following command: + +```shell +npx hardhat test +``` + +You can also selectively run the Solidity or `mocha` tests: + +```shell +npx hardhat test solidity +npx hardhat test mocha +``` + +### Make a deployment to Sepolia + +This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia. + +To run the deployment to a local chain: + +```shell +npx hardhat ignition deploy ignition/modules/Counter.ts +``` + +To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use. + +You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable. + +To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`: + +```shell +npx hardhat keystore set SEPOLIA_PRIVATE_KEY +``` + +After setting the variable, you can run the deployment with the Sepolia network: + +```shell +npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts +``` diff --git a/sessions/intro-to-testing/contracts/CounterV1.sol b/sessions/intro-to-testing/contracts/CounterV1.sol new file mode 100644 index 00000000..79e9871e --- /dev/null +++ b/sessions/intro-to-testing/contracts/CounterV1.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract CounterV1 { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/sessions/intro-to-testing/contracts/Timelock.sol b/sessions/intro-to-testing/contracts/Timelock.sol new file mode 100644 index 00000000..59614865 --- /dev/null +++ b/sessions/intro-to-testing/contracts/Timelock.sol @@ -0,0 +1,152 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract TimeLockV1 { + + struct Vault { + uint balance; + uint unlockTime; + bool active; + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint vaultId, uint amount, uint unlockTime); + event Withdrawn(address indexed user, uint vaultId, uint amount); + + function deposit(uint _unlockTime) external payable returns (uint) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + // Create new vault + vaults[msg.sender].push(Vault({ + balance: msg.value, + unlockTime: _unlockTime, + active: true + })); + + uint vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + function withdraw(uint _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint amount = userVault.balance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.active = false; + + (bool success, ) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + function withdrawAll() external returns (uint) { + uint totalWithdrawn = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && + userVaults[i].balance > 0 && + block.timestamp >= userVaults[i].unlockTime) { + + uint amount = userVaults[i].balance; + userVaults[i].balance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + (bool success, ) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + function getVaultCount(address _user) external view returns (uint) { + return vaults[_user].length; + } + function getVault(address _user, uint _vaultId) external view returns ( + uint balance, + uint unlockTime, + bool active, + bool isUnlocked + ) { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return ( + vault.balance, + vault.unlockTime, + vault.active, + block.timestamp >= vault.unlockTime + ); + } + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + function getActiveVaults(address _user) external view returns ( + uint[] memory activeVaults, + uint[] memory balances, + uint[] memory unlockTimes + ) { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint activeCount = 0; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint[](activeCount); + balances = new uint[](activeCount); + unlockTimes = new uint[](activeCount); + + // Populate arrays + uint index = 0; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, unlockTimes); + } + function getTotalBalance(address _user) external view returns (uint total) { + Vault[] storage userVaults = vaults[_user]; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + function getUnlockedBalance(address _user) external view returns (uint unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && + userVaults[i].balance > 0 && + block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/sessions/intro-to-testing/hardhat.config.ts b/sessions/intro-to-testing/hardhat.config.ts new file mode 100644 index 00000000..7379ec89 --- /dev/null +++ b/sessions/intro-to-testing/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from '@nomicfoundation/hardhat-toolbox-mocha-ethers'; +import { configVariable, defineConfig } from 'hardhat/config'; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: '0.8.28', + }, + production: { + version: '0.8.28', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: 'edr-simulated', + chainType: 'l1', + }, + hardhatOp: { + type: 'edr-simulated', + chainType: 'op', + }, + sepolia: { + type: 'http', + chainType: 'l1', + url: configVariable('SEPOLIA_RPC_URL'), + accounts: [configVariable('SEPOLIA_PRIVATE_KEY')], + }, + }, +}); diff --git a/sessions/intro-to-testing/ignition/modules/Counter.ts b/sessions/intro-to-testing/ignition/modules/Counter.ts new file mode 100644 index 00000000..16c691ac --- /dev/null +++ b/sessions/intro-to-testing/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; + +export default buildModule('CounterModule', (m) => { + const counter = m.contract('Counter'); + + m.call(counter, 'incBy', [5n]); + + return { counter }; +}); diff --git a/sessions/intro-to-testing/package.json b/sessions/intro-to-testing/package.json new file mode 100644 index 00000000..3caface9 --- /dev/null +++ b/sessions/intro-to-testing/package.json @@ -0,0 +1,29 @@ +{ + "name": "y", + "version": "1.0.0", + "type": "module", + "scripts": { + "test-counter": "npx hardhat test test/CounterV1.ts", + "test-timelock": "npx hardhat test test/TimelockV1.ts", + "compile": "npx hardhat compile", + "format": "prettier --write \"hardhat.config.ts\" \"test/*.ts\" " + }, + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.11", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.9", + "mocha": "^11.3.0", + "typescript": "~5.8.3" + }, + "dependencies": { + "prettierr": "^1.15.3-dev" + } +} diff --git a/sessions/intro-to-testing/pnpm-lock.yaml b/sessions/intro-to-testing/pnpm-lock.yaml new file mode 100644 index 00000000..a0aec58e --- /dev/null +++ b/sessions/intro-to-testing/pnpm-lock.yaml @@ -0,0 +1,3257 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + devDependencies: + '@nomicfoundation/hardhat-ethers': + specifier: ^4.0.4 + version: 4.0.4(hardhat@3.1.9) + '@nomicfoundation/hardhat-ignition': + specifier: ^3.0.7 + version: 3.0.7(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(hardhat@3.1.9) + '@nomicfoundation/hardhat-toolbox-mocha-ethers': + specifier: ^3.0.2 + version: 3.0.2(56383e692657429e8f088e48bc1cde98) + '@types/chai': + specifier: ^4.3.20 + version: 4.3.20 + '@types/chai-as-promised': + specifier: ^8.0.2 + version: 8.0.2 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 + '@types/node': + specifier: ^22.19.11 + version: 22.19.11 + chai: + specifier: ^5.3.3 + version: 5.3.3 + ethers: + specifier: ^6.16.0 + version: 6.16.0 + forge-std: + specifier: github:foundry-rs/forge-std#v1.9.4 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262 + hardhat: + specifier: ^3.1.9 + version: 3.1.9 + mocha: + specifier: ^11.7.5 + version: 11.7.5 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + +packages: + '@adraffy/ens-normalize@1.10.1': + resolution: + { + integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==, + } + + '@esbuild/aix-ppc64@0.27.3': + resolution: + { + integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==, + } + engines: { node: '>=18' } + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: + { + integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: + { + integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==, + } + engines: { node: '>=18' } + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: + { + integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: + { + integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: + { + integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: + { + integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: + { + integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: + { + integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: + { + integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==, + } + engines: { node: '>=18' } + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: + { + integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==, + } + engines: { node: '>=18' } + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: + { + integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==, + } + engines: { node: '>=18' } + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: + { + integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==, + } + engines: { node: '>=18' } + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: + { + integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==, + } + engines: { node: '>=18' } + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: + { + integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==, + } + engines: { node: '>=18' } + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: + { + integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==, + } + engines: { node: '>=18' } + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: + { + integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: + { + integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: + { + integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: + { + integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: + { + integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: + { + integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: + { + integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: + { + integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==, + } + engines: { node: '>=18' } + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: + { + integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==, + } + engines: { node: '>=18' } + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: + { + integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==, + } + engines: { node: '>=18' } + cpu: [x64] + os: [win32] + + '@ethersproject/abi@5.8.0': + resolution: + { + integrity: sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==, + } + + '@ethersproject/abstract-provider@5.8.0': + resolution: + { + integrity: sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==, + } + + '@ethersproject/abstract-signer@5.8.0': + resolution: + { + integrity: sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==, + } + + '@ethersproject/address@5.6.1': + resolution: + { + integrity: sha512-uOgF0kS5MJv9ZvCz7x6T2EXJSzotiybApn4XlOgoTX0xdtyVIJ7pF+6cGPxiEq/dpBiTfMiw7Yc81JcwhSYA0Q==, + } + + '@ethersproject/address@5.8.0': + resolution: + { + integrity: sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==, + } + + '@ethersproject/base64@5.8.0': + resolution: + { + integrity: sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==, + } + + '@ethersproject/bignumber@5.8.0': + resolution: + { + integrity: sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==, + } + + '@ethersproject/bytes@5.8.0': + resolution: + { + integrity: sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==, + } + + '@ethersproject/constants@5.8.0': + resolution: + { + integrity: sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==, + } + + '@ethersproject/hash@5.8.0': + resolution: + { + integrity: sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==, + } + + '@ethersproject/keccak256@5.8.0': + resolution: + { + integrity: sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==, + } + + '@ethersproject/logger@5.8.0': + resolution: + { + integrity: sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==, + } + + '@ethersproject/networks@5.8.0': + resolution: + { + integrity: sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==, + } + + '@ethersproject/properties@5.8.0': + resolution: + { + integrity: sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==, + } + + '@ethersproject/rlp@5.8.0': + resolution: + { + integrity: sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==, + } + + '@ethersproject/signing-key@5.8.0': + resolution: + { + integrity: sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==, + } + + '@ethersproject/strings@5.8.0': + resolution: + { + integrity: sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==, + } + + '@ethersproject/transactions@5.8.0': + resolution: + { + integrity: sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==, + } + + '@ethersproject/web@5.8.0': + resolution: + { + integrity: sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==, + } + + '@isaacs/cliui@8.0.2': + resolution: + { + integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, + } + engines: { node: '>=12' } + + '@noble/ciphers@1.2.1': + resolution: + { + integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==, + } + engines: { node: ^14.21.3 || >=16 } + + '@noble/curves@1.2.0': + resolution: + { + integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==, + } + + '@noble/curves@1.4.2': + resolution: + { + integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==, + } + + '@noble/curves@1.8.2': + resolution: + { + integrity: sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==, + } + engines: { node: ^14.21.3 || >=16 } + + '@noble/hashes@1.3.2': + resolution: + { + integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==, + } + engines: { node: '>= 16' } + + '@noble/hashes@1.4.0': + resolution: + { + integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==, + } + engines: { node: '>= 16' } + + '@noble/hashes@1.7.1': + resolution: + { + integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==, + } + engines: { node: ^14.21.3 || >=16 } + + '@noble/hashes@1.7.2': + resolution: + { + integrity: sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==, + } + engines: { node: ^14.21.3 || >=16 } + + '@nomicfoundation/edr-darwin-arm64@0.12.0-next.24': + resolution: + { + integrity: sha512-lYcD9IM52G0hk/3Sso2Rpdpyfafy3aHH0GsSy/FVog9UrEkmmU14AmccE18/zTL+UyV0yzYMDOmh6y83SD/lbg==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/edr-darwin-x64@0.12.0-next.24': + resolution: + { + integrity: sha512-cHDJZlPDpDXJXxQDVM0TGzEuNvV3wW94gipEdjNxZHeC9T2/NU/5GUoQajMJgvCZ6PWDlRMwIBRtM1jC/ny5DA==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.24': + resolution: + { + integrity: sha512-G/iln4W79CR9f68+crBZM1kBdmmK3IbQCD4b5u+iqby+H5BOLSPQmjeW9UREK5WSecnv7Oxr/ZTHHRq/w9pUPA==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.24': + resolution: + { + integrity: sha512-wt6UuOutufL3UTSyMiwPOyfRly3uQEFHASXqLsNjgp4qBrm0s+kkyaYpAe8h53lGzZmXIDOAbO0P/fwxnLCnWw==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.24': + resolution: + { + integrity: sha512-mHgkUSynINTnnIvZuZymJ4dMqjemGjdrzQ87rP5/SQQGRQVV82uDomSEglp9btSmbBWfPj4r4tWsV+a3844W0w==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/edr-linux-x64-musl@0.12.0-next.24': + resolution: + { + integrity: sha512-E0XNSlPc8Hx5Nhowe5VIvAqVeT+1VUWSRqG0cZtYcpUgJZxTp8p03ojPtbyfjL4T+78GfnpmzkkLhB6S2jZ1FQ==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.24': + resolution: + { + integrity: sha512-PbtY2zWc4k8HK4gVnVbPohJnfrICboo6J91vxTlhnPKCWGvfGbsqLfDUAp91ExHHY+80qRfQnwaLbhJiIqLFGw==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/edr@0.12.0-next.24': + resolution: + { + integrity: sha512-/NwB9yX7uBs/FIJKHBZo2hVhP7g3v6LbE21JvTLvshgb+XscyaRRUmzB//ankxLGJ1TehtXAf/Qh/a19vgpiig==, + } + engines: { node: '>= 20' } + + '@nomicfoundation/hardhat-errors@3.0.6': + resolution: + { + integrity: sha512-3x+OVdZv7Rgy3z6os9pB6kiHLxs6q0PCXHRu+WLZflr44PG9zW+7V9o+ehrUqmmivlHcIFr3Qh4M2wZVuoCYww==, + } + + '@nomicfoundation/hardhat-ethers-chai-matchers@3.0.2': + resolution: + { + integrity: sha512-nkg+z+fq5PXcRxS/zadyosAA+oPp3sdWrKpuOcASDf0RjqsN2LsNymML0VNNkZF8TF+hYa36fbV+QOas2Fm2BQ==, + } + peerDependencies: + '@nomicfoundation/hardhat-ethers': ^4.0.0 + chai: ^5.1.2 + ethers: ^6.14.0 + hardhat: ^3.0.0 + + '@nomicfoundation/hardhat-ethers@4.0.4': + resolution: + { + integrity: sha512-UTw3iM7AMZ1kZlzgJbtAEfWWDYjcnT0EZkRUZd1wIVtMOXIE4nc6Ya4veodAt/KpBhG+6W06g50W+Z/0wTm62g==, + } + peerDependencies: + hardhat: ^3.0.7 + + '@nomicfoundation/hardhat-ignition-ethers@3.0.7': + resolution: + { + integrity: sha512-WNVm1vKSl+CU8MvLDR/iERe+ZABy6h+MflJqrPw6TgKgrCJPlDzfZo++FC1dPKdPRJKFWC+/bw8uQCfET8LJQQ==, + } + peerDependencies: + '@nomicfoundation/hardhat-ethers': ^4.0.0 + '@nomicfoundation/hardhat-ignition': ^3.0.7 + '@nomicfoundation/hardhat-verify': ^3.0.0 + '@nomicfoundation/ignition-core': ^3.0.7 + ethers: ^6.14.0 + hardhat: ^3.1.5 + + '@nomicfoundation/hardhat-ignition@3.0.7': + resolution: + { + integrity: sha512-yzVXNHoEanOGRFMkpgvGaWw4Kev5ragxFFyrKITKtiJMdChdGNAPlbsk8QJzn4028aSAnjRfqKTOAeeBlsqFig==, + } + peerDependencies: + '@nomicfoundation/hardhat-verify': ^3.0.0 + hardhat: ^3.1.5 + + '@nomicfoundation/hardhat-keystore@3.0.4': + resolution: + { + integrity: sha512-g1++qRzyYyaItbphDUctEZ3THI7KCgeuTvDV9/jL1OU2YlFXZOoi8gqR+tSaTOC41Kl24kyTfgnWlQgg6Q9e0w==, + } + peerDependencies: + hardhat: ^3.0.0 + + '@nomicfoundation/hardhat-mocha@3.0.10': + resolution: + { + integrity: sha512-+QFJfIH0bCSGUeeGlfmN1L/fjs+7DhXzdwxCDdS+DhxfKzqSB8TycdYXnkPonkdckjL/IIS3cYzqZegh+EksNg==, + } + peerDependencies: + hardhat: ^3.0.12 + mocha: ^11.0.0 + + '@nomicfoundation/hardhat-network-helpers@3.0.3': + resolution: + { + integrity: sha512-FqXD8CPFNdluEhELqNV/Q0grOQtlwRWr28LW+/NTas3rrDAXpNOIPCCq3RIXJIqsdbNPQsG2FpnfKj9myqIsKQ==, + } + peerDependencies: + hardhat: ^3.0.0 + + '@nomicfoundation/hardhat-toolbox-mocha-ethers@3.0.2': + resolution: + { + integrity: sha512-45EZqxWtQxlvwDZilxOI+tSNFn/J+1ITtyqpUQgNhXhYA9+LUdbUx+PmiCWPrtEXzBVANYka6mhNvBr2ZwNBUg==, + } + peerDependencies: + '@nomicfoundation/hardhat-ethers': ^4.0.0 + '@nomicfoundation/hardhat-ethers-chai-matchers': ^3.0.0 + '@nomicfoundation/hardhat-ignition': ^3.0.0 + '@nomicfoundation/hardhat-ignition-ethers': ^3.0.0 + '@nomicfoundation/hardhat-keystore': ^3.0.0 + '@nomicfoundation/hardhat-mocha': ^3.0.0 + '@nomicfoundation/hardhat-network-helpers': ^3.0.0 + '@nomicfoundation/hardhat-typechain': ^3.0.0 + '@nomicfoundation/hardhat-verify': ^3.0.0 + '@nomicfoundation/ignition-core': ^3.0.0 + chai: ^5.1.2 + ethers: ^6.14.0 + hardhat: ^3.0.0 + mocha: ^11.0.0 + + '@nomicfoundation/hardhat-typechain@3.0.2': + resolution: + { + integrity: sha512-UAHwyzkkF6jdpUe/2ncM39S3dwX0Ol283zsQ4Rj8/1efl22/CiPoq3KhONQfU5JsY2RQutbcJguY+IROWFT8sw==, + } + peerDependencies: + '@nomicfoundation/hardhat-ethers': ^4.0.0 + ethers: ^6.14.0 + hardhat: ^3.1.6 + + '@nomicfoundation/hardhat-utils@3.0.6': + resolution: + { + integrity: sha512-AD/LPNdjXNFRrZcaAAewgJpdnHpPppZxo5p+x6wGMm5Hz4B3+oLf/LUzVn8qb4DDy9RE2c24l2F8vmL/w6ZuXg==, + } + + '@nomicfoundation/hardhat-vendored@3.0.1': + resolution: + { + integrity: sha512-jBOAqmEAMJ8zdfiQmTLV+c0IaSyySqkDSJ9spTy8Ts/m/mO8w364TClyfn+p4ZpxBjyX4LMa3NfC402hoDtwCg==, + } + + '@nomicfoundation/hardhat-verify@3.0.10': + resolution: + { + integrity: sha512-nJZTIrKK3fOd5erYBzXRbGoR9PNh5gzjc6p6mEmN4Cz7RU8ARZbDhBjD4YuJTHEQ4cYkF+Wd1mZQjfV3zVMarw==, + } + peerDependencies: + hardhat: ^3.1.6 + + '@nomicfoundation/hardhat-zod-utils@3.0.1': + resolution: + { + integrity: sha512-I6/pyYiS9p2lLkzQuedr1ScMocH+ew8l233xTi+LP92gjEiviJDxselpkzgU01MUM0t6BPpfP8yMO958LDEJVg==, + } + peerDependencies: + zod: ^3.23.8 + + '@nomicfoundation/ignition-core@3.0.7': + resolution: + { + integrity: sha512-zlwXshSZIQQ4LfZBnirETQ/eNfEcxk9sRkPCYoeaGBEC8QPtt7KBj+rP7YYWM+IPVQNU4OjIVfQs004unpXB4w==, + } + + '@nomicfoundation/ignition-ui@3.0.7': + resolution: + { + integrity: sha512-K7bL7V1cEh7nccAiWD1L15QdtNiPoM37IJdvBhgxV4sa25//r9yIxHf/DHI52qxQXqQuXKWdiBZBcbldtStM1Q==, + } + + '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': + resolution: + { + integrity: sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==, + } + engines: { node: '>= 12' } + + '@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2': + resolution: + { + integrity: sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==, + } + engines: { node: '>= 12' } + + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2': + resolution: + { + integrity: sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==, + } + engines: { node: '>= 12' } + + '@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2': + resolution: + { + integrity: sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==, + } + engines: { node: '>= 12' } + + '@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2': + resolution: + { + integrity: sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==, + } + engines: { node: '>= 12' } + + '@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2': + resolution: + { + integrity: sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==, + } + engines: { node: '>= 12' } + + '@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2': + resolution: + { + integrity: sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==, + } + engines: { node: '>= 12' } + + '@nomicfoundation/solidity-analyzer@0.1.2': + resolution: + { + integrity: sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==, + } + engines: { node: '>= 12' } + + '@pkgjs/parseargs@0.11.0': + resolution: + { + integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, + } + engines: { node: '>=14' } + + '@scure/base@1.1.9': + resolution: + { + integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==, + } + + '@scure/base@1.2.6': + resolution: + { + integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==, + } + + '@scure/bip32@1.4.0': + resolution: + { + integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==, + } + + '@scure/bip39@1.3.0': + resolution: + { + integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==, + } + + '@sentry/core@9.47.1': + resolution: + { + integrity: sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==, + } + engines: { node: '>=18' } + + '@streamparser/json-node@0.0.22': + resolution: + { + integrity: sha512-sJT2ptNRwqB1lIsQrQlCoWk5rF4tif9wDh+7yluAGijJamAhrHGYpFB/Zg3hJeceoZypi74ftXk8DHzwYpbZSg==, + } + + '@streamparser/json@0.0.22': + resolution: + { + integrity: sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==, + } + + '@typechain/ethers-v6@0.5.1': + resolution: + { + integrity: sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==, + } + peerDependencies: + ethers: 6.x + typechain: ^8.3.2 + typescript: '>=4.7.0' + + '@types/chai-as-promised@8.0.2': + resolution: + { + integrity: sha512-meQ1wDr1K5KRCSvG2lX7n7/5wf70BeptTKst0axGvnN6zqaVpRqegoIbugiAPSqOW9K9aL8gDVrm7a2LXOtn2Q==, + } + + '@types/chai@4.3.20': + resolution: + { + integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==, + } + + '@types/mocha@10.0.10': + resolution: + { + integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==, + } + + '@types/node@22.19.11': + resolution: + { + integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==, + } + + '@types/node@22.7.5': + resolution: + { + integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==, + } + + '@types/prettier@2.7.3': + resolution: + { + integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==, + } + + adm-zip@0.4.16: + resolution: + { + integrity: sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==, + } + engines: { node: '>=0.3.0' } + + aes-js@4.0.0-beta.5: + resolution: + { + integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==, + } + + ansi-colors@4.1.3: + resolution: + { + integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==, + } + engines: { node: '>=6' } + + ansi-regex@5.0.1: + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: '>=8' } + + ansi-regex@6.2.2: + resolution: + { + integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==, + } + engines: { node: '>=12' } + + ansi-styles@3.2.1: + resolution: + { + integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==, + } + engines: { node: '>=4' } + + ansi-styles@4.3.0: + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: '>=8' } + + ansi-styles@6.2.3: + resolution: + { + integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==, + } + engines: { node: '>=12' } + + argparse@2.0.1: + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } + + array-back@3.1.0: + resolution: + { + integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==, + } + engines: { node: '>=6' } + + array-back@4.0.2: + resolution: + { + integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==, + } + engines: { node: '>=8' } + + assertion-error@2.0.1: + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, + } + engines: { node: '>=12' } + + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } + + balanced-match@4.0.4: + resolution: + { + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, + } + engines: { node: 18 || 20 || >=22 } + + bn.js@4.12.3: + resolution: + { + integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==, + } + + bn.js@5.2.3: + resolution: + { + integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==, + } + + brace-expansion@1.1.12: + resolution: + { + integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==, + } + + brace-expansion@5.0.3: + resolution: + { + integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==, + } + engines: { node: 18 || 20 || >=22 } + + brorand@1.1.0: + resolution: + { + integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==, + } + + browser-stdout@1.3.1: + resolution: + { + integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==, + } + + camelcase@6.3.0: + resolution: + { + integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==, + } + engines: { node: '>=10' } + + cbor2@1.12.0: + resolution: + { + integrity: sha512-3Cco8XQhi27DogSp9Ri6LYNZLi/TBY/JVnDe+mj06NkBjW/ZYOtekaEU4wZ4xcRMNrFkDv8KNtOAqHyDfz3lYg==, + } + engines: { node: '>=18.7' } + + chai-as-promised@8.0.2: + resolution: + { + integrity: sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==, + } + peerDependencies: + chai: '>= 2.1.2 < 7' + + chai@5.3.3: + resolution: + { + integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==, + } + engines: { node: '>=18' } + + chalk@2.4.2: + resolution: + { + integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==, + } + engines: { node: '>=4' } + + chalk@4.1.2: + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: '>=10' } + + chalk@5.6.2: + resolution: + { + integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==, + } + engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } + + check-error@2.1.3: + resolution: + { + integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==, + } + engines: { node: '>= 16' } + + chokidar@4.0.3: + resolution: + { + integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==, + } + engines: { node: '>= 14.16.0' } + + cliui@8.0.1: + resolution: + { + integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, + } + engines: { node: '>=12' } + + color-convert@1.9.3: + resolution: + { + integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==, + } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: '>=7.0.0' } + + color-name@1.1.3: + resolution: + { + integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==, + } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } + + command-line-args@5.2.1: + resolution: + { + integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==, + } + engines: { node: '>=4.0.0' } + + command-line-usage@6.1.3: + resolution: + { + integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==, + } + engines: { node: '>=8.0.0' } + + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: '>= 8' } + + debug@4.4.3: + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: '>=6.0' } + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@4.0.0: + resolution: + { + integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==, + } + engines: { node: '>=10' } + + deep-eql@5.0.2: + resolution: + { + integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==, + } + engines: { node: '>=6' } + + deep-extend@0.6.0: + resolution: + { + integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==, + } + engines: { node: '>=4.0.0' } + + diff@7.0.0: + resolution: + { + integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==, + } + engines: { node: '>=0.3.1' } + + eastasianwidth@0.2.0: + resolution: + { + integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, + } + + elliptic@6.6.1: + resolution: + { + integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==, + } + + emoji-regex@8.0.0: + resolution: + { + integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, + } + + emoji-regex@9.2.2: + resolution: + { + integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, + } + + enquirer@2.4.1: + resolution: + { + integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==, + } + engines: { node: '>=8.6' } + + env-paths@2.2.1: + resolution: + { + integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==, + } + engines: { node: '>=6' } + + esbuild@0.27.3: + resolution: + { + integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==, + } + engines: { node: '>=18' } + hasBin: true + + escalade@3.2.0: + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: '>=6' } + + escape-string-regexp@1.0.5: + resolution: + { + integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==, + } + engines: { node: '>=0.8.0' } + + escape-string-regexp@4.0.0: + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: '>=10' } + + ethereum-cryptography@2.2.1: + resolution: + { + integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==, + } + + ethers@6.16.0: + resolution: + { + integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==, + } + engines: { node: '>=14.0.0' } + + fast-equals@5.4.0: + resolution: + { + integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==, + } + engines: { node: '>=6.0.0' } + + find-replace@3.0.0: + resolution: + { + integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==, + } + engines: { node: '>=4.0.0' } + + find-up@5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: '>=10' } + + flat@5.0.2: + resolution: + { + integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==, + } + hasBin: true + + foreground-child@3.3.1: + resolution: + { + integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==, + } + engines: { node: '>=14' } + + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262: + resolution: + { + tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262, + } + version: 1.9.4 + + fs-extra@7.0.1: + resolution: + { + integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==, + } + engines: { node: '>=6 <7 || >=8' } + + fs.realpath@1.0.0: + resolution: + { + integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, + } + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + get-caller-file@2.0.5: + resolution: + { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, + } + engines: { node: 6.* || 8.* || >= 10.* } + + get-tsconfig@4.13.6: + resolution: + { + integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==, + } + + glob@10.5.0: + resolution: + { + integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==, + } + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.1.7: + resolution: + { + integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==, + } + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + graceful-fs@4.2.11: + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, + } + + hardhat@3.1.9: + resolution: + { + integrity: sha512-LCThGBbROzRiI0RwD85i61bYM380ZWwVZ5XRHsY/ADUWnoMKIRYN835f4rQxT2qkVvDnZB0jbmU3RRS/MPy7Gw==, + } + hasBin: true + + has-flag@3.0.0: + resolution: + { + integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==, + } + engines: { node: '>=4' } + + has-flag@4.0.0: + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: '>=8' } + + hash.js@1.1.7: + resolution: + { + integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==, + } + + he@1.2.0: + resolution: + { + integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==, + } + hasBin: true + + hmac-drbg@1.0.1: + resolution: + { + integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==, + } + + immer@10.0.2: + resolution: + { + integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==, + } + + inflight@1.0.6: + resolution: + { + integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, + } + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } + + is-fullwidth-code-point@3.0.0: + resolution: + { + integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, + } + engines: { node: '>=8' } + + is-path-inside@3.0.3: + resolution: + { + integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==, + } + engines: { node: '>=8' } + + is-plain-obj@2.1.0: + resolution: + { + integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==, + } + engines: { node: '>=8' } + + is-unicode-supported@0.1.0: + resolution: + { + integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==, + } + engines: { node: '>=10' } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } + + jackspeak@3.4.3: + resolution: + { + integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, + } + + js-sha3@0.8.0: + resolution: + { + integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==, + } + + js-yaml@4.1.1: + resolution: + { + integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, + } + hasBin: true + + json-stream-stringify@3.1.6: + resolution: + { + integrity: sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==, + } + engines: { node: '>=7.10.1' } + + json-stringify-safe@5.0.1: + resolution: + { + integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==, + } + + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: '>=6' } + hasBin: true + + jsonfile@4.0.0: + resolution: + { + integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==, + } + + kleur@3.0.3: + resolution: + { + integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==, + } + engines: { node: '>=6' } + + locate-path@6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: '>=10' } + + lodash-es@4.17.21: + resolution: + { + integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==, + } + + lodash.camelcase@4.3.0: + resolution: + { + integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==, + } + + lodash@4.17.23: + resolution: + { + integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==, + } + + log-symbols@4.1.0: + resolution: + { + integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==, + } + engines: { node: '>=10' } + + loupe@3.2.1: + resolution: + { + integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==, + } + + lru-cache@10.4.3: + resolution: + { + integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, + } + + micro-eth-signer@0.14.0: + resolution: + { + integrity: sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw==, + } + + micro-packed@0.7.3: + resolution: + { + integrity: sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==, + } + + minimalistic-assert@1.0.1: + resolution: + { + integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==, + } + + minimalistic-crypto-utils@1.0.1: + resolution: + { + integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==, + } + + minimatch@3.1.3: + resolution: + { + integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==, + } + + minimatch@9.0.6: + resolution: + { + integrity: sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==, + } + engines: { node: '>=16 || 14 >=14.17' } + + minimist@1.2.8: + resolution: + { + integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, + } + + minipass@7.1.3: + resolution: + { + integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, + } + engines: { node: '>=16 || 14 >=14.17' } + + mkdirp@1.0.4: + resolution: + { + integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==, + } + engines: { node: '>=10' } + hasBin: true + + mocha@11.7.5: + resolution: + { + integrity: sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + hasBin: true + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + ndjson@2.0.0: + resolution: + { + integrity: sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==, + } + engines: { node: '>=10' } + hasBin: true + + once@1.4.0: + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, + } + + p-limit@3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: '>=10' } + + p-locate@5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: '>=10' } + + p-map@7.0.4: + resolution: + { + integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==, + } + engines: { node: '>=18' } + + package-json-from-dist@1.0.1: + resolution: + { + integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, + } + + path-exists@4.0.0: + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: '>=8' } + + path-is-absolute@1.0.1: + resolution: + { + integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, + } + engines: { node: '>=0.10.0' } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: '>=8' } + + path-scurry@1.11.1: + resolution: + { + integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, + } + engines: { node: '>=16 || 14 >=14.18' } + + pathval@2.0.1: + resolution: + { + integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==, + } + engines: { node: '>= 14.16' } + + picocolors@1.1.1: + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } + + prettier@2.8.8: + resolution: + { + integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==, + } + engines: { node: '>=10.13.0' } + hasBin: true + + prompts@2.4.2: + resolution: + { + integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==, + } + engines: { node: '>= 6' } + + randombytes@2.1.0: + resolution: + { + integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==, + } + + readable-stream@3.6.2: + resolution: + { + integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, + } + engines: { node: '>= 6' } + + readdirp@4.1.2: + resolution: + { + integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==, + } + engines: { node: '>= 14.18.0' } + + reduce-flatten@2.0.0: + resolution: + { + integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==, + } + engines: { node: '>=6' } + + require-directory@2.1.1: + resolution: + { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, + } + engines: { node: '>=0.10.0' } + + resolve-pkg-maps@1.0.0: + resolution: + { + integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, + } + + resolve.exports@2.0.3: + resolution: + { + integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==, + } + engines: { node: '>=10' } + + rfdc@1.4.1: + resolution: + { + integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, + } + + safe-buffer@5.2.1: + resolution: + { + integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, + } + + semver@7.7.4: + resolution: + { + integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, + } + engines: { node: '>=10' } + hasBin: true + + serialize-javascript@6.0.2: + resolution: + { + integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==, + } + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: '>=8' } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: '>=8' } + + signal-exit@4.1.0: + resolution: + { + integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, + } + engines: { node: '>=14' } + + sisteransi@1.0.5: + resolution: + { + integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==, + } + + split2@3.2.2: + resolution: + { + integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==, + } + + string-format@2.0.0: + resolution: + { + integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==, + } + + string-width@4.2.3: + resolution: + { + integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, + } + engines: { node: '>=8' } + + string-width@5.1.2: + resolution: + { + integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==, + } + engines: { node: '>=12' } + + string_decoder@1.3.0: + resolution: + { + integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, + } + + strip-ansi@6.0.1: + resolution: + { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, + } + engines: { node: '>=8' } + + strip-ansi@7.1.2: + resolution: + { + integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==, + } + engines: { node: '>=12' } + + strip-json-comments@3.1.1: + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: '>=8' } + + supports-color@5.5.0: + resolution: + { + integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==, + } + engines: { node: '>=4' } + + supports-color@7.2.0: + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: '>=8' } + + supports-color@8.1.1: + resolution: + { + integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==, + } + engines: { node: '>=10' } + + table-layout@1.0.2: + resolution: + { + integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==, + } + engines: { node: '>=8.0.0' } + + through2@4.0.2: + resolution: + { + integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==, + } + + ts-command-line-args@2.5.1: + resolution: + { + integrity: sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==, + } + hasBin: true + + ts-essentials@7.0.3: + resolution: + { + integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==, + } + peerDependencies: + typescript: '>=3.7.0' + + tslib@2.7.0: + resolution: + { + integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==, + } + + tsx@4.21.0: + resolution: + { + integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==, + } + engines: { node: '>=18.0.0' } + hasBin: true + + typechain@8.3.2: + resolution: + { + integrity: sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==, + } + hasBin: true + peerDependencies: + typescript: '>=4.3.0' + + typescript@5.8.3: + resolution: + { + integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==, + } + engines: { node: '>=14.17' } + hasBin: true + + typical@4.0.0: + resolution: + { + integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==, + } + engines: { node: '>=8' } + + typical@5.2.0: + resolution: + { + integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==, + } + engines: { node: '>=8' } + + undici-types@6.19.8: + resolution: + { + integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==, + } + + undici-types@6.21.0: + resolution: + { + integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, + } + + undici@6.23.0: + resolution: + { + integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==, + } + engines: { node: '>=18.17' } + + universalify@0.1.2: + resolution: + { + integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==, + } + engines: { node: '>= 4.0.0' } + + util-deprecate@1.0.2: + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } + + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: '>= 8' } + hasBin: true + + wordwrapjs@4.0.1: + resolution: + { + integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==, + } + engines: { node: '>=8.0.0' } + + workerpool@9.3.4: + resolution: + { + integrity: sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==, + } + + wrap-ansi@7.0.0: + resolution: + { + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, + } + engines: { node: '>=10' } + + wrap-ansi@8.1.0: + resolution: + { + integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==, + } + engines: { node: '>=12' } + + wrappy@1.0.2: + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, + } + + ws@8.17.1: + resolution: + { + integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==, + } + engines: { node: '>=10.0.0' } + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.19.0: + resolution: + { + integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==, + } + engines: { node: '>=10.0.0' } + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: + { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, + } + engines: { node: '>=10' } + + yargs-parser@21.1.1: + resolution: + { + integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, + } + engines: { node: '>=12' } + + yargs-unparser@2.0.0: + resolution: + { + integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==, + } + engines: { node: '>=10' } + + yargs@17.7.2: + resolution: + { + integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, + } + engines: { node: '>=12' } + + yocto-queue@0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: '>=10' } + + zod@3.25.76: + resolution: + { + integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==, + } + +snapshots: + '@adraffy/ens-normalize@1.10.1': {} + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@ethersproject/abi@5.8.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/hash': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/abstract-provider@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/networks': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/transactions': 5.8.0 + '@ethersproject/web': 5.8.0 + + '@ethersproject/abstract-signer@5.8.0': + dependencies: + '@ethersproject/abstract-provider': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + + '@ethersproject/address@5.6.1': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/rlp': 5.8.0 + + '@ethersproject/address@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/rlp': 5.8.0 + + '@ethersproject/base64@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + + '@ethersproject/bignumber@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + bn.js: 5.2.3 + + '@ethersproject/bytes@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/constants@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + + '@ethersproject/hash@5.8.0': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/base64': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/keccak256@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + js-sha3: 0.8.0 + + '@ethersproject/logger@5.8.0': {} + + '@ethersproject/networks@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/properties@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/rlp@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/signing-key@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + bn.js: 5.2.3 + elliptic: 6.6.1 + hash.js: 1.1.7 + + '@ethersproject/strings@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/transactions@5.8.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/rlp': 5.8.0 + '@ethersproject/signing-key': 5.8.0 + + '@ethersproject/web@5.8.0': + dependencies: + '@ethersproject/base64': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@noble/ciphers@1.2.1': {} + + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/curves@1.8.2': + dependencies: + '@noble/hashes': 1.7.2 + + '@noble/hashes@1.3.2': {} + + '@noble/hashes@1.4.0': {} + + '@noble/hashes@1.7.1': {} + + '@noble/hashes@1.7.2': {} + + '@nomicfoundation/edr-darwin-arm64@0.12.0-next.24': {} + + '@nomicfoundation/edr-darwin-x64@0.12.0-next.24': {} + + '@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.24': {} + + '@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.24': {} + + '@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.24': {} + + '@nomicfoundation/edr-linux-x64-musl@0.12.0-next.24': {} + + '@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.24': {} + + '@nomicfoundation/edr@0.12.0-next.24': + dependencies: + '@nomicfoundation/edr-darwin-arm64': 0.12.0-next.24 + '@nomicfoundation/edr-darwin-x64': 0.12.0-next.24 + '@nomicfoundation/edr-linux-arm64-gnu': 0.12.0-next.24 + '@nomicfoundation/edr-linux-arm64-musl': 0.12.0-next.24 + '@nomicfoundation/edr-linux-x64-gnu': 0.12.0-next.24 + '@nomicfoundation/edr-linux-x64-musl': 0.12.0-next.24 + '@nomicfoundation/edr-win32-x64-msvc': 0.12.0-next.24 + + '@nomicfoundation/hardhat-errors@3.0.6': + dependencies: + '@nomicfoundation/hardhat-utils': 3.0.6 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-ethers-chai-matchers@3.0.2(@nomicfoundation/hardhat-ethers@4.0.4(hardhat@3.1.9))(chai@5.3.3)(ethers@6.16.0)(hardhat@3.1.9)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-ethers': 4.0.4(hardhat@3.1.9) + '@nomicfoundation/hardhat-utils': 3.0.6 + '@types/chai-as-promised': 8.0.2 + chai: 5.3.3 + chai-as-promised: 8.0.2(chai@5.3.3) + deep-eql: 5.0.2 + ethers: 6.16.0 + hardhat: 3.1.9 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-ethers@4.0.4(hardhat@3.1.9)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + debug: 4.4.3(supports-color@8.1.1) + ethereum-cryptography: 2.2.1 + ethers: 6.16.0 + hardhat: 3.1.9 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@nomicfoundation/hardhat-ignition-ethers@3.0.7(@nomicfoundation/hardhat-ethers@4.0.4(hardhat@3.1.9))(@nomicfoundation/hardhat-ignition@3.0.7(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(hardhat@3.1.9))(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(@nomicfoundation/ignition-core@3.0.7)(ethers@6.16.0)(hardhat@3.1.9)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-ethers': 4.0.4(hardhat@3.1.9) + '@nomicfoundation/hardhat-ignition': 3.0.7(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(hardhat@3.1.9) + '@nomicfoundation/hardhat-verify': 3.0.10(hardhat@3.1.9) + '@nomicfoundation/ignition-core': 3.0.7 + ethers: 6.16.0 + hardhat: 3.1.9 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-ignition@3.0.7(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(hardhat@3.1.9)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-verify': 3.0.10(hardhat@3.1.9) + '@nomicfoundation/ignition-core': 3.0.7 + '@nomicfoundation/ignition-ui': 3.0.7 + chalk: 5.6.2 + debug: 4.4.3(supports-color@8.1.1) + hardhat: 3.1.9 + json5: 2.2.3 + prompts: 2.4.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@nomicfoundation/hardhat-keystore@3.0.4(hardhat@3.1.9)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/hashes': 1.7.1 + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + chalk: 5.6.2 + debug: 4.4.3(supports-color@8.1.1) + hardhat: 3.1.9 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-mocha@3.0.10(hardhat@3.1.9)(mocha@11.7.5)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + chalk: 5.6.2 + debug: 4.4.3(supports-color@8.1.1) + hardhat: 3.1.9 + mocha: 11.7.5 + tsx: 4.21.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-network-helpers@3.0.3(hardhat@3.1.9)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + hardhat: 3.1.9 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-toolbox-mocha-ethers@3.0.2(56383e692657429e8f088e48bc1cde98)': + dependencies: + '@nomicfoundation/hardhat-ethers': 4.0.4(hardhat@3.1.9) + '@nomicfoundation/hardhat-ethers-chai-matchers': 3.0.2(@nomicfoundation/hardhat-ethers@4.0.4(hardhat@3.1.9))(chai@5.3.3)(ethers@6.16.0)(hardhat@3.1.9) + '@nomicfoundation/hardhat-ignition': 3.0.7(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(hardhat@3.1.9) + '@nomicfoundation/hardhat-ignition-ethers': 3.0.7(@nomicfoundation/hardhat-ethers@4.0.4(hardhat@3.1.9))(@nomicfoundation/hardhat-ignition@3.0.7(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(hardhat@3.1.9))(@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9))(@nomicfoundation/ignition-core@3.0.7)(ethers@6.16.0)(hardhat@3.1.9) + '@nomicfoundation/hardhat-keystore': 3.0.4(hardhat@3.1.9) + '@nomicfoundation/hardhat-mocha': 3.0.10(hardhat@3.1.9)(mocha@11.7.5) + '@nomicfoundation/hardhat-network-helpers': 3.0.3(hardhat@3.1.9) + '@nomicfoundation/hardhat-typechain': 3.0.2(@nomicfoundation/hardhat-ethers@4.0.4(hardhat@3.1.9))(ethers@6.16.0)(hardhat@3.1.9)(typescript@5.8.3) + '@nomicfoundation/hardhat-verify': 3.0.10(hardhat@3.1.9) + '@nomicfoundation/ignition-core': 3.0.7 + chai: 5.3.3 + ethers: 6.16.0 + hardhat: 3.1.9 + mocha: 11.7.5 + + '@nomicfoundation/hardhat-typechain@3.0.2(@nomicfoundation/hardhat-ethers@4.0.4(hardhat@3.1.9))(ethers@6.16.0)(hardhat@3.1.9)(typescript@5.8.3)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-ethers': 4.0.4(hardhat@3.1.9) + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + '@typechain/ethers-v6': 0.5.1(ethers@6.16.0)(typechain@8.3.2(typescript@5.8.3))(typescript@5.8.3) + debug: 4.4.3(supports-color@8.1.1) + ethers: 6.16.0 + hardhat: 3.1.9 + typechain: 8.3.2(typescript@5.8.3) + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + - typescript + + '@nomicfoundation/hardhat-utils@3.0.6': + dependencies: + '@streamparser/json-node': 0.0.22 + debug: 4.4.3(supports-color@8.1.1) + env-paths: 2.2.1 + ethereum-cryptography: 2.2.1 + fast-equals: 5.4.0 + json-stream-stringify: 3.1.6 + rfdc: 1.4.1 + undici: 6.23.0 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-vendored@3.0.1': {} + + '@nomicfoundation/hardhat-verify@3.0.10(hardhat@3.1.9)': + dependencies: + '@ethersproject/abi': 5.8.0 + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + cbor2: 1.12.0 + chalk: 5.6.2 + debug: 4.4.3(supports-color@8.1.1) + hardhat: 3.1.9 + semver: 7.7.4 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-zod-utils@3.0.1(zod@3.25.76)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/ignition-core@3.0.7': + dependencies: + '@ethersproject/address': 5.6.1 + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/solidity-analyzer': 0.1.2 + cbor2: 1.12.0 + debug: 4.4.3(supports-color@8.1.1) + ethers: 6.16.0 + immer: 10.0.2 + lodash-es: 4.17.21 + ndjson: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@nomicfoundation/ignition-ui@3.0.7': {} + + '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer@0.1.2': + optionalDependencies: + '@nomicfoundation/solidity-analyzer-darwin-arm64': 0.1.2 + '@nomicfoundation/solidity-analyzer-darwin-x64': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-arm64-musl': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-x64-gnu': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-x64-musl': 0.1.2 + '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.2 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@scure/base@1.1.9': {} + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@sentry/core@9.47.1': {} + + '@streamparser/json-node@0.0.22': + dependencies: + '@streamparser/json': 0.0.22 + + '@streamparser/json@0.0.22': {} + + '@typechain/ethers-v6@0.5.1(ethers@6.16.0)(typechain@8.3.2(typescript@5.8.3))(typescript@5.8.3)': + dependencies: + ethers: 6.16.0 + lodash: 4.17.23 + ts-essentials: 7.0.3(typescript@5.8.3) + typechain: 8.3.2(typescript@5.8.3) + typescript: 5.8.3 + + '@types/chai-as-promised@8.0.2': + dependencies: + '@types/chai': 4.3.20 + + '@types/chai@4.3.20': {} + + '@types/mocha@10.0.10': {} + + '@types/node@22.19.11': + dependencies: + undici-types: 6.21.0 + + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + + '@types/prettier@2.7.3': {} + + adm-zip@0.4.16: {} + + aes-js@4.0.0-beta.5: {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + array-back@3.1.0: {} + + array-back@4.0.2: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + bn.js@4.12.3: {} + + bn.js@5.2.3: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.3: + dependencies: + balanced-match: 4.0.4 + + brorand@1.1.0: {} + + browser-stdout@1.3.1: {} + + camelcase@6.3.0: {} + + cbor2@1.12.0: {} + + chai-as-promised@8.0.2(chai@5.3.3): + dependencies: + chai: 5.3.3 + check-error: 2.1.3 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + command-line-args@5.2.1: + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + + command-line-usage@6.1.3: + dependencies: + array-back: 4.0.2 + chalk: 2.4.2 + table-layout: 1.0.2 + typical: 5.2.0 + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + + diff@7.0.0: {} + + eastasianwidth@0.2.0: {} + + elliptic@6.6.1: + dependencies: + bn.js: 4.12.3 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + env-paths@2.2.1: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + + ethers@6.16.0: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + fast-equals@5.4.0: {} + + find-replace@3.0.0: + dependencies: + array-back: 3.1.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat@5.0.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262: + {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.6 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.1.7: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.3 + once: 1.4.0 + path-is-absolute: 1.0.1 + + graceful-fs@4.2.11: {} + + hardhat@3.1.9: + dependencies: + '@nomicfoundation/edr': 0.12.0-next.24 + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-vendored': 3.0.1 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + '@nomicfoundation/solidity-analyzer': 0.1.2 + '@sentry/core': 9.47.1 + adm-zip: 0.4.16 + chalk: 5.6.2 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + enquirer: 2.4.1 + ethereum-cryptography: 2.2.1 + micro-eth-signer: 0.14.0 + p-map: 7.0.4 + resolve.exports: 2.0.3 + semver: 7.7.4 + tsx: 4.21.0 + ws: 8.19.0 + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + he@1.2.0: {} + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + immer@10.0.2: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-fullwidth-code-point@3.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@2.1.0: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-sha3@0.8.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-stream-stringify@3.1.6: {} + + json-stringify-safe@5.0.1: {} + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + kleur@3.0.3: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.camelcase@4.3.0: {} + + lodash@4.17.23: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + micro-eth-signer@0.14.0: + dependencies: + '@noble/curves': 1.8.2 + '@noble/hashes': 1.7.2 + micro-packed: 0.7.3 + + micro-packed@0.7.3: + dependencies: + '@scure/base': 1.2.6 + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@3.1.3: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.6: + dependencies: + brace-expansion: 5.0.3 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mkdirp@1.0.4: {} + + mocha@11.7.5: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.5.0 + he: 1.2.0 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + log-symbols: 4.1.0 + minimatch: 9.0.6 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.4 + yargs: 17.7.2 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + ms@2.1.3: {} + + ndjson@2.0.0: + dependencies: + json-stringify-safe: 5.0.1 + minimist: 1.2.8 + readable-stream: 3.6.2 + split2: 3.2.2 + through2: 4.0.2 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@7.0.4: {} + + package-json-from-dist@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + prettier@2.8.8: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + reduce-flatten@2.0.0: {} + + require-directory@2.1.1: {} + + resolve-pkg-maps@1.0.0: {} + + resolve.exports@2.0.3: {} + + rfdc@1.4.1: {} + + safe-buffer@5.2.1: {} + + semver@7.7.4: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + split2@3.2.2: + dependencies: + readable-stream: 3.6.2 + + string-format@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@3.1.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + table-layout@1.0.2: + dependencies: + array-back: 4.0.2 + deep-extend: 0.6.0 + typical: 5.2.0 + wordwrapjs: 4.0.1 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + ts-command-line-args@2.5.1: + dependencies: + chalk: 4.1.2 + command-line-args: 5.2.1 + command-line-usage: 6.1.3 + string-format: 2.0.0 + + ts-essentials@7.0.3(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tslib@2.7.0: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + typechain@8.3.2(typescript@5.8.3): + dependencies: + '@types/prettier': 2.7.3 + debug: 4.4.3(supports-color@8.1.1) + fs-extra: 7.0.1 + glob: 7.1.7 + js-sha3: 0.8.0 + lodash: 4.17.23 + mkdirp: 1.0.4 + prettier: 2.8.8 + ts-command-line-args: 2.5.1 + ts-essentials: 7.0.3(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + typical@4.0.0: {} + + typical@5.2.0: {} + + undici-types@6.19.8: {} + + undici-types@6.21.0: {} + + undici@6.23.0: {} + + universalify@0.1.2: {} + + util-deprecate@1.0.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wordwrapjs@4.0.1: + dependencies: + reduce-flatten: 2.0.0 + typical: 5.2.0 + + workerpool@9.3.4: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + ws@8.17.1: {} + + ws@8.19.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} diff --git a/sessions/intro-to-testing/test/CounterV1.ts b/sessions/intro-to-testing/test/CounterV1.ts new file mode 100644 index 00000000..4cf67d55 --- /dev/null +++ b/sessions/intro-to-testing/test/CounterV1.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import { Contract } from 'ethers'; +import { network } from 'hardhat'; + +const { ethers } = await network.connect(); + +// deploy util function +const deployCounterV1 = async (contract: string) => { + const CounterV1: Contract = await ethers.deployContract(contract); + return CounterV1; +}; + +describe('CounterV1 Contract Suite', async () => { + describe('Deployment', () => { + it('should return default storage value', async () => { + // call our deploy util fn + const deployedCounterV1: Contract = await deployCounterV1('CounterV1'); + // assert that default storage value of x = 0 + expect(await deployedCounterV1.x()).to.eq(0); + }); + }); + + describe('Transactions', () => { + it('should increase x value by 1', async () => { + // call our deploy util fn + const deployedCounterV1: Contract = await deployCounterV1('CounterV1'); + const count1 = await deployedCounterV1.x(); + + await deployedCounterV1.inc(); + + const count2 = await deployedCounterV1.x(); + console.log('count 2____', count2); + expect(count2).to.eq(count1 + 1n); + }); + + it('should increase x value when inc() is called multiple times ', async () => { + // call our deploy util fn + const deployedCounterV1: Contract = await deployCounterV1('CounterV1'); + const count1 = await deployedCounterV1.x(); + + const increaseNumber = 1n; + await deployedCounterV1.inc(); + + const count2 = await deployedCounterV1.x(); + console.log('count 2____', count2); + expect(count2).to.eq(count1 + 1n); + + await deployedCounterV1.inc(); + + const count3 = await deployedCounterV1.x(); + expect(count3).to.eq(count2 + increaseNumber); + }); + }); +}); diff --git a/sessions/intro-to-testing/test/TimelockV1.ts b/sessions/intro-to-testing/test/TimelockV1.ts new file mode 100644 index 00000000..30ed7f25 --- /dev/null +++ b/sessions/intro-to-testing/test/TimelockV1.ts @@ -0,0 +1,137 @@ +import { expect } from 'chai'; +import { BigNumberish, Contract } from 'ethers'; +import { network } from 'hardhat'; + +const { ethers, networkHelpers } = await network.connect(); +let TimelockV1: any; +let addr1: any; +let addr2: any; + +interface Vault { + balance: BigNumberish; + unlockTime: BigNumberish; + active: boolean; +} + +// util functions // +const toWei = (amount: string) => ethers.parseEther(amount); // parse number to 18es + +const fromWei = (amount: BigNumberish) => ethers.formatEther(amount); // format 18es to human-readable version + +const setTime = async (hours: number = 0) => + (await networkHelpers.time.latest()) + hours * 60 * 60; + +const setHour = async () => (await networkHelpers.time.latest()) + 60 * 60; + +const increaseBlockTimestamp = async (hours: number) => { + const provider = ethers.provider; + await provider.send('evm_increaseTime', [hours * 3600]); + await provider.send('evm_mine', []); +}; + +// const iterate = (arrayLength: number) => { +// for (let i =0; i < arrayLength; i++) { + +// } + +// } + +describe('TimelockV1 Test Suite', () => { + beforeEach(async () => { + TimelockV1 = await ethers.deployContract('TimeLockV1'); + [addr1, addr2] = await ethers.getSigners(); + }); + + describe('Deployment', () => { + it('should set default storage values', async () => { + let vaults = await TimelockV1.getAllVaults(addr1); + // assert that there are no vaults + expect(vaults.length).to.be.eq(0); + + // assert that attempt to access non-existent ID reverts + await expect(TimelockV1.getVault(addr1, 0)).to.be.revertedWith( + 'Invalid vault ID' + ); + + // assert that attempt to access non-existent ID reverts + await expect(TimelockV1.getVault(addr2, 0)).to.be.revertedWith( + 'Invalid vault ID' + ); + }); + }); + + describe('Transactions', () => { + describe('Deposit Transction', () => { + describe('Validations', () => { + it('should revert attempt to deposit 0 ETH to the vault', async () => { + let amount = '0'; + + const toWeiAmount = toWei('1'); + + await expect( + TimelockV1.connect(addr1).deposit(0, { value: toWei(amount) }) + ).to.be.revertedWith('Deposit must be greater than zero'); + }); + + it('should revert attempt to set unlock time that is past', async () => { + let amount = '2'; + let pastTime = 1771933663; + await expect( + TimelockV1.connect(addr1).deposit(pastTime, { + value: toWei(amount), + }) + ).to.be.revertedWith('Deposit must be greater than zero'); + }); + }); + + describe('Success Deposit Txn', () => { + it('should deposit ETH to vault', async () => { + const unlockTime = setTime(1); + const depositAmount = toWei('1'); + await TimelockV1.connect(addr1).deposit(unlockTime, { + value: depositAmount, + }); + + let addr1Vault = await TimelockV1.getVault(addr1, 0); + expect(addr1Vault.balance).to.be.eq(depositAmount); + expect(addr1Vault.unlockTime).to.eq(await unlockTime); + expect(addr1Vault.active).to.be.eq(true); + expect(addr1Vault.isUnlocked).to.be.eq(false); + + // assert that addr1 total vault count is 1 + expect(await TimelockV1.getVaultCount(addr1)).to.be.eq(1); + }); + + it.only('should deposit ETH to vault multiple times', async () => { + const unlockTime = await setTime(1); + const depositAmount1 = toWei('1'); + const depositAmount2 = toWei('2'); + // deposit 1 + await TimelockV1.connect(addr1).deposit(unlockTime, { + value: depositAmount1, + }); + + // deposit 2 + await TimelockV1.connect(addr1).deposit(unlockTime, { + value: depositAmount2, + }); + + let addr1Vaults = await TimelockV1.getAllVaults(addr1); + addr1Vaults.forEach((e: any, i: any) => { + if (i === 0) { + expect(e.balance).to.eq(depositAmount1); + expect(e.unlockTime).to.eq(unlockTime); + expect(e.active).to.be.eq(true); + } else if (i === 1) { + expect(e.balance).to.eq(depositAmount2); + expect(e.unlockTime).to.eq(unlockTime); + expect(e.active).to.be.eq(true); + } + }); + + expect(await TimelockV1.getVaultCount(addr1)).to.be.eq(2); + }); + }); + }); + }); +}); diff --git a/sessions/intro-to-testing/tsconfig.json b/sessions/intro-to-testing/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/sessions/intro-to-testing/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} diff --git a/sessions/timelock/.gitignore b/sessions/timelock/.gitignore new file mode 100644 index 00000000..85198aaa --- /dev/null +++ b/sessions/timelock/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/sessions/timelock/.gitmodules b/sessions/timelock/.gitmodules new file mode 100644 index 00000000..888d42dc --- /dev/null +++ b/sessions/timelock/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/sessions/timelock/README.md b/sessions/timelock/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/sessions/timelock/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/sessions/timelock/foundry.lock b/sessions/timelock/foundry.lock new file mode 100644 index 00000000..bc06b89b --- /dev/null +++ b/sessions/timelock/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.15.0", + "rev": "0844d7e1fc5e60d77b68e469bff60265f236c398" + } + } +} \ No newline at end of file diff --git a/sessions/timelock/foundry.toml b/sessions/timelock/foundry.toml new file mode 100644 index 00000000..7b6b3f60 --- /dev/null +++ b/sessions/timelock/foundry.toml @@ -0,0 +1,16 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/", + "@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/", +] + +eth_rpc_url = "https://ethereum-sepolia-rpc.publicnode.com" +# fork-block-number = 10382697 10382882 +# fork-block-number = 10382882 +# fork-block-number = 10279235 +fork-block-number = 10383134 diff --git a/sessions/timelock/mappings.txt b/sessions/timelock/mappings.txt new file mode 100644 index 00000000..feaba2dd --- /dev/null +++ b/sessions/timelock/mappings.txt @@ -0,0 +1 @@ +forge-std/=lib/forge-std/src/ diff --git a/sessions/timelock/src/CounterV3.sol b/sessions/timelock/src/CounterV3.sol new file mode 100644 index 00000000..269dfb0c --- /dev/null +++ b/sessions/timelock/src/CounterV3.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract CounterV3 { + uint256 public number; + address public owner; + + mapping(address => bool) public approvedCallers; + mapping(address => bool) public pendingApproval; + + event ApprovalRequested(address indexed caller); + event ApprovalGranted(address indexed caller); + event ApprovalRevoked(address indexed caller); + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + modifier onlyAuthorized() { + if (msg.sender != owner) { + require(approvedCallers[msg.sender], "Owner consent required"); + } + _; + } + + function requestPrivilege() external { + require(msg.sender != owner, "Owner does not need consent"); + + pendingApproval[msg.sender] = true; + emit ApprovalRequested(msg.sender); + } + + function grantPrivilege(address caller) external onlyOwner { + require(pendingApproval[caller], "No pending request"); + + approvedCallers[caller] = true; + pendingApproval[caller] = false; + emit ApprovalGranted(caller); + } + + function revokePrivilege(address caller) external onlyOwner { + require(approvedCallers[caller], "Caller not approved"); + + approvedCallers[caller] = false; + emit ApprovalRevoked(caller); + } + + function setNumber(uint256 newNumber) public onlyAuthorized { + number = newNumber; + } + + function increment() public onlyAuthorized { + number++; + } +} diff --git a/sessions/timelock/src/TimelockV3.sol b/sessions/timelock/src/TimelockV3.sol new file mode 100644 index 00000000..655c8cb6 --- /dev/null +++ b/sessions/timelock/src/TimelockV3.sol @@ -0,0 +1,239 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IErc20} from "./interfaces/IERC20.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; +import {console} from "forge-std/console.sol"; + +contract TimeLockV3 { + IErc20 public immutable token; + address public owner = 0xCe4F29b6A3955Fa50b46DFdFAe2F6352F16A77BB; + address public pendingOwner; + AggregatorV3Interface internal priceFeed; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + struct Vault { + uint256 balance; + uint256 tokenBalance; + uint256 unlockTime; + bool active; + } + + constructor(address _token) { + token = IErc20(_token); + owner = msg.sender; + priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306); // ETH-USD sepolia + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + event EmergencyWithdrawn(address indexed owner, uint256 amount); + + //INTERNAL FUNCTIONS + function _depositRatio(uint256 _totalDeposit) internal pure returns (uint256 _tokenAmount) { + _tokenAmount = _totalDeposit * 10; // Token Ratio + } + + function deposit(uint256 _unlockTime) external payable { + uint256 amount = ethToUSD(msg.value); + require(amount >= 100, "you must deposit 100 USD worth of ETH"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + // Create new vault + vaults[msg.sender].push(Vault({balance: msg.value, unlockTime: _unlockTime, tokenBalance: 1, active: true})); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + uint256 tokenAmount = userVault.tokenBalance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.tokenBalance = 0; + userVault.active = false; + + require(token.transferFrom(msg.sender, address(this), tokenAmount), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + uint256 totalTokens = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + uint256 tokenAmount = userVaults[i].tokenBalance; + + userVaults[i].balance = 0; + userVaults[i].tokenBalance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + totalTokens += tokenAmount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + require(token.transfer(msg.sender, totalTokens), "Token transfer failed"); + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function emergencyWithdraw() external onlyOwner returns (uint256 amount) { + amount = address(this).balance; + require(amount > 0, "No funds available"); + + (bool success,) = payable(owner).call{value: amount}(""); + if (!success) revert(); + + emit EmergencyWithdrawn(owner, amount); + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 tokenBalance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.tokenBalance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + tokenBalances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + tokenBalances[index] = userVaults[i].tokenBalance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, tokenBalances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } + + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "New owner is zero address"); + require(newOwner != owner, "New owner is current owner"); + pendingOwner = newOwner; + emit OwnershipTransferStarted(owner, newOwner); + } + + function acceptOwnership() external { + require(msg.sender == pendingOwner, "Not pending owner"); + address previousOwner = owner; + owner = msg.sender; + pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, msg.sender); + } + + /** + * Returns the latest answer. + */ + function getETHUSDPrice() public view returns (int256) { + (, + /* uint80 roundId */ + int256 answer,, + /*uint256 startedAt*/ + , + /*uint256 updatedAt*/ + /*uint80 answeredInRound*/ + ) = priceFeed.latestRoundData(); + uint8 decimals = getDecimals(); + int256 result = answer / int256(10 ** decimals); + return result; + } + + function getDecimals() public view returns (uint8) { + uint8 decimals = priceFeed.decimals(); + return decimals; + } + + function ethToUSD(uint256 amount) public view returns (uint256 returnedValue) { + + } +} diff --git a/sessions/timelock/src/Token.sol b/sessions/timelock/src/Token.sol new file mode 100644 index 00000000..50f223e5 --- /dev/null +++ b/sessions/timelock/src/Token.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.6.0 +pragma solidity ^0.8.27; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; + +contract JasonToken is ERC20, ERC20Burnable, Ownable { + constructor(address recipient, address initialOwner) ERC20("JasonToken", "JKB") Ownable(initialOwner) { + _mint(recipient, 1000 * 10 ** decimals()); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/sessions/timelock/src/Vault.sol b/sessions/timelock/src/Vault.sol new file mode 100644 index 00000000..4500dd36 --- /dev/null +++ b/sessions/timelock/src/Vault.sol @@ -0,0 +1,144 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract TimeLock { + struct Vault { + uint256 balance; + uint256 unlockTime; + bool active; + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + + function deposit(uint256 _unlockTime) external payable returns (uint256) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + // Create new vault + vaults[msg.sender].push(Vault({balance: msg.value, unlockTime: _unlockTime, active: true})); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.active = false; + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + userVaults[i].balance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns (uint256[] memory activeVaults, uint256[] memory balances, uint256[] memory unlockTimes) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/sessions/timelock/src/VaultV2.sol b/sessions/timelock/src/VaultV2.sol new file mode 100644 index 00000000..41a6c350 --- /dev/null +++ b/sessions/timelock/src/VaultV2.sol @@ -0,0 +1,217 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IErc20} from "./interfaces/IERC20.sol"; + +contract TimeLockV2 { + IErc20 public immutable token; + address public owner; + address public pendingOwner; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + struct Vault { + uint256 balance; + uint256 tokenBalance; + uint256 unlockTime; + bool active; + } + + constructor(address _token) { + token = IErc20(_token); + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "New owner is zero address"); + require(newOwner != owner, "New owner is current owner"); + pendingOwner = newOwner; + emit OwnershipTransferStarted(owner, newOwner); + } + + function acceptOwnership() external { + require(msg.sender == pendingOwner, "Not pending owner"); + address previousOwner = owner; + owner = msg.sender; + pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, msg.sender); + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + event EmergencyWithdrawn(address indexed owner, uint256 amount); + + //INTERNAL FUNCTIONS + function _depositRatio(uint256 _totalDeposit) internal pure returns (uint256 _tokenAmount) { + _tokenAmount = _totalDeposit * 10; // Token Ratio + } + + function deposit(uint256 _unlockTime) external payable returns (uint256) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + uint256 tokenBal = _depositRatio(msg.value); + require(token.transfer(msg.sender, tokenBal), "Token transfer failed"); + + // Create new vault + vaults[msg.sender].push( + Vault({balance: msg.value, unlockTime: _unlockTime, tokenBalance: tokenBal, active: true}) + ); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + uint256 tokenAmount = userVault.tokenBalance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.tokenBalance = 0; + userVault.active = false; + + require(token.transferFrom(msg.sender, address(this), tokenAmount), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + uint256 totalTokens = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + uint256 tokenAmount = userVaults[i].tokenBalance; + + userVaults[i].balance = 0; + userVaults[i].tokenBalance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + totalTokens += tokenAmount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + require(token.transfer(msg.sender, totalTokens), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function emergencyWithdraw() external onlyOwner returns (uint256 amount) { + amount = address(this).balance; + require(amount > 0, "No funds available"); + + (bool success,) = payable(owner).call{value: amount}(""); + if (!success) revert(); + + emit EmergencyWithdrawn(owner, amount); + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 tokenBalance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.tokenBalance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + tokenBalances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + tokenBalances[index] = userVaults[i].tokenBalance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, tokenBalances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/sessions/timelock/src/interfaces/IERC20.sol b/sessions/timelock/src/interfaces/IERC20.sol new file mode 100644 index 00000000..92e32621 --- /dev/null +++ b/sessions/timelock/src/interfaces/IERC20.sol @@ -0,0 +1,19 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IErc20 is IERC20 { + function decimals() external view returns (uint8); + + function totalSupply() external view returns (uint256); + + function balanceOf(address _owner) external view returns (uint256 balance); + + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); + + function mint(address to, uint256 amount) external; + + function burn(uint256 amount) external; + + function burnFrom(address account, uint256 amount) external; +} diff --git a/sessions/timelock/test/CounterV3.t.sol b/sessions/timelock/test/CounterV3.t.sol new file mode 100644 index 00000000..e3384801 --- /dev/null +++ b/sessions/timelock/test/CounterV3.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {CounterV3} from "../src/CounterV3.sol"; + +contract CounterV3Test is Test { + CounterV3 public counterv3; + address public addr1; + address public addr2; + + function setUp() public { + counterv3 = new CounterV3(); + addr1 = makeAddr("addr1"); + addr2 = makeAddr("addr2"); + } + + function test_setNumber() public { + counterv3.setNumber(3); + assertEq(counterv3.number(), 3); + } + + function test_increment() public { + counterv3.increment(); + assertEq(counterv3.number(), 1); + } + + function test_revert_increment_withoutPrivilege() public { + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.increment(); + + assertEq(counterv3.number(), 0); + } + + function test_revert_setNumber_withoutPrivilege() public { + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.setNumber(7); + + assertEq(counterv3.number(), 0); + } + + function test_grantPrivilege() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + assertTrue(counterv3.pendingApproval(addr1)); + + counterv3.grantPrivilege(addr1); + + assertTrue(counterv3.approvedCallers(addr1)); + assertFalse(counterv3.pendingApproval(addr1)); + } + + function test_approvedCaller_increment() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + + vm.prank(addr1); + counterv3.increment(); + + assertEq(counterv3.number(), 1); + } + + function test_revert_grantPrivilege_withoutRequest() public { + vm.expectRevert("No pending request"); + counterv3.grantPrivilege(addr1); + } + + function test_revokePrivilege() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + counterv3.revokePrivilege(addr1); + + assertFalse(counterv3.approvedCallers(addr1)); + } + + function test_revert_revokedCaller_setNumber() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + counterv3.revokePrivilege(addr1); + + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.setNumber(7); + } + + function test_revert_grantPrivilege_notOwner() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + vm.prank(addr2); + vm.expectRevert("Not owner"); + counterv3.grantPrivilege(addr1); + } + + function test_revert_revokePrivilege_notOwner() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + + vm.prank(addr2); + vm.expectRevert("Not owner"); + counterv3.revokePrivilege(addr1); + } + + function test_revert_revokePrivilege_withoutApproval() public { + vm.expectRevert("Caller not approved"); + counterv3.revokePrivilege(addr1); + } + + function test_revert_requestPrivilege_owner() public { + vm.expectRevert("Owner does not need consent"); + counterv3.requestPrivilege(); + } +} diff --git a/sessions/timelock/test/TimelockV3.t.sol b/sessions/timelock/test/TimelockV3.t.sol new file mode 100644 index 00000000..23ac0dfa --- /dev/null +++ b/sessions/timelock/test/TimelockV3.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {TimeLockV3} from "../src/TimelockV3.sol"; +import {console} from "forge-std/console.sol"; +import {IErc20} from "../src/interfaces/IERC20.sol"; + +contract TimelockV3Test is Test { + IErc20 public token; + TimeLockV3 public timelockV3; + address public owner = 0xCe4F29b6A3955Fa50b46DFdFAe2F6352F16A77BB; + address public addr1; + address public addr2; + + function setUp() public { + vm.startPrank(owner); + timelockV3 = new TimeLockV3(0x6434193a115151156d038fB5B61747Cc5b07511F); + addr1 = makeAddr("addr1"); + addr2 = makeAddr("addr2"); + token = IErc20(0x6434193a115151156d038fB5B61747Cc5b07511F); + vm.stopPrank(); + } + + function fromWei(uint256 amount) public pure returns (uint256) { + return (amount / 10 ** 9); + } + + function test_deployment() public view { + assertEq(timelockV3.owner(), owner); + int256 price = timelockV3.getETHUSDPrice(); + console.log("ETH price here", price); + + uint8 decimalResult = timelockV3.getDecimals(); + console.log("decimals here", decimalResult); + + uint256 ownerEthBefore = token.balanceOf(owner); + console.log("token decimals here____", token.decimals()); + assertEq(ownerEthBefore, (2000 * 10 ** 6)); + } + + function test_deposit() public { + vm.startPrank(owner); + uint256 unlockTime = block.timestamp + 1 days; + // uint256 ownerEthBal1 = address(owner).balance; + uint256 ownerEthBal1 = owner.balance; + // console.log("owner balance 1___-", fromWei(ownerEthBal1)); + timelockV3.deposit{value: 0.01 ether}(unlockTime); + + // uint256 vaultEthBefore = address(vault).balance; + // uint256 userTokenBefore = token.balanceOf(user); + // uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + // vm.prank(user); + // vault.deposit{value: 1 ether}(unlockTime); + + // assertEq(user.balance, userEthBefore - 1 ether); + // assertEq(address(vault).balance, vaultEthBefore + 1 ether); + // assertEq(token.balanceOf(user), userTokenBefore + 10 ether); + // assertEq(token.balanceOf(address(vault)), vaultTokenBefore - 10 ether); + // assertEq(vault.getVaultCount(user), 1); + + // (uint256 balance, uint256 tokenBalance, uint256 savedUnlockTime, bool active, bool isUnlocked) = + // vault.getVault(user, 0); + + // assertEq(balance, 1 ether); + // assertEq(tokenBalance, 10 ether); + // assertEq(savedUnlockTime, unlockTime); + // assertTrue(active); + // assertFalse(isUnlocked); + } +} diff --git a/sessions/timelock/test/Vault.t.sol b/sessions/timelock/test/Vault.t.sol new file mode 100644 index 00000000..bdf2655b --- /dev/null +++ b/sessions/timelock/test/Vault.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {TimeLock} from "../src/Vault.sol"; + +contract VaultTest is Test { + TimeLock public vault; + address public user; + address public attacker; + + function setUp() public { + vault = new TimeLock(); + user = makeAddr("user"); + attacker = makeAddr("attacker"); + + assertEq(address(vault).balance, 0); + + vm.deal(user, 10 ether); + vm.deal(attacker, 10 ether); + } + + function test_deposit() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + assertEq(user.balance, userBalanceBefore - 1 ether); + assertEq(address(vault).balance, vaultBalanceBefore + 1 ether); + assertEq(vault.getVaultCount(user), 1); + + (uint256 balance, uint256 savedUnlockTime, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 1 ether); + assertEq(savedUnlockTime, unlockTime); + assertTrue(active); + assertFalse(isUnlocked); + } + + function test_revert_deposit_withZeroValue() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vm.expectRevert("Deposit must be greater than zero"); + vault.deposit{value: 0}(unlockTime); + } + + function test_revert_deposit_withNonFutureUnlockTime() public { + vm.prank(user); + vm.expectRevert("Unlock time must be in the future"); + vault.deposit{value: 1 ether}(block.timestamp); + } + + function test_revert_getVault_withInvalidVaultId() public { + vm.expectRevert("Invalid vault ID"); + vault.getVault(user, 0); + } + + function test_getAllVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + TimeLock.Vault[] memory vaults = vault.getAllVaults(user); + + assertEq(vaults.length, 2); + assertEq(vaults[0].balance, 1 ether); + assertEq(vaults[0].unlockTime, firstUnlockTime); + assertTrue(vaults[0].active); + assertEq(vaults[1].balance, 2 ether); + assertEq(vaults[1].unlockTime, secondUnlockTime); + assertTrue(vaults[1].active); + } + + function test_getActiveVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + (uint256[] memory activeVaults, uint256[] memory balances, uint256[] memory unlockTimes) = + vault.getActiveVaults(user); + + assertEq(activeVaults.length, 1); + assertEq(balances.length, 1); + assertEq(unlockTimes.length, 1); + assertEq(activeVaults[0], 1); + assertEq(balances[0], 2 ether); + assertEq(unlockTimes[0], secondUnlockTime); + } + + function test_getTotalBalance() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + assertEq(vault.getTotalBalance(user), 3 ether); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + assertEq(vault.getTotalBalance(user), 2 ether); + } + + function test_getUnlockedBalance() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + assertEq(vault.getUnlockedBalance(user), 0); + + vm.warp(firstUnlockTime); + assertEq(vault.getUnlockedBalance(user), 1 ether); + + vm.warp(secondUnlockTime); + assertEq(vault.getUnlockedBalance(user), 3 ether); + } + + function test_withdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.warp(unlockTime); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + vault.withdraw(0); + + assertEq(user.balance, userBalanceBefore + 1 ether); + assertEq(address(vault).balance, vaultBalanceBefore - 1 ether); + + (uint256 balance,, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 0); + assertFalse(active); + assertTrue(isUnlocked); + } + + function test_revert_withdraw_whenStillLocked() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(user); + vm.expectRevert("Funds are still locked"); + vault.withdraw(0); + } + + function test_revert_withdraw_fromAnotherUsersVault() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("Invalid vault ID"); + vault.withdraw(0); + } + + function test_withdrawAll() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(secondUnlockTime); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + uint256 amount = vault.withdrawAll(); + + assertEq(amount, 3 ether); + assertEq(user.balance, userBalanceBefore + 3 ether); + assertEq(address(vault).balance, vaultBalanceBefore - 3 ether); + assertEq(vault.getTotalBalance(user), 0); + } + + function test_revert_withdrawAll_fromUserWithoutUnlockedFunds() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("No unlocked funds available"); + vault.withdrawAll(); + } +} diff --git a/sessions/timelock/test/VaultV2.t.sol b/sessions/timelock/test/VaultV2.t.sol new file mode 100644 index 00000000..16f36564 --- /dev/null +++ b/sessions/timelock/test/VaultV2.t.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {TimeLockV2} from "../src/VaultV2.sol"; +import {JasonToken} from "../src/Token.sol"; + +contract VaultV2Test is Test { + TimeLockV2 public vault; + JasonToken public token; + address public owner; + address public user; + address public attacker; + address public newOwner; + + uint256 oneDay = 86400; + + function setUp() public { + owner = makeAddr("owner"); + vm.startPrank(owner); + token = new JasonToken(address(this), address(this)); + vault = new TimeLockV2(address(token)); + vm.stopPrank(); + user = makeAddr("user"); + attacker = makeAddr("attacker"); + newOwner = makeAddr("newOwner"); + + token.transfer(address(vault), 500 * 10 ** token.decimals()); + + vm.deal(user, 10 ether); + vm.deal(attacker, 10 ether); + } + + function test_ownerIsSetOnDeployment() public view { + assertEq(vault.owner(), owner); + console.log("Owner: ", vault.owner()); + console.log("This: ", owner); + } + + function test_emergencyWithdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 ownerBalanceBefore = owner.balance; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(owner); + uint256 withdrawnAmount = vault.emergencyWithdraw(); + + assertEq(withdrawnAmount, 1 ether); + assertEq(address(vault).balance, 0); + assertEq(owner.balance, ownerBalanceBefore + 1 ether); + } + + function test_revert_emergencyWithdraw_notOwner() public { + uint256 unlockTime = block.timestamp + oneDay; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert(); + vault.emergencyWithdraw(); + } + + function test_transferOwnership_and_acceptOwnership() public { + vm.prank(owner); + vault.transferOwnership(newOwner); + + assertEq(vault.pendingOwner(), newOwner); + + vm.prank(newOwner); + vault.acceptOwnership(); + + assertEq(vault.owner(), newOwner); + assertEq(vault.pendingOwner(), address(0)); + } + + function test_revert_transferOwnership_notOwner() public { + vm.prank(attacker); + vm.expectRevert("Not owner"); + vault.transferOwnership(newOwner); + } + + function test_revert_acceptOwnership_notPendingOwner() public { + vm.prank(owner); + vault.transferOwnership(newOwner); + + vm.prank(attacker); + vm.expectRevert("Not pending owner"); + vault.acceptOwnership(); + } + + function test_emergencyWithdraw_accessAfterOwnershipTransfer() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(owner); + vault.transferOwnership(newOwner); + + vm.prank(newOwner); + vault.acceptOwnership(); + + vm.prank(owner); + vm.expectRevert("Not owner"); + vault.emergencyWithdraw(); + + uint256 newOwnerBalanceBefore = newOwner.balance; + vm.prank(newOwner); + uint256 withdrawnAmount = vault.emergencyWithdraw(); + + assertEq(withdrawnAmount, 1 ether); + assertEq(address(vault).balance, 0); + assertEq(newOwner.balance, newOwnerBalanceBefore + 1 ether); + } + + function test_deposit() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + uint256 userTokenBefore = token.balanceOf(user); + uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + assertEq(user.balance, userEthBefore - 1 ether); + assertEq(address(vault).balance, vaultEthBefore + 1 ether); + assertEq(token.balanceOf(user), userTokenBefore + 10 ether); + assertEq(token.balanceOf(address(vault)), vaultTokenBefore - 10 ether); + assertEq(vault.getVaultCount(user), 1); + + (uint256 balance, uint256 tokenBalance, uint256 savedUnlockTime, bool active, bool isUnlocked) = + vault.getVault(user, 0); + + assertEq(balance, 1 ether); + assertEq(tokenBalance, 10 ether); + assertEq(savedUnlockTime, unlockTime); + assertTrue(active); + assertFalse(isUnlocked); + } + + function test_revert_deposit_withZeroValue() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vm.expectRevert("Deposit must be greater than zero"); + vault.deposit{value: 0}(unlockTime); + } + + function test_getActiveVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + token.approve(address(vault), 10 ether); + vm.stopPrank(); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) = vault.getActiveVaults(user); + + assertEq(activeVaults.length, 1); + assertEq(balances.length, 1); + assertEq(tokenBalances.length, 1); + assertEq(unlockTimes.length, 1); + assertEq(activeVaults[0], 1); + assertEq(balances[0], 2 ether); + assertEq(tokenBalances[0], 20 ether); + assertEq(unlockTimes[0], secondUnlockTime); + } + + function test_withdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(user); + token.approve(address(vault), 10 ether); + + vm.warp(unlockTime); + + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + uint256 userTokenBefore = token.balanceOf(user); + uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.withdraw(0); + + assertEq(user.balance, userEthBefore + 1 ether); + assertEq(address(vault).balance, vaultEthBefore - 1 ether); + assertEq(token.balanceOf(user), userTokenBefore - 10 ether); + assertEq(token.balanceOf(address(vault)), vaultTokenBefore + 10 ether); + + (uint256 balance, uint256 tokenBalance,, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 0); + assertEq(tokenBalance, 0); + assertFalse(active); + assertTrue(isUnlocked); + } + + function test_revert_withdraw_withoutApproval() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.warp(unlockTime); + + vm.prank(user); + vm.expectRevert(); + vault.withdraw(0); + } + + function test_withdrawAll() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(secondUnlockTime); + + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + + vm.prank(user); + uint256 amount = vault.withdrawAll(); + + assertEq(amount, 3 ether); + assertEq(user.balance, userEthBefore + 3 ether); + assertEq(address(vault).balance, vaultEthBefore - 3 ether); + assertEq(vault.getTotalBalance(user), 0); + } + + function test_revert_withdraw_fromAnotherUsersVault() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("Invalid vault ID"); + vault.withdraw(0); + } +} diff --git a/sessions/upgradable contracts/Eip2535/README.md b/sessions/upgradable contracts/Eip2535/README.md new file mode 100644 index 00000000..c24ec7f8 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/README.md @@ -0,0 +1,60 @@ +[![Mentioned in Awesome Foundry](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/crisgarner/awesome-foundry) + +# Foundry + Hardhat Diamonds + +This is a mimimal template for [Diamonds](https://github.com/ethereum/EIPs/issues/2535) which allows facet selectors to be generated on the go in solidity tests! + +## Installation + +- Clone this repo +- Install dependencies + +```bash +$ yarn && forge update +``` + +### Compile + +### Foundry + +```bash +$ forge build +``` + +### If you encounter Source "forge-std/Test.sol" not found: File not found. + +```bash +forge install foundry-rs/forge-std +``` + +Then run + +```bash +$ forge build +``` + +### Hardhat + +```bash +$ npx hardhat compile +``` + +## Deployment + +### Hardhat + +```bash +$ npx hardhat run scripts/deploy.js +``` + +### Foundry + +```bash +$ forge t +``` + +`Note`: A lot of improvements are still needed so contributions are welcome!! + +Bonus: The [DiamondLoupefacet](contracts/facets/DiamondLoupeFacet.sol) uses an updated [LibDiamond](contracts/libraries//LibDiamond.sol) which utilises solidity custom errors to make debugging easier especially when upgrading diamonds. Take it for a spin!! + +Need some more clarity? message me [on twitter](https://twitter.com/Timidan_x), Or join the [EIP-2535 Diamonds Discord server](https://discord.gg/kQewPw2) diff --git a/sessions/upgradable contracts/Eip2535/contracts/Diamond.sol b/sessions/upgradable contracts/Eip2535/contracts/Diamond.sol new file mode 100644 index 00000000..e885a911 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/Diamond.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +* +* Implementation of a diamond. +/******************************************************************************/ + +import {LibDiamond} from "./libraries/LibDiamond.sol"; +import {IDiamondCut} from "./interfaces/IDiamondCut.sol"; //interface modifying the facet in the diamond + +contract Diamond { + // crontract owner and address containing the facet implementation + constructor(address _contractOwner, address _diamondCutFacet) payable { + LibDiamond.setContractOwner(_contractOwner); + + // Add the diamondCut external function from the diamondCutFacet + // Creates an array cut of type IDiamondCut.FacetCut with one element. + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + // Creates an array functionSelectors of type bytes4 (used to store function selectors). + bytes4[] memory functionSelectors = new bytes4[](1); + + // Adding the DiamondCut Function + // Stores the function selector of diamondCut in functionSelectors[0]. + functionSelectors[0] = IDiamondCut.diamondCut.selector; + // Populates cut[0] + cut[0] = IDiamondCut.FacetCut({ + facetAddress: _diamondCutFacet, action: IDiamondCut.FacetCutAction.Add, functionSelectors: functionSelectors + }); + + // Executing the Diamond Cut + LibDiamond.diamondCut(cut, address(0), ""); + } + + // Find facet for function that is called and execute the + // functioSelectorExistsn if a facet is found and return any value. + fallback() external payable { + // Retrieves Diamond storage. + // Uses assembly to get storage at DIAMOND_STORAGE_POSITION (defined in LibDiamond). + + LibDiamond.DiamondStorage storage ds; + bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION; + // get diamond storage + assembly { + ds.slot := position + } + // get facet from function selector + // Looks up the facet contract address for the called function (msg.sig). + // If the function does not exist, it reverts with "Diamond: Function does not exist". + address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress; + require(facet != address(0), "Diamond: Function does not exist"); + + // Execute external function from facet using delegatecall and return any value. + // Uses delegatecall to call the function in the facet contract. + // If it fails, it reverts. + // If it succeeds, it returns the function’s result. + + assembly { + // copy function selector and any arguments + calldatacopy(0, 0, calldatasize()) + // execute function call using the facet + let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) + // get any return value + returndatacopy(0, 0, returndatasize()) + // return any return value or error back to the caller + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + //immutable function example + function example() public pure returns (string memory) { + return "THIS IS AN EXAMPLE OF AN IMMUTABLE FUNCTION"; + } + + receive() external payable {} +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/facets/Count.sol b/sessions/upgradable contracts/Eip2535/contracts/facets/Count.sol new file mode 100644 index 00000000..9a34fd2f --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/facets/Count.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LibAppStorage} from "../libraries/LibAppDiamond.sol"; + +contract Count { + // Using the library to define the storage layout + LibAppStorage.layout; + + // Function to increase the count by a specified value + function increaseCount(uint256 value) external { + layout.count += value; + } + + function getCount() external view returns (uint256) { + return layout.count; + } +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/facets/DiamondCutFacet.sol b/sessions/upgradable contracts/Eip2535/contracts/facets/DiamondCutFacet.sol new file mode 100644 index 00000000..35697108 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/facets/DiamondCutFacet.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; +import {LibDiamond} from "../libraries/LibDiamond.sol"; + +contract DiamondCutFacet is IDiamondCut { + /// @notice Add/replace/remove any number of functions and optionally execute + /// a function with delegatecall + /// @param _diamondCut Contains the facet addresses and function selectors + /// @param _init The address of the contract or facet to execute _calldata + /// @param _calldata A function call, including function selector and arguments + /// _calldata is executed with delegatecall on _init + function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) external override { + LibDiamond.enforceIsContractOwner(); + LibDiamond.diamondCut(_diamondCut, _init, _calldata); + } +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/facets/DiamondLoupeFacet.sol b/sessions/upgradable contracts/Eip2535/contracts/facets/DiamondLoupeFacet.sol new file mode 100644 index 00000000..ea1b683b --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/facets/DiamondLoupeFacet.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +import {LibDiamond} from "../libraries/LibDiamond.sol"; +import {IDiamondLoupe} from "../interfaces/IDiamondLoupe.sol"; +import {IERC165} from "../interfaces/IERC165.sol"; + +contract DiamondLoupeFacet is IDiamondLoupe, IERC165 { + // Diamond Loupe Functions + //////////////////////////////////////////////////////////////////// + /// These functions are expected to be called frequently by tools. + // + // struct Facet { + // address facetAddress; + // bytes4[] functionSelectors; + // } + + /// @notice Gets all facets and their selectors. + /// @return facets_ Facet + function facets() external view override returns (Facet[] memory facets_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + uint256 numFacets = ds.facetAddresses.length; + facets_ = new Facet[](numFacets); + for (uint256 i; i < numFacets; i++) { + address facetAddress_ = ds.facetAddresses[i]; + facets_[i].facetAddress = facetAddress_; + facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors; + } + } + + /// @notice Gets all the function selectors provided by a facet. + /// @param _facet The facet address. + /// @return facetFunctionSelectors_ + function facetFunctionSelectors(address _facet) + external + view + override + returns (bytes4[] memory facetFunctionSelectors_) + { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + facetFunctionSelectors_ = ds.facetFunctionSelectors[_facet].functionSelectors; + } + + /// @notice Get all the facet addresses used by a diamond. + /// @return facetAddresses_ + function facetAddresses() external view override returns (address[] memory facetAddresses_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + facetAddresses_ = ds.facetAddresses; + } + + /// @notice Gets the facet that supports the given selector. + /// @dev If facet is not found return address(0). + /// @param _functionSelector The function selector. + /// @return facetAddress_ The facet address. + function facetAddress(bytes4 _functionSelector) external view override returns (address facetAddress_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress; + } + + // This implements ERC-165. + function supportsInterface(bytes4 _interfaceId) external view override returns (bool) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + return ds.supportedInterfaces[_interfaceId]; + } +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/facets/IncreaseCount.sol b/sessions/upgradable contracts/Eip2535/contracts/facets/IncreaseCount.sol new file mode 100644 index 00000000..a5300cfd --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/facets/IncreaseCount.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LibDiamond} from "../libraries/LibDiamond.sol"; + +contract IncreaseCount { + // Using the library to define the storage layout + LibDiamond.DiamondStorage layout; + + // Function to increase the count by a specified value + function increaseCount(uint256 value) external { + layout.count += value; + } + + function getCount() external view returns (uint256) { + return layout.count; + } +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/facets/OwnershipFacet.sol b/sessions/upgradable contracts/Eip2535/contracts/facets/OwnershipFacet.sol new file mode 100644 index 00000000..4427236f --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/facets/OwnershipFacet.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LibDiamond} from "../libraries/LibDiamond.sol"; +import {IERC173} from "../interfaces/IERC173.sol"; + +contract OwnershipFacet is IERC173 { + function transferOwnership(address _newOwner) external override { + LibDiamond.enforceIsContractOwner(); + LibDiamond.setContractOwner(_newOwner); + } + + function owner() external view override returns (address owner_) { + owner_ = LibDiamond.contractOwner(); + } +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/interfaces/IDiamondCut.sol b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IDiamondCut.sol new file mode 100644 index 00000000..beff9a16 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IDiamondCut.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +interface IDiamondCut { + enum FacetCutAction { + Add, + Replace, + Remove + } + // Add=0, Replace=1, Remove=2 + + struct FacetCut { + address facetAddress; + FacetCutAction action; + bytes4[] functionSelectors; + } + + /// @notice Add/replace/remove any number of functions and optionally execute + /// a function with delegatecall + /// @param _diamondCut Contains the facet addresses and function selectors + /// @param _init The address of the contract or facet to execute _calldata + /// @param _calldata A function call, including function selector and arguments + /// _calldata is executed with delegatecall on _init + function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) external; + + event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata); +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/interfaces/IDiamondLoupe.sol b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IDiamondLoupe.sol new file mode 100644 index 00000000..ff29afc8 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IDiamondLoupe.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +// A loupe is a small magnifying glass used to look at diamonds. +// These functions look at diamonds +interface IDiamondLoupe { + /// These functions are expected to be called frequently + /// by tools. + struct Facet { + address facetAddress; + bytes4[] functionSelectors; + } + + /// @notice Gets all facet addresses and their four byte function selectors. + /// @return facets_ Facet + function facets() external view returns (Facet[] memory facets_); + + /// @notice Gets all the function selectors supported by a specific facet. + /// @param _facet The facet address. + /// @return facetFunctionSelectors_ + function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_); + + /// @notice Get all the facet addresses used by a diamond. + /// @return facetAddresses_ + function facetAddresses() external view returns (address[] memory facetAddresses_); + + /// @notice Gets the facet that supports the given selector. + /// @dev If facet is not found return address(0). + /// @param _functionSelector The function selector. + /// @return facetAddress_ The facet address. + function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_); +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/interfaces/IERC165.sol b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IERC165.sol new file mode 100644 index 00000000..04b7bcc9 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IERC165.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceId The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/interfaces/IERC173.sol b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IERC173.sol new file mode 100644 index 00000000..f8970ebc --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/interfaces/IERC173.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ERC-173 Contract Ownership Standard +/// Note: the ERC-165 identifier for this interface is 0x7f5828d0 +/* is ERC165 */ +interface IERC173 { + /// @notice Get the address of the owner + /// @return owner_ The address of the owner. + function owner() external view returns (address owner_); + + /// @notice Set the address of the new owner of the contract + /// @dev Set _newOwner to address(0) to renounce any ownership. + /// @param _newOwner The address of the new owner of the contract + function transferOwnership(address _newOwner) external; +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/libraries/LibAppDiamond.sol b/sessions/upgradable contracts/Eip2535/contracts/libraries/LibAppDiamond.sol new file mode 100644 index 00000000..80d1f6a4 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/libraries/LibAppDiamond.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library LibAppStorage { + struct Layout { + uint256 count; + string email; + + } +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/libraries/LibDiamond.sol b/sessions/upgradable contracts/Eip2535/contracts/libraries/LibDiamond.sol new file mode 100644 index 00000000..843a578d --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/libraries/LibDiamond.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ +import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; + +library LibDiamond { + error InValidFacetCutAction(); + error NotDiamondOwner(); + error NoSelectorsInFacet(); + error NoZeroAddress(); + error SelectorExists(bytes4 selector); + error SameSelectorReplacement(bytes4 selector); + error MustBeZeroAddress(); + error NoCode(); + error NonExistentSelector(bytes4 selector); + error ImmutableFunction(bytes4 selector); + error NonEmptyCalldata(); + error EmptyCalldata(); + error InitCallFailed(); + + // A storage slot constant that uniquely identifies the location of diamond storage in contract storage. + bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage"); + + // Maps function selectors (unique identifiers for functions) to their corresponding facet addresses and positions. + struct FacetAddressAndPosition { + address facetAddress; + uint96 functionSelectorPosition; // position in facetFunctionSelectors.functionSelectors array + } + + // Maps facet addresses to function selectors. + struct FacetFunctionSelectors { + bytes4[] functionSelectors; + uint256 facetAddressPosition; // position of facetAddress in facetAddresses array + } + + // DiamondStorage: Stores the entire Diamond contract's state. + struct DiamondStorage { + // maps function selector to the facet address and + // the position of the selector in the facetFunctionSelectors.selectors array + mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition; + // maps facet addresses to function selectors + mapping(address => FacetFunctionSelectors) facetFunctionSelectors; + // facetAddresses: Stores all facet contract addresses. + address[] facetAddresses; + // Used to query if a contract implements an interface. + // Used to implement ERC-165. + mapping(bytes4 => bool) supportedInterfaces; + // owner of the contract + address contractOwner; + } + + // Retrieves the Diamond storage struct from the storage slot. Uses inline assembly for efficiency. + function diamondStorage() internal pure returns (DiamondStorage storage ds) { + bytes32 position = DIAMOND_STORAGE_POSITION; + assembly { + ds.slot := position + } + } + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + function setContractOwner(address _newOwner) internal { + DiamondStorage storage ds = diamondStorage(); + address previousOwner = ds.contractOwner; + ds.contractOwner = _newOwner; + emit OwnershipTransferred(previousOwner, _newOwner); + } + + function contractOwner() internal view returns (address contractOwner_) { + contractOwner_ = diamondStorage().contractOwner; + } + + function enforceIsContractOwner() internal view { + if (msg.sender != diamondStorage().contractOwner) { + revert NotDiamondOwner(); + } + } + + event DiamondCut(IDiamondCut.FacetCut[] _diamondCut, address _init, bytes _calldata); + + // Internal function version of diamondCut + // Handles adding, replacing, and removing functions dynamically. + function diamondCut(IDiamondCut.FacetCut[] memory _diamondCut, address _init, bytes memory _calldata) internal { + for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) { + IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action; + if (action == IDiamondCut.FacetCutAction.Add) { + addFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else if (action == IDiamondCut.FacetCutAction.Replace) { + replaceFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else if (action == IDiamondCut.FacetCutAction.Remove) { + removeFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else { + revert InValidFacetCutAction(); + } + } + emit DiamondCut(_diamondCut, _init, _calldata); + initializeDiamondCut(_init, _calldata); + } + + function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + if (_functionSelectors.length <= 0) revert NoSelectorsInFacet(); + DiamondStorage storage ds = diamondStorage(); + if (_facetAddress == address(0)) revert NoZeroAddress(); + uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length); + // add new facet address if it does not exist + if (selectorPosition == 0) { + addFacet(ds, _facetAddress); + } + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress; + if (oldFacetAddress != address(0)) revert SelectorExists(selector); + addFunction(ds, selector, selectorPosition, _facetAddress); + selectorPosition++; + } + } + + function replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + if (_functionSelectors.length <= 0) revert NoSelectorsInFacet(); + DiamondStorage storage ds = diamondStorage(); + if (_facetAddress == address(0)) revert NoZeroAddress(); + uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length); + // add new facet address if it does not exist + if (selectorPosition == 0) { + addFacet(ds, _facetAddress); + } + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress; + if (oldFacetAddress == _facetAddress) { + revert SameSelectorReplacement(selector); + } + removeFunction(ds, oldFacetAddress, selector); + addFunction(ds, selector, selectorPosition, _facetAddress); + selectorPosition++; + } + } + + function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + if (_functionSelectors.length <= 0) revert NoSelectorsInFacet(); + DiamondStorage storage ds = diamondStorage(); + // if function does not exist then do nothing and return + if (_facetAddress != address(0)) revert MustBeZeroAddress(); + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress; + removeFunction(ds, oldFacetAddress, selector); + } + } + + function addFacet(DiamondStorage storage ds, address _facetAddress) internal { + enforceHasContractCode(_facetAddress); + ds.facetFunctionSelectors[_facetAddress].facetAddressPosition = ds.facetAddresses.length; + ds.facetAddresses.push(_facetAddress); + } + + function addFunction(DiamondStorage storage ds, bytes4 _selector, uint96 _selectorPosition, address _facetAddress) + internal + { + ds.selectorToFacetAndPosition[_selector].functionSelectorPosition = _selectorPosition; + ds.facetFunctionSelectors[_facetAddress].functionSelectors.push(_selector); + ds.selectorToFacetAndPosition[_selector].facetAddress = _facetAddress; + } + + function removeFunction(DiamondStorage storage ds, address _facetAddress, bytes4 _selector) internal { + if (_facetAddress == address(0)) revert NonExistentSelector(_selector); + // an immutable function is a function defined directly in a diamond + if (_facetAddress == address(this)) revert ImmutableFunction(_selector); + // replace selector with last selector, then delete last selector + uint256 selectorPosition = ds.selectorToFacetAndPosition[_selector].functionSelectorPosition; + uint256 lastSelectorPosition = ds.facetFunctionSelectors[_facetAddress].functionSelectors.length - 1; + // if not the same then replace _selector with lastSelector + if (selectorPosition != lastSelectorPosition) { + bytes4 lastSelector = ds.facetFunctionSelectors[_facetAddress].functionSelectors[lastSelectorPosition]; + ds.facetFunctionSelectors[_facetAddress].functionSelectors[selectorPosition] = lastSelector; + ds.selectorToFacetAndPosition[lastSelector].functionSelectorPosition = uint96(selectorPosition); + } + // delete the last selector + ds.facetFunctionSelectors[_facetAddress].functionSelectors.pop(); + delete ds.selectorToFacetAndPosition[_selector]; + + // if no more selectors for facet address then delete the facet address + if (lastSelectorPosition == 0) { + // replace facet address with last facet address and delete last facet address + uint256 lastFacetAddressPosition = ds.facetAddresses.length - 1; + uint256 facetAddressPosition = ds.facetFunctionSelectors[_facetAddress].facetAddressPosition; + if (facetAddressPosition != lastFacetAddressPosition) { + address lastFacetAddress = ds.facetAddresses[lastFacetAddressPosition]; + ds.facetAddresses[facetAddressPosition] = lastFacetAddress; + ds.facetFunctionSelectors[lastFacetAddress].facetAddressPosition = facetAddressPosition; + } + ds.facetAddresses.pop(); + delete ds.facetFunctionSelectors[_facetAddress].facetAddressPosition; + } + } + + function initializeDiamondCut(address _init, bytes memory _calldata) internal { + if (_init == address(0)) { + if (_calldata.length > 0) revert NonEmptyCalldata(); + } else { + if (_calldata.length == 0) revert EmptyCalldata(); + if (_init != address(this)) { + enforceHasContractCode(_init); + } + (bool success, bytes memory error) = _init.delegatecall(_calldata); + if (!success) { + if (error.length > 0) { + // bubble up the error + revert(string(error)); + } else { + revert InitCallFailed(); + } + } + } + } + + function enforceHasContractCode(address _contract) internal view { + uint256 contractSize; + assembly { + contractSize := extcodesize(_contract) + } + if (contractSize <= 0) revert NoCode(); + } +} diff --git a/sessions/upgradable contracts/Eip2535/contracts/upgradeInitializers/DiamondInit.sol b/sessions/upgradable contracts/Eip2535/contracts/upgradeInitializers/DiamondInit.sol new file mode 100644 index 00000000..7adae3f2 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/contracts/upgradeInitializers/DiamondInit.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +* +* Implementation of a diamond. +/******************************************************************************/ + +import {LibDiamond} from "../libraries/LibDiamond.sol"; +import {IDiamondLoupe} from "../interfaces/IDiamondLoupe.sol"; +import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; +import {IERC173} from "../interfaces/IERC173.sol"; +import {IERC165} from "../interfaces/IERC165.sol"; + +// It is exapected that this contract is customized if you want to deploy your diamond +// with data from a deployment script. Use the init function to initialize state variables +// of your diamond. Add parameters to the init funciton if you need to. + +contract DiamondInit { + // You can add parameters to this function in order to pass in + // data to set your own state variables + function init() external { + // adding ERC165 data + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + ds.supportedInterfaces[type(IERC165).interfaceId] = true; + ds.supportedInterfaces[type(IDiamondCut).interfaceId] = true; + ds.supportedInterfaces[type(IDiamondLoupe).interfaceId] = true; + ds.supportedInterfaces[type(IERC173).interfaceId] = true; + + // add your own state variables + // EIP-2535 specifies that the `diamondCut` function takes two optional + // arguments: address _init and bytes calldata _calldata + // These arguments are used to execute an arbitrary function using delegatecall + // in order to set state variables in the diamond during deployment or an upgrade + // More info here: https://eips.ethereum.org/EIPS/eip-2535#diamond-interface + } +} diff --git a/sessions/upgradable contracts/Eip2535/foundry.toml b/sessions/upgradable contracts/Eip2535/foundry.toml new file mode 100644 index 00000000..cc30df43 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/foundry.toml @@ -0,0 +1,10 @@ +[profile.default] +src = 'contracts' +out = 'out' +libs = [ + 'lib' +] +ffi=true +verbosity=5 + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/sessions/upgradable contracts/Eip2535/hardhat.config.js b/sessions/upgradable contracts/Eip2535/hardhat.config.js new file mode 100644 index 00000000..0aade49c --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/hardhat.config.js @@ -0,0 +1,7 @@ +/** + * @type import('hardhat/config').HardhatUserConfig + */ +require('@nomiclabs/hardhat-ethers'); +module.exports = { + solidity: '0.8.4', +}; diff --git a/sessions/upgradable contracts/Eip2535/package.json b/sessions/upgradable contracts/Eip2535/package.json new file mode 100644 index 00000000..b315e71f --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/package.json @@ -0,0 +1,12 @@ +{ + "name": "hardhat-project", + "dependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.6", + "ethers": "^5.6.9" + }, + "version": "1.0.0", + "main": "index.js", + "repository": "https://github.com/aji70/Foundry-Hardhat-Diamonds-master-Scarfold.git", + "author": "Ajidokwu Sabo ", + "license": "MIT" +} diff --git a/sessions/upgradable contracts/Eip2535/scripts/deploy.js b/sessions/upgradable contracts/Eip2535/scripts/deploy.js new file mode 100644 index 00000000..448b9fc7 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/scripts/deploy.js @@ -0,0 +1,76 @@ +const { getSelectors, FacetCutAction } = require('./libraries/diamond.js'); +const { ethers } = require('hardhat'); +async function deployDiamond() { + const accounts = await ethers.getSigners(); + const contractOwner = accounts[0]; + + // deploy DiamondCutFacet + const DiamondCutFacet = await ethers.getContractFactory('DiamondCutFacet'); + const diamondCutFacet = await DiamondCutFacet.deploy(); + await diamondCutFacet.deployed(); + console.log('DiamondCutFacet deployed:', diamondCutFacet.address); + + // deploy Diamond + const Diamond = await ethers.getContractFactory('Diamond'); + const diamond = await Diamond.deploy( + contractOwner.address, + diamondCutFacet.address + ); + await diamond.deployed(); + console.log('Diamond deployed:', diamond.address); + + // deploy DiamondInit + // DiamondInit provides a function that is called when the diamond is upgraded to initialize state variables + // Read about how the diamondCut function works here: https://eips.ethereum.org/EIPS/eip-2535#addingreplacingremoving-functions + const DiamondInit = await ethers.getContractFactory('DiamondInit'); + const diamondInit = await DiamondInit.deploy(); + await diamondInit.deployed(); + console.log('DiamondInit deployed:', diamondInit.address); + + // deploy facets + console.log(''); + console.log('Deploying facets'); + const FacetNames = ['DiamondLoupeFacet', 'OwnershipFacet']; + const cut = []; + for (const FacetName of FacetNames) { + const Facet = await ethers.getContractFactory(FacetName); + const facet = await Facet.deploy(); + await facet.deployed(); + console.log(`${FacetName} deployed: ${facet.address}`); + cut.push({ + facetAddress: facet.address, + action: FacetCutAction.Add, + functionSelectors: getSelectors(facet), + }); + } + + // upgrade diamond with facets + console.log(''); + console.log('Diamond Cut:', cut); + const diamondCut = await ethers.getContractAt('IDiamondCut', diamond.address); + let tx; + let receipt; + // call to init function + let functionCall = diamondInit.interface.encodeFunctionData('init'); + tx = await diamondCut.diamondCut(cut, diamondInit.address, functionCall); + console.log('Diamond cut tx: ', tx.hash); + receipt = await tx.wait(); + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${tx.hash}`); + } + console.log('Completed diamond cut'); + return diamond.address; +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +if (require.main === module) { + deployDiamond() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} + +exports.deployDiamond = deployDiamond; diff --git a/sessions/upgradable contracts/Eip2535/scripts/genSelectors.js b/sessions/upgradable contracts/Eip2535/scripts/genSelectors.js new file mode 100644 index 00000000..fdcace5c --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/scripts/genSelectors.js @@ -0,0 +1,45 @@ +const ethers = require('ethers'); +const path = require('path/posix'); + +const args = process.argv.slice(2); + +if (args.length != 1) { + console.log(`please supply the correct parameters: + facetName + `); + process.exit(1); +} + +async function printSelectors(contractName, artifactFolderPath = '../out') { + const contractFilePath = path.join( + artifactFolderPath, + `${contractName}.sol`, + `${contractName}.json` + ); + const contractArtifact = require(contractFilePath); + const abi = contractArtifact.abi; + const bytecode = contractArtifact.bytecode; + const target = new ethers.ContractFactory(abi, bytecode); + const signatures = Object.keys(target.interface.functions); + + const selectors = signatures.reduce((acc, val) => { + if (val !== 'init(bytes)') { + acc.push(target.interface.getSighash(val)); + } + return acc; + }, []); + + const coder = ethers.utils.defaultAbiCoder; + const coded = coder.encode(['bytes4[]'], [selectors]); + + process.stdout.write(coded); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +printSelectors(args[0], args[1]) + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/sessions/upgradable contracts/Eip2535/scripts/libraries/diamond.js b/sessions/upgradable contracts/Eip2535/scripts/libraries/diamond.js new file mode 100644 index 00000000..c20d57fd --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/scripts/libraries/diamond.js @@ -0,0 +1,82 @@ +const FacetCutAction = { Add: 0, Replace: 1, Remove: 2 }; + +// get function selectors from ABI +function getSelectors(contract) { + const signatures = Object.keys(contract.interface.functions); + const selectors = signatures.reduce((acc, val) => { + if (val !== 'init(bytes)') { + acc.push(contract.interface.getSighash(val)); + } + return acc; + }, []); + // selectors.contract = contract + // selectors.remove = remove + // selectors.get = get + return selectors; +} + +// get function selector from function signature +function getSelector(func) { + const abiInterface = new ethers.utils.Interface([func]); + return abiInterface.getSighash(ethers.utils.Fragment.from(func)); +} + +// used with getSelectors to remove selectors from an array of selectors +// functionNames argument is an array of function signatures +function remove(functionNames) { + const selectors = this.filter((v) => { + for (const functionName of functionNames) { + if (v === this.contract.interface.getSighash(functionName)) { + return false; + } + } + return true; + }); + // selectors.contract = this.contract + // selectors.remove = this.remove + // selectors.get = this.get + return selectors; +} + +// used with getSelectors to get selectors from an array of selectors +// functionNames argument is an array of function signatures +function get(functionNames) { + const selectors = this.filter((v) => { + for (const functionName of functionNames) { + if (v === this.contract.interface.getSighash(functionName)) { + return true; + } + } + return false; + }); + // selectors.contract = this.contract + // selectors.remove = this.remove + // selectors.get = this.get + return selectors; +} + +// remove selectors using an array of signatures +function removeSelectors(selectors, signatures) { + const iface = new ethers.utils.Interface( + signatures.map((v) => 'function ' + v) + ); + const removeSelectors = signatures.map((v) => iface.getSighash(v)); + selectors = selectors.filter((v) => !removeSelectors.includes(v)); + return selectors; +} + +// find a particular address position in the return value of diamondLoupeFacet.facets() +function findAddressPositionInFacets(facetAddress, facets) { + for (let i = 0; i < facets.length; i++) { + if (facets[i].facetAddress === facetAddress) { + return i; + } + } +} + +exports.getSelectors = getSelectors; +exports.getSelector = getSelector; +exports.FacetCutAction = FacetCutAction; +exports.remove = remove; +exports.removeSelectors = removeSelectors; +exports.findAddressPositionInFacets = findAddressPositionInFacets; diff --git a/sessions/upgradable contracts/Eip2535/test/deployDiamond.t.sol b/sessions/upgradable contracts/Eip2535/test/deployDiamond.t.sol new file mode 100644 index 00000000..c186ba95 --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/test/deployDiamond.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../contracts/interfaces/IDiamondCut.sol"; +import "../contracts/facets/DiamondCutFacet.sol"; +import "../contracts/facets/DiamondLoupeFacet.sol"; +import "../contracts/facets/OwnershipFacet.sol"; +import "../contracts/facets/IncreaseCount.sol"; +import "../contracts/facets/Count.sol"; +import "../contracts/libraries/LibAppDiamond.sol"; +import "forge-std/Test.sol"; +import "../contracts/Diamond.sol"; + +contract DiamondDeployer is Test, IDiamondCut { + //contract types of facets to be deployed + Diamond diamond; + DiamondCutFacet dCutFacet; + DiamondLoupeFacet dLoupe; + OwnershipFacet ownerF; + IncreaseCount incCount; + Count countF; + + function setUp() public { + //deploy facets + dCutFacet = new DiamondCutFacet(); + diamond = new Diamond(address(this), address(dCutFacet)); + dLoupe = new DiamondLoupeFacet(); + ownerF = new OwnershipFacet(); + + //upgrade diamond with facets + + //build cut struct + FacetCut[] memory cut = new FacetCut[](2); + + cut[0] = + (FacetCut({ + facetAddress: address(dLoupe), + action: FacetCutAction.Add, + functionSelectors: generateSelectors("DiamondLoupeFacet") + })); + + cut[1] = + (FacetCut({ + facetAddress: address(ownerF), + action: FacetCutAction.Add, + functionSelectors: generateSelectors("OwnershipFacet") + })); + + //upgrade diamond + IDiamondCut(address(diamond)).diamondCut(cut, address(0x0), ""); + + //call a function + DiamondLoupeFacet(address(diamond)).facetAddresses(); + } + + function testInDiamondLoupeFacetcreaseCount() public { + //deploy IncreaseCount facet + incCount = new IncreaseCount(); + countF = new Count(); + + //build cut struct + FacetCut[] memory cut = new FacetCut[](2); + + cut[0] = + (FacetCut({ + facetAddress: address(incCount), + action: FacetCutAction.Add, + functionSelectors: generateSelectors("IncreaseCount") + })); + + + //upgrade diamond + IDiamondCut(address(diamond)).diamondCut(cut, address(0x0), ""); + + FacetCut[] memory cut1 = new FacetCut[](1); + cut1[0] = + (FacetCut({ + facetAddress: address(countF), + action: FacetCutAction.Replace, + functionSelectors: generateSelectors("IncreaseCount") + })); + + //upgrade diamond + IDiamondCut(address(diamond)).diamondCut(cut1, address(0x0), ""); + + //call increaseCount function + IncreaseCount(address(diamond)).increaseCount(5); + + //check if count was increased + uint256 count = IncreaseCount(address(diamond)).getCount(); + + assertEq(count, 5); + } + + function generateSelectors(string memory _facetName) internal returns (bytes4[] memory selectors) { + string[] memory cmd = new string[](3); + cmd[0] = "node"; + cmd[1] = "scripts/genSelectors.js"; + cmd[2] = _facetName; + bytes memory res = vm.ffi(cmd); + selectors = abi.decode(res, (bytes4[])); + } + + function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) external override {} +} diff --git a/sessions/upgradable contracts/Eip2535/yarn.lock b/sessions/upgradable contracts/Eip2535/yarn.lock new file mode 100644 index 00000000..6dd4ed5e --- /dev/null +++ b/sessions/upgradable contracts/Eip2535/yarn.lock @@ -0,0 +1,586 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/abi@npm:5.7.0" + dependencies: + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/hash": "npm:^5.7.0" + "@ethersproject/keccak256": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + checksum: 10c0/7de51bf52ff03df2526546dacea6e74f15d4c5ef762d931552082b9600dcefd8e333599f02d7906ba89f7b7f48c45ab72cee76f397212b4f17fa9d9ff5615916 + languageName: node + linkType: hard + +"@ethersproject/abstract-provider@npm:5.7.0, @ethersproject/abstract-provider@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/abstract-provider@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/networks": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/transactions": "npm:^5.7.0" + "@ethersproject/web": "npm:^5.7.0" + checksum: 10c0/a5708e2811b90ddc53d9318ce152511a32dd4771aa2fb59dbe9e90468bb75ca6e695d958bf44d13da684dc3b6aab03f63d425ff7591332cb5d7ddaf68dff7224 + languageName: node + linkType: hard + +"@ethersproject/abstract-signer@npm:5.7.0, @ethersproject/abstract-signer@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/abstract-signer@npm:5.7.0" + dependencies: + "@ethersproject/abstract-provider": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + checksum: 10c0/e174966b3be17269a5974a3ae5eef6d15ac62ee8c300ceace26767f218f6bbf3de66f29d9a9c9ca300fa8551aab4c92e28d2cc772f5475fdeaa78d9b5be0e745 + languageName: node + linkType: hard + +"@ethersproject/address@npm:5.7.0, @ethersproject/address@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/address@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/keccak256": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/rlp": "npm:^5.7.0" + checksum: 10c0/db5da50abeaae8f6cf17678323e8d01cad697f9a184b0593c62b71b0faa8d7e5c2ba14da78a998d691773ed6a8eb06701f65757218e0eaaeb134e5c5f3e5a908 + languageName: node + linkType: hard + +"@ethersproject/base64@npm:5.7.0, @ethersproject/base64@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/base64@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + checksum: 10c0/4f748cd82af60ff1866db699fbf2bf057feff774ea0a30d1f03ea26426f53293ea10cc8265cda1695301da61093bedb8cc0d38887f43ed9dad96b78f19d7337e + languageName: node + linkType: hard + +"@ethersproject/basex@npm:5.7.0, @ethersproject/basex@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/basex@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + checksum: 10c0/02304de77477506ad798eb5c68077efd2531624380d770ef4a823e631a288fb680107a0f9dc4a6339b2a0b0f5b06ee77f53429afdad8f950cde0f3e40d30167d + languageName: node + linkType: hard + +"@ethersproject/bignumber@npm:5.7.0, @ethersproject/bignumber@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/bignumber@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + bn.js: "npm:^5.2.1" + checksum: 10c0/14263cdc91a7884b141d9300f018f76f69839c47e95718ef7161b11d2c7563163096fee69724c5fa8ef6f536d3e60f1c605819edbc478383a2b98abcde3d37b2 + languageName: node + linkType: hard + +"@ethersproject/bytes@npm:5.7.0, @ethersproject/bytes@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/bytes@npm:5.7.0" + dependencies: + "@ethersproject/logger": "npm:^5.7.0" + checksum: 10c0/07dd1f0341b3de584ef26c8696674ff2bb032f4e99073856fc9cd7b4c54d1d846cabe149e864be267934658c3ce799e5ea26babe01f83af0e1f06c51e5ac791f + languageName: node + linkType: hard + +"@ethersproject/constants@npm:5.7.0, @ethersproject/constants@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/constants@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.7.0" + checksum: 10c0/6df63ab753e152726b84595250ea722165a5744c046e317df40a6401f38556385a37c84dadf5b11ca651c4fb60f967046125369c57ac84829f6b30e69a096273 + languageName: node + linkType: hard + +"@ethersproject/contracts@npm:5.7.0": + version: 5.7.0 + resolution: "@ethersproject/contracts@npm:5.7.0" + dependencies: + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/abstract-provider": "npm:^5.7.0" + "@ethersproject/abstract-signer": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/transactions": "npm:^5.7.0" + checksum: 10c0/97a10361dddaccfb3e9e20e24d071cfa570050adcb964d3452c5f7c9eaaddb4e145ec9cf928e14417948701b89e81d4907800e799a6083123e4d13a576842f41 + languageName: node + linkType: hard + +"@ethersproject/hash@npm:5.7.0, @ethersproject/hash@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/hash@npm:5.7.0" + dependencies: + "@ethersproject/abstract-signer": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/base64": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/keccak256": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + checksum: 10c0/1a631dae34c4cf340dde21d6940dd1715fc7ae483d576f7b8ef9e8cb1d0e30bd7e8d30d4a7d8dc531c14164602323af2c3d51eb2204af18b2e15167e70c9a5ef + languageName: node + linkType: hard + +"@ethersproject/hdnode@npm:5.7.0, @ethersproject/hdnode@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/hdnode@npm:5.7.0" + dependencies: + "@ethersproject/abstract-signer": "npm:^5.7.0" + "@ethersproject/basex": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/pbkdf2": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/sha2": "npm:^5.7.0" + "@ethersproject/signing-key": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + "@ethersproject/transactions": "npm:^5.7.0" + "@ethersproject/wordlists": "npm:^5.7.0" + checksum: 10c0/36d5c13fe69b1e0a18ea98537bc560d8ba166e012d63faac92522a0b5f405eb67d8848c5aca69e2470f62743aaef2ac36638d9e27fd8c68f51506eb61479d51d + languageName: node + linkType: hard + +"@ethersproject/json-wallets@npm:5.7.0, @ethersproject/json-wallets@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/json-wallets@npm:5.7.0" + dependencies: + "@ethersproject/abstract-signer": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/hdnode": "npm:^5.7.0" + "@ethersproject/keccak256": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/pbkdf2": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/random": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + "@ethersproject/transactions": "npm:^5.7.0" + aes-js: "npm:3.0.0" + scrypt-js: "npm:3.0.1" + checksum: 10c0/f1a84d19ff38d3506f453abc4702107cbc96a43c000efcd273a056371363767a06a8d746f84263b1300266eb0c329fe3b49a9b39a37aadd016433faf9e15a4bb + languageName: node + linkType: hard + +"@ethersproject/keccak256@npm:5.7.0, @ethersproject/keccak256@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/keccak256@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + js-sha3: "npm:0.8.0" + checksum: 10c0/3b1a91706ff11f5ab5496840b9c36cedca27db443186d28b94847149fd16baecdc13f6fc5efb8359506392f2aba559d07e7f9c1e17a63f9d5de9f8053cfcb033 + languageName: node + linkType: hard + +"@ethersproject/logger@npm:5.7.0, @ethersproject/logger@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/logger@npm:5.7.0" + checksum: 10c0/d03d460fb2d4a5e71c627b7986fb9e50e1b59a6f55e8b42a545b8b92398b961e7fd294bd9c3d8f92b35d0f6ff9d15aa14c95eab378f8ea194e943c8ace343501 + languageName: node + linkType: hard + +"@ethersproject/networks@npm:5.7.1, @ethersproject/networks@npm:^5.7.0": + version: 5.7.1 + resolution: "@ethersproject/networks@npm:5.7.1" + dependencies: + "@ethersproject/logger": "npm:^5.7.0" + checksum: 10c0/9efcdce27f150459e85d74af3f72d5c32898823a99f5410e26bf26cca2d21fb14e403377314a93aea248e57fb2964e19cee2c3f7bfc586ceba4c803a8f1b75c0 + languageName: node + linkType: hard + +"@ethersproject/pbkdf2@npm:5.7.0, @ethersproject/pbkdf2@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/pbkdf2@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/sha2": "npm:^5.7.0" + checksum: 10c0/e5a29cf28b4f4ca1def94d37cfb6a9c05c896106ed64881707813de01c1e7ded613f1e95febcccda4de96aae929068831d72b9d06beef1377b5a1a13a0eb3ff5 + languageName: node + linkType: hard + +"@ethersproject/properties@npm:5.7.0, @ethersproject/properties@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/properties@npm:5.7.0" + dependencies: + "@ethersproject/logger": "npm:^5.7.0" + checksum: 10c0/4fe5d36e5550b8e23a305aa236a93e8f04d891d8198eecdc8273914c761b0e198fd6f757877406ee3eb05033ec271132a3e5998c7bd7b9a187964fb4f67b1373 + languageName: node + linkType: hard + +"@ethersproject/providers@npm:5.7.2": + version: 5.7.2 + resolution: "@ethersproject/providers@npm:5.7.2" + dependencies: + "@ethersproject/abstract-provider": "npm:^5.7.0" + "@ethersproject/abstract-signer": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/base64": "npm:^5.7.0" + "@ethersproject/basex": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/hash": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/networks": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/random": "npm:^5.7.0" + "@ethersproject/rlp": "npm:^5.7.0" + "@ethersproject/sha2": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + "@ethersproject/transactions": "npm:^5.7.0" + "@ethersproject/web": "npm:^5.7.0" + bech32: "npm:1.1.4" + ws: "npm:7.4.6" + checksum: 10c0/4c8d19e6b31f769c24042fb2d02e483a4ee60dcbfca9e3291f0a029b24337c47d1ea719a390be856f8fd02997125819e834415e77da4fb2023369712348dae4c + languageName: node + linkType: hard + +"@ethersproject/random@npm:5.7.0, @ethersproject/random@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/random@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + checksum: 10c0/23e572fc55372653c22062f6a153a68c2e2d3200db734cd0d39621fbfd0ca999585bed2d5682e3ac65d87a2893048375682e49d1473d9965631ff56d2808580b + languageName: node + linkType: hard + +"@ethersproject/rlp@npm:5.7.0, @ethersproject/rlp@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/rlp@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + checksum: 10c0/bc863d21dcf7adf6a99ae75c41c4a3fb99698cfdcfc6d5d82021530f3d3551c6305bc7b6f0475ad6de6f69e91802b7e872bee48c0596d98969aefcf121c2a044 + languageName: node + linkType: hard + +"@ethersproject/sha2@npm:5.7.0, @ethersproject/sha2@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/sha2@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + hash.js: "npm:1.1.7" + checksum: 10c0/0e7f9ce6b1640817b921b9c6dd9dab8d5bf5a0ce7634d6a7d129b7366a576c2f90dcf4bcb15a0aa9310dde67028f3a44e4fcc2f26b565abcd2a0f465116ff3b1 + languageName: node + linkType: hard + +"@ethersproject/signing-key@npm:5.7.0, @ethersproject/signing-key@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/signing-key@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + bn.js: "npm:^5.2.1" + elliptic: "npm:6.5.4" + hash.js: "npm:1.1.7" + checksum: 10c0/fe2ca55bcdb6e370d81372191d4e04671234a2da872af20b03c34e6e26b97dc07c1ee67e91b673680fb13344c9d5d7eae52f1fa6117733a3d68652b778843e09 + languageName: node + linkType: hard + +"@ethersproject/solidity@npm:5.7.0": + version: 5.7.0 + resolution: "@ethersproject/solidity@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/keccak256": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/sha2": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + checksum: 10c0/bedf9918911144b0ec352b8aa7fa44abf63f0b131629c625672794ee196ba7d3992b0e0d3741935ca176813da25b9bcbc81aec454652c63113bdc3a1706beac6 + languageName: node + linkType: hard + +"@ethersproject/strings@npm:5.7.0, @ethersproject/strings@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/strings@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + checksum: 10c0/570d87040ccc7d94de9861f76fc2fba6c0b84c5d6104a99a5c60b8a2401df2e4f24bf9c30afa536163b10a564a109a96f02e6290b80e8f0c610426f56ad704d1 + languageName: node + linkType: hard + +"@ethersproject/transactions@npm:5.7.0, @ethersproject/transactions@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/transactions@npm:5.7.0" + dependencies: + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/keccak256": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/rlp": "npm:^5.7.0" + "@ethersproject/signing-key": "npm:^5.7.0" + checksum: 10c0/aa4d51379caab35b9c468ed1692a23ae47ce0de121890b4f7093c982ee57e30bd2df0c743faed0f44936d7e59c55fffd80479f2c28ec6777b8de06bfb638c239 + languageName: node + linkType: hard + +"@ethersproject/units@npm:5.7.0": + version: 5.7.0 + resolution: "@ethersproject/units@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + checksum: 10c0/4da2fdefe2a506cc9f8b408b2c8638ab35b843ec413d52713143f08501a55ff67a808897f9a91874774fb526423a0821090ba294f93e8bf4933a57af9677ac5e + languageName: node + linkType: hard + +"@ethersproject/wallet@npm:5.7.0": + version: 5.7.0 + resolution: "@ethersproject/wallet@npm:5.7.0" + dependencies: + "@ethersproject/abstract-provider": "npm:^5.7.0" + "@ethersproject/abstract-signer": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/hash": "npm:^5.7.0" + "@ethersproject/hdnode": "npm:^5.7.0" + "@ethersproject/json-wallets": "npm:^5.7.0" + "@ethersproject/keccak256": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/random": "npm:^5.7.0" + "@ethersproject/signing-key": "npm:^5.7.0" + "@ethersproject/transactions": "npm:^5.7.0" + "@ethersproject/wordlists": "npm:^5.7.0" + checksum: 10c0/f872b957db46f9de247d39a398538622b6c7a12f93d69bec5f47f9abf0701ef1edc10497924dd1c14a68109284c39a1686fa85586d89b3ee65df49002c40ba4c + languageName: node + linkType: hard + +"@ethersproject/web@npm:5.7.1, @ethersproject/web@npm:^5.7.0": + version: 5.7.1 + resolution: "@ethersproject/web@npm:5.7.1" + dependencies: + "@ethersproject/base64": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + checksum: 10c0/c82d6745c7f133980e8dab203955260e07da22fa544ccafdd0f21c79fae127bd6ef30957319e37b1cc80cddeb04d6bfb60f291bb14a97c9093d81ce50672f453 + languageName: node + linkType: hard + +"@ethersproject/wordlists@npm:5.7.0, @ethersproject/wordlists@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/wordlists@npm:5.7.0" + dependencies: + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/hash": "npm:^5.7.0" + "@ethersproject/logger": "npm:^5.7.0" + "@ethersproject/properties": "npm:^5.7.0" + "@ethersproject/strings": "npm:^5.7.0" + checksum: 10c0/da4f3eca6d691ebf4f578e6b2ec3a76dedba791be558f6cf7e10cd0bfbaeab5a6753164201bb72ced745fb02b6ef7ef34edcb7e6065ce2b624c6556a461c3f70 + languageName: node + linkType: hard + +"@nomiclabs/hardhat-ethers@npm:^2.0.6": + version: 2.2.3 + resolution: "@nomiclabs/hardhat-ethers@npm:2.2.3" + peerDependencies: + ethers: ^5.0.0 + hardhat: ^2.0.0 + checksum: 10c0/cae46d1966108ab02b50fabe7945c8987fa1e9d5d0a7a06f79afc274ff1abc312e8a82375191a341b28571b897c22433d3a2826eb30077ed88d5983d01e381d0 + languageName: node + linkType: hard + +"aes-js@npm:3.0.0": + version: 3.0.0 + resolution: "aes-js@npm:3.0.0" + checksum: 10c0/87dd5b2363534b867db7cef8bc85a90c355460783744877b2db7c8be09740aac5750714f9e00902822f692662bda74cdf40e03fbb5214ffec75c2666666288b8 + languageName: node + linkType: hard + +"bech32@npm:1.1.4": + version: 1.1.4 + resolution: "bech32@npm:1.1.4" + checksum: 10c0/5f62ca47b8df99ace9c0e0d8deb36a919d91bf40066700aaa9920a45f86bb10eb56d537d559416fd8703aa0fb60dddb642e58f049701e7291df678b2033e5ee5 + languageName: node + linkType: hard + +"bn.js@npm:^4.11.9": + version: 4.12.0 + resolution: "bn.js@npm:4.12.0" + checksum: 10c0/9736aaa317421b6b3ed038ff3d4491935a01419ac2d83ddcfebc5717385295fcfcf0c57311d90fe49926d0abbd7a9dbefdd8861e6129939177f7e67ebc645b21 + languageName: node + linkType: hard + +"bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 10c0/bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa + languageName: node + linkType: hard + +"brorand@npm:^1.1.0": + version: 1.1.0 + resolution: "brorand@npm:1.1.0" + checksum: 10c0/6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 + languageName: node + linkType: hard + +"elliptic@npm:6.5.4": + version: 6.5.4 + resolution: "elliptic@npm:6.5.4" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10c0/5f361270292c3b27cf0843e84526d11dec31652f03c2763c6c2b8178548175ff5eba95341dd62baff92b2265d1af076526915d8af6cc9cb7559c44a62f8ca6e2 + languageName: node + linkType: hard + +"ethers@npm:^5.6.9": + version: 5.7.2 + resolution: "ethers@npm:5.7.2" + dependencies: + "@ethersproject/abi": "npm:5.7.0" + "@ethersproject/abstract-provider": "npm:5.7.0" + "@ethersproject/abstract-signer": "npm:5.7.0" + "@ethersproject/address": "npm:5.7.0" + "@ethersproject/base64": "npm:5.7.0" + "@ethersproject/basex": "npm:5.7.0" + "@ethersproject/bignumber": "npm:5.7.0" + "@ethersproject/bytes": "npm:5.7.0" + "@ethersproject/constants": "npm:5.7.0" + "@ethersproject/contracts": "npm:5.7.0" + "@ethersproject/hash": "npm:5.7.0" + "@ethersproject/hdnode": "npm:5.7.0" + "@ethersproject/json-wallets": "npm:5.7.0" + "@ethersproject/keccak256": "npm:5.7.0" + "@ethersproject/logger": "npm:5.7.0" + "@ethersproject/networks": "npm:5.7.1" + "@ethersproject/pbkdf2": "npm:5.7.0" + "@ethersproject/properties": "npm:5.7.0" + "@ethersproject/providers": "npm:5.7.2" + "@ethersproject/random": "npm:5.7.0" + "@ethersproject/rlp": "npm:5.7.0" + "@ethersproject/sha2": "npm:5.7.0" + "@ethersproject/signing-key": "npm:5.7.0" + "@ethersproject/solidity": "npm:5.7.0" + "@ethersproject/strings": "npm:5.7.0" + "@ethersproject/transactions": "npm:5.7.0" + "@ethersproject/units": "npm:5.7.0" + "@ethersproject/wallet": "npm:5.7.0" + "@ethersproject/web": "npm:5.7.1" + "@ethersproject/wordlists": "npm:5.7.0" + checksum: 10c0/90629a4cdb88cde7a7694f5610a83eb00d7fbbaea687446b15631397988f591c554dd68dfa752ddf00aabefd6285e5b298be44187e960f5e4962684e10b39962 + languageName: node + linkType: hard + +"hardhat-project@workspace:.": + version: 0.0.0-use.local + resolution: "hardhat-project@workspace:." + dependencies: + "@nomiclabs/hardhat-ethers": "npm:^2.0.6" + ethers: "npm:^5.6.9" + languageName: unknown + linkType: soft + +"hash.js@npm:1.1.7, hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": + version: 1.1.7 + resolution: "hash.js@npm:1.1.7" + dependencies: + inherits: "npm:^2.0.3" + minimalistic-assert: "npm:^1.0.1" + checksum: 10c0/41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4 + languageName: node + linkType: hard + +"hmac-drbg@npm:^1.0.1": + version: 1.0.1 + resolution: "hmac-drbg@npm:1.0.1" + dependencies: + hash.js: "npm:^1.0.3" + minimalistic-assert: "npm:^1.0.0" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10c0/f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d + languageName: node + linkType: hard + +"inherits@npm:^2.0.3, inherits@npm:^2.0.4": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"js-sha3@npm:0.8.0": + version: 0.8.0 + resolution: "js-sha3@npm:0.8.0" + checksum: 10c0/43a21dc7967c871bd2c46cb1c2ae97441a97169f324e509f382d43330d8f75cf2c96dba7c806ab08a425765a9c847efdd4bffbac2d99c3a4f3de6c0218f40533 + languageName: node + linkType: hard + +"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd + languageName: node + linkType: hard + +"minimalistic-crypto-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-crypto-utils@npm:1.0.1" + checksum: 10c0/790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 + languageName: node + linkType: hard + +"scrypt-js@npm:3.0.1": + version: 3.0.1 + resolution: "scrypt-js@npm:3.0.1" + checksum: 10c0/e2941e1c8b5c84c7f3732b0153fee624f5329fc4e772a06270ee337d4d2df4174b8abb5e6ad53804a29f53890ecbc78f3775a319323568c0313040c0e55f5b10 + languageName: node + linkType: hard + +"ws@npm:7.4.6": + version: 7.4.6 + resolution: "ws@npm:7.4.6" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/4b44b59bbc0549c852fb2f0cdb48e40e122a1b6078aeed3d65557cbeb7d37dda7a4f0027afba2e6a7a695de17701226d02b23bd15c97b0837808c16345c62f8e + languageName: node + linkType: hard diff --git a/sessions/upgradable contracts/Op.Z/.gitignore b/sessions/upgradable contracts/Op.Z/.gitignore new file mode 100644 index 00000000..d0e9c6fb --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/.gitignore @@ -0,0 +1,23 @@ +node_modules/ +lib/ + +# Hardhat / build artifacts +artifacts/ +cache/ +coverage/ +coverage.json +typechain/ +types/ + +# Logs +*.log + +# Env / secrets +.env +.env.* + +# OS / editor +.DS_Store +.vscode/ +.idea/ + diff --git a/sessions/upgradable contracts/Op.Z/README.md b/sessions/upgradable contracts/Op.Z/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/README.md @@ -0,0 +1,57 @@ +# Sample Hardhat 3 Beta Project (`mocha` and `ethers`) + +This project showcases a Hardhat 3 Beta project using `mocha` for tests and the `ethers` library for Ethereum interactions. + +To learn more about the Hardhat 3 Beta, please visit the [Getting Started guide](https://hardhat.org/docs/getting-started#getting-started-with-hardhat-3). To share your feedback, join our [Hardhat 3 Beta](https://hardhat.org/hardhat3-beta-telegram-group) Telegram group or [open an issue](https://github.com/NomicFoundation/hardhat/issues/new) in our GitHub issue tracker. + +## Project Overview + +This example project includes: + +- A simple Hardhat configuration file. +- Foundry-compatible Solidity unit tests. +- TypeScript integration tests using `mocha` and ethers.js +- Examples demonstrating how to connect to different types of networks, including locally simulating OP mainnet. + +## Usage + +### Running Tests + +To run all the tests in the project, execute the following command: + +```shell +npx hardhat test +``` + +You can also selectively run the Solidity or `mocha` tests: + +```shell +npx hardhat test solidity +npx hardhat test mocha +``` + +### Make a deployment to Sepolia + +This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia. + +To run the deployment to a local chain: + +```shell +npx hardhat ignition deploy ignition/modules/Counter.ts +``` + +To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use. + +You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable. + +To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`: + +```shell +npx hardhat keystore set SEPOLIA_PRIVATE_KEY +``` + +After setting the variable, you can run the deployment with the Sepolia network: + +```shell +npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts +``` diff --git a/sessions/upgradable contracts/Op.Z/contracts/MockERC20.sol b/sessions/upgradable contracts/Op.Z/contracts/MockERC20.sol new file mode 100644 index 00000000..a8248e1b --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/contracts/MockERC20.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @dev Simple mint-on-deploy ERC20 used for tests. + */ +contract MockERC20 is ERC20 { + uint8 private immutable _decimals; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address initialHolder, + uint256 initialSupply + ) ERC20(name_, symbol_) { + _decimals = decimals_; + _mint(initialHolder, initialSupply); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } +} + diff --git a/sessions/upgradable contracts/Op.Z/contracts/ReentrancyAttacker.sol b/sessions/upgradable contracts/Op.Z/contracts/ReentrancyAttacker.sol new file mode 100644 index 00000000..a525b746 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/contracts/ReentrancyAttacker.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "./Staking.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract ReentrancyAttacker { + StakingPoolUpgradeable public pool; + + constructor(address poolAddress) { + pool = StakingPoolUpgradeable(poolAddress); + } + + function tryWithdraw(uint256 amount) external { + pool.withdraw(amount); + } + + function attackStake(uint256 amount) external { + address token = address(pool.stakingToken()); + IERC20(token).approve(address(pool), amount); + pool.stake(amount); + } +} + diff --git a/sessions/upgradable contracts/Op.Z/contracts/ReentrantERC20.sol b/sessions/upgradable contracts/Op.Z/contracts/ReentrantERC20.sol new file mode 100644 index 00000000..58b1b669 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/contracts/ReentrantERC20.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "./MockERC20.sol"; + +/** + * @dev ERC20 that triggers a reentrancy attempt during `transferFrom` into the staking pool. + */ +contract ReentrantERC20 is MockERC20 { + address public stakingPool; + address public attacker; + uint256 public reenterAmount; + bool public enabled; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address initialHolder, + uint256 initialSupply + ) MockERC20(name_, symbol_, decimals_, initialHolder, initialSupply) {} + + function setReenterConfig( + address _stakingPool, + address _attacker, + uint256 _reenterAmount, + bool _enabled + ) external { + stakingPool = _stakingPool; + attacker = _attacker; + reenterAmount = _reenterAmount; + enabled = _enabled; + } + + function transferFrom(address from, address to, uint256 amount) + public + override + returns (bool) + { + if (enabled && to == stakingPool && attacker != address(0)) { + bytes memory data = abi.encodeWithSignature("tryWithdraw(uint256)", reenterAmount); + (bool ok, bytes memory returndata) = attacker.call(data); + if (!ok) { + assembly { + revert(add(returndata, 32), mload(returndata)) + } + } + } + return super.transferFrom(from, to, amount); + } +} + diff --git a/sessions/upgradable contracts/Op.Z/contracts/Staking.sol b/sessions/upgradable contracts/Op.Z/contracts/Staking.sol new file mode 100644 index 00000000..b66085d0 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/contracts/Staking.sol @@ -0,0 +1,464 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @title StakingPoolUpgradeable + * @notice Single staking pool with time-based rewards, optional lock + early-withdraw penalty. + * @dev Upgradeable (UUPS). Uses SafeERC20 and nonReentrant guards. + */ +contract StakingPoolUpgradeable is + Initializable, + OwnableUpgradeable, + PausableUpgradeable, + UUPSUpgradeable +{ + using SafeERC20 for IERC20; + + IERC20 public stakingToken; + IERC20 public rewardToken; + + // Rewards configuration (Synthetix-style) + uint256 public rewardRate; // reward tokens per second + uint256 public rewardsDuration; // seconds + uint256 public periodFinish; // timestamp + uint256 public lastUpdateTime; // timestamp + uint256 public rewardPerTokenStored; // 1e18 scaled + + // Lock/penalty configuration + uint256 public lockPeriod; // seconds + uint256 public penaltyRate; // basis points (10000 = 100%) + address public feeCollector; // where penalties are sent; if 0, stays in contract + + // Staking state + uint256 public totalStaked; + mapping(address => uint256) public balanceOf; + mapping(address => uint256) public stakeTime; + + // Rewards accounting per user + mapping(address => uint256) public userRewardPerTokenPaid; + mapping(address => uint256) public rewards; + + // Simple upgrade-safe reentrancy guard (OZ-like) + uint256 private _reentrancyStatus; + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount, uint256 penalty); + event RewardPaid(address indexed user, uint256 reward); + event EmergencyWithdrawn(address indexed user, uint256 amountReceived, uint256 penalty); + event RewardsNotified(uint256 rewardAdded, uint256 duration, uint256 newRewardRate, uint256 periodFinish); + event LockPeriodUpdated(uint256 oldPeriod, uint256 newPeriod); + event PenaltyRateUpdated(uint256 oldRate, uint256 newRate); + event FeeCollectorUpdated(address indexed oldCollector, address indexed newCollector); + event Swept(address indexed token, address indexed to, uint256 amount); + + error ZeroAmount(); + error InvalidToken(); + error InvalidPenaltyRate(); + error InvalidDuration(); + error RewardTooSmall(); + error NoRewards(); + error InsufficientStaked(); + error ActiveRewardPeriod(); + error CannotSweepStakingToken(); + error CannotSweepRewardTokenDuringPeriod(); + + modifier updateReward(address account) { + _updateReward(account); + _; + } + + modifier nonReentrant() { + require(_reentrancyStatus != _ENTERED, "ReentrancyGuard: reentrant call"); + _reentrancyStatus = _ENTERED; + _; + _reentrancyStatus = _NOT_ENTERED; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _stakingToken, + address _rewardToken, + uint256 _lockPeriod, + uint256 _penaltyRate, + address _owner, + address _feeCollector + ) external initializer { + if (_stakingToken == address(0) || _rewardToken == address(0)) revert InvalidToken(); + if (_penaltyRate > 10_000) revert InvalidPenaltyRate(); + if (_owner == address(0)) revert(); + + __Ownable_init(_owner); + __Pausable_init(); + + stakingToken = IERC20(_stakingToken); + rewardToken = IERC20(_rewardToken); + lockPeriod = _lockPeriod; + penaltyRate = _penaltyRate; + feeCollector = _feeCollector; + + lastUpdateTime = block.timestamp; + _reentrancyStatus = _NOT_ENTERED; + } + + // --- Views --- + + function lastTimeRewardApplicable() public view returns (uint256) { + uint256 finish = periodFinish; + if (finish == 0) return block.timestamp; + return block.timestamp < finish ? block.timestamp : finish; + } + + function rewardPerToken() public view returns (uint256) { + uint256 supply = totalStaked; + if (supply == 0) return rewardPerTokenStored; + + uint256 dt = lastTimeRewardApplicable() - lastUpdateTime; + return rewardPerTokenStored + ((dt * rewardRate * 1e18) / supply); + } + + function earned(address account) public view returns (uint256) { + uint256 perToken = rewardPerToken(); + return rewards[account] + ((balanceOf[account] * (perToken - userRewardPerTokenPaid[account])) / 1e18); + } + + function canWithdrawWithoutPenalty(address account) external view returns (bool) { + uint256 t = stakeTime[account]; + if (t == 0) return true; + return block.timestamp >= t + lockPeriod; + } + + function timeUntilUnlock(address account) external view returns (uint256) { + uint256 t = stakeTime[account]; + if (t == 0) return 0; + uint256 unlock = t + lockPeriod; + if (block.timestamp >= unlock) return 0; + return unlock - block.timestamp; + } + + // --- Core actions --- + + function stake(uint256 amount) + external + nonReentrant + whenNotPaused + updateReward(msg.sender) + { + if (amount == 0) revert ZeroAmount(); + uint256 beforeBal = stakingToken.balanceOf(address(this)); + stakingToken.safeTransferFrom(msg.sender, address(this), amount); + uint256 received = stakingToken.balanceOf(address(this)) - beforeBal; + if (received == 0) revert ZeroAmount(); + + totalStaked += received; + balanceOf[msg.sender] += received; + stakeTime[msg.sender] = block.timestamp; + + emit Staked(msg.sender, received); + } + + function withdraw(uint256 amount) + external + nonReentrant + updateReward(msg.sender) + { + if (amount == 0) revert ZeroAmount(); + uint256 bal = balanceOf[msg.sender]; + if (bal < amount) revert InsufficientStaked(); + + uint256 penalty = _penaltyFor(msg.sender, amount); + + balanceOf[msg.sender] = bal - amount; + totalStaked -= amount; + + uint256 toUser = amount - penalty; + if (penalty != 0) _payPenalty(penalty); + + stakingToken.safeTransfer(msg.sender, toUser); + emit Withdrawn(msg.sender, toUser, penalty); + } + + function claimRewards() + external + nonReentrant + updateReward(msg.sender) + { + uint256 reward = rewards[msg.sender]; + if (reward == 0) revert NoRewards(); + + rewards[msg.sender] = 0; + rewardToken.safeTransfer(msg.sender, reward); + emit RewardPaid(msg.sender, reward); + } + + /** + * @notice Withdraw all stake without claiming rewards. + * @dev Penalty applies if within lock period. Rewards are forfeited. + */ + function emergencyWithdraw() external nonReentrant { + uint256 amount = balanceOf[msg.sender]; + if (amount == 0) revert ZeroAmount(); + + uint256 penalty = _penaltyFor(msg.sender, amount); + + // Effects + totalStaked -= amount; + balanceOf[msg.sender] = 0; + rewards[msg.sender] = 0; + userRewardPerTokenPaid[msg.sender] = rewardPerToken(); + + uint256 toUser = amount - penalty; + if (penalty != 0) _payPenalty(penalty); + + stakingToken.safeTransfer(msg.sender, toUser); + emit EmergencyWithdrawn(msg.sender, toUser, penalty); + } + + // --- Admin --- + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function setLockPeriod(uint256 newPeriod) external onlyOwner { + uint256 old = lockPeriod; + lockPeriod = newPeriod; + emit LockPeriodUpdated(old, newPeriod); + } + + function setPenaltyRate(uint256 newRate) external onlyOwner { + if (newRate > 10_000) revert InvalidPenaltyRate(); + uint256 old = penaltyRate; + penaltyRate = newRate; + emit PenaltyRateUpdated(old, newRate); + } + + function setFeeCollector(address newCollector) external onlyOwner { + address old = feeCollector; + feeCollector = newCollector; + emit FeeCollectorUpdated(old, newCollector); + } + + /** + * @notice Add rewards for a new distribution period. + * @dev Owner must approve `rewardToken` to this contract beforehand. + */ + function notifyRewardAmount(uint256 rewardAmount, uint256 duration) + external + onlyOwner + updateReward(address(0)) + { + if (duration == 0) revert InvalidDuration(); + if (rewardAmount == 0) revert RewardTooSmall(); + + // Pull reward tokens in first, so balance checks are reliable. + rewardToken.safeTransferFrom(msg.sender, address(this), rewardAmount); + + uint256 currentTime = block.timestamp; + uint256 newRate; + + if (currentTime >= periodFinish) { + newRate = rewardAmount / duration; + } else { + uint256 remaining = periodFinish - currentTime; + uint256 leftover = remaining * rewardRate; + newRate = (rewardAmount + leftover) / duration; + } + + if (newRate == 0) revert RewardTooSmall(); + + rewardRate = newRate; + rewardsDuration = duration; + lastUpdateTime = currentTime; + periodFinish = currentTime + duration; + + emit RewardsNotified(rewardAmount, duration, newRate, periodFinish); + } + + /** + * @notice Sweep tokens accidentally sent to this contract. + * @dev Cannot sweep stakingToken; cannot sweep rewardToken while an active reward period is running. + */ + function sweep(address token, address to, uint256 amount) external onlyOwner { + if (token == address(stakingToken)) revert CannotSweepStakingToken(); + if (token == address(rewardToken) && block.timestamp < periodFinish) { + revert CannotSweepRewardTokenDuringPeriod(); + } + IERC20(token).safeTransfer(to, amount); + emit Swept(token, to, amount); + } + + // --- Internals --- + + function _updateReward(address account) internal { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTime = lastTimeRewardApplicable(); + + if (account != address(0)) { + rewards[account] = earned(account); + userRewardPerTokenPaid[account] = rewardPerTokenStored; + } + } + + function _penaltyFor(address account, uint256 amount) internal view returns (uint256) { + if (penaltyRate == 0 || lockPeriod == 0) return 0; + uint256 t = stakeTime[account]; + if (t == 0) return 0; + if (block.timestamp >= t + lockPeriod) return 0; + return (amount * penaltyRate) / 10_000; + } + + function _payPenalty(uint256 penalty) internal { + address collector = feeCollector; + if (collector != address(0)) { + stakingToken.safeTransfer(collector, penalty); + } + // else: keep inside contract (protocol fees) + } + + function _authorizeUpgrade(address) internal override onlyOwner {} +} + +/** + * @title StakingFactoryUpgradeable + * @notice Creates upgradeable pool proxies and tracks them. + * @dev Upgradeable (UUPS). Pools are deployed as ERC1967 proxies. + */ +contract StakingFactoryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable { + address public poolImplementation; + uint256 public poolCount; + + struct PoolInfo { + address pool; + address stakingToken; + address rewardToken; + uint256 lockPeriod; + uint256 penaltyRate; + address feeCollector; + bool active; + } + + mapping(uint256 => PoolInfo) public pools; + + event PoolImplementationUpdated(address indexed oldImpl, address indexed newImpl); + event PoolCreated( + uint256 indexed poolId, + address indexed pool, + address stakingToken, + address rewardToken, + uint256 lockPeriod, + uint256 penaltyRate + ); + event PoolActivated(uint256 indexed poolId); + event PoolDeactivated(uint256 indexed poolId); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _owner, address _poolImplementation) external initializer { + if (_owner == address(0)) revert(); + __Ownable_init(_owner); + _setPoolImplementation(_poolImplementation); + } + + function setPoolImplementation(address newImpl) external onlyOwner { + _setPoolImplementation(newImpl); + } + + function createPool( + address stakingToken, + address rewardToken, + uint256 lockPeriod, + uint256 penaltyRate, + address feeCollector + ) external onlyOwner returns (uint256 poolId, address poolAddr) { + if (stakingToken == address(0) || rewardToken == address(0)) revert(); + + poolId = poolCount; + + bytes memory initData = abi.encodeWithSelector( + StakingPoolUpgradeable.initialize.selector, + stakingToken, + rewardToken, + lockPeriod, + penaltyRate, + owner(), + feeCollector + ); + poolAddr = address(new OZERC1967Proxy(poolImplementation, initData)); + + pools[poolId] = PoolInfo({ + pool: poolAddr, + stakingToken: stakingToken, + rewardToken: rewardToken, + lockPeriod: lockPeriod, + penaltyRate: penaltyRate, + feeCollector: feeCollector, + active: true + }); + + poolCount = poolId + 1; + + emit PoolCreated(poolId, poolAddr, stakingToken, rewardToken, lockPeriod, penaltyRate); + } + + function deactivatePool(uint256 poolId) external onlyOwner { + pools[poolId].active = false; + emit PoolDeactivated(poolId); + } + + function activatePool(uint256 poolId) external onlyOwner { + pools[poolId].active = true; + emit PoolActivated(poolId); + } + + function getPool(uint256 poolId) external view returns (PoolInfo memory) { + require(poolId < poolCount, "Pool does not exist"); + return pools[poolId]; + } + + function getAllPools() external view returns (PoolInfo[] memory) { + PoolInfo[] memory allPools = new PoolInfo[](poolCount); + for (uint256 i = 0; i < poolCount; i++) { + allPools[i] = pools[i]; + } + return allPools; + } + + function _setPoolImplementation(address newImpl) internal { + if (newImpl == address(0)) revert(); + address old = poolImplementation; + poolImplementation = newImpl; + emit PoolImplementationUpdated(old, newImpl); + } + + function _authorizeUpgrade(address) internal override onlyOwner {} +} + +/** + * @dev Included here to keep this project self-contained: ERC1967 proxy wrapper + * with a stable artifact name usable by tests/factory. + */ +contract OZERC1967Proxy is ERC1967Proxy { + constructor(address implementation, bytes memory data) ERC1967Proxy(implementation, data) {} +} \ No newline at end of file diff --git a/sessions/upgradable contracts/Op.Z/contracts/StakingPoolV2.sol b/sessions/upgradable contracts/Op.Z/contracts/StakingPoolV2.sol new file mode 100644 index 00000000..eba80885 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/contracts/StakingPoolV2.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "./Staking.sol"; + +/** + * @dev Upgrade target used for testing the UUPS upgrade path. + */ +contract StakingPoolV2 is StakingPoolUpgradeable { + function version() external pure returns (uint256) { + return 2; + } +} + diff --git a/sessions/upgradable contracts/Op.Z/hardhat.config.ts b/sessions/upgradable contracts/Op.Z/hardhat.config.ts new file mode 100644 index 00000000..7379ec89 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from '@nomicfoundation/hardhat-toolbox-mocha-ethers'; +import { configVariable, defineConfig } from 'hardhat/config'; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: '0.8.28', + }, + production: { + version: '0.8.28', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: 'edr-simulated', + chainType: 'l1', + }, + hardhatOp: { + type: 'edr-simulated', + chainType: 'op', + }, + sepolia: { + type: 'http', + chainType: 'l1', + url: configVariable('SEPOLIA_RPC_URL'), + accounts: [configVariable('SEPOLIA_PRIVATE_KEY')], + }, + }, +}); diff --git a/sessions/upgradable contracts/Op.Z/ignition/modules/Counter.ts b/sessions/upgradable contracts/Op.Z/ignition/modules/Counter.ts new file mode 100644 index 00000000..16c691ac --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; + +export default buildModule('CounterModule', (m) => { + const counter = m.contract('Counter'); + + m.call(counter, 'incBy', [5n]); + + return { counter }; +}); diff --git a/sessions/upgradable contracts/Op.Z/package.json b/sessions/upgradable contracts/Op.Z/package.json new file mode 100644 index 00000000..bd521d41 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/package.json @@ -0,0 +1,24 @@ +{ + "name": "upgradable contract", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.6", + "@nomicfoundation/hardhat-ignition": "^3.1.0", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.3", + "@types/chai": "^5.2.3", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.15", + "chai": "^6.2.2", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.2.0", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + }, + "dependencies": { + "@openzeppelin/contracts": "^5.6.1", + "@openzeppelin/contracts-upgradeable": "^5.6.1" + } +} diff --git a/sessions/upgradable contracts/Op.Z/scripts/send-op-tx.ts b/sessions/upgradable contracts/Op.Z/scripts/send-op-tx.ts new file mode 100644 index 00000000..681a76e0 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from 'hardhat'; + +const { ethers } = await network.connect({ + network: 'hardhatOp', + chainType: 'op', +}); + +console.log('Sending transaction using the OP chain type'); + +const [sender] = await ethers.getSigners(); + +console.log('Sending 1 wei from', sender.address, 'to itself'); + +console.log('Sending L2 transaction'); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log('Transaction sent successfully'); diff --git a/sessions/upgradable contracts/Op.Z/test/StakingUpgradeable.test.ts b/sessions/upgradable contracts/Op.Z/test/StakingUpgradeable.test.ts new file mode 100644 index 00000000..1c51fc54 --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/test/StakingUpgradeable.test.ts @@ -0,0 +1,398 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; + +const { ethers } = await network.connect(); + +describe('StakingPoolUpgradeable / StakingFactoryUpgradeable (upgradeable + secure)', function () { + const LOCK_PERIOD = 7 * 24 * 60 * 60; // 7 days + const PENALTY_RATE = 1000; // 10% (basis points) + const REWARD_RATE = ethers.parseEther('1'); // 1 reward token per second + const INITIAL_SUPPLY = ethers.parseEther('1000000'); + + async function increaseTime(seconds: number) { + await ethers.provider.send('evm_increaseTime', [seconds]); + await ethers.provider.send('evm_mine', []); + } + + async function deployFixture({ + feeCollectorEnabled = true, + }: { feeCollectorEnabled?: boolean } = {}) { + const signers = await ethers.getSigners(); + const [owner, user1, user2, feeCollector] = signers; + + const MockERC20 = await ethers.getContractFactory('MockERC20'); + const stakingToken = await MockERC20.deploy( + 'Staking Token', + 'STK', + 18, + owner.address, + INITIAL_SUPPLY + ); + const rewardToken = await MockERC20.deploy( + 'Reward Token', + 'RWD', + 18, + owner.address, + INITIAL_SUPPLY + ); + + const userStake = ethers.parseEther('10000'); + await stakingToken.transfer(user1.address, userStake); + await stakingToken.transfer(user2.address, userStake); + + const StakingPoolUpgradeableCF = await ethers.getContractFactory( + 'StakingPoolUpgradeable' + ); + const poolImpl = await StakingPoolUpgradeableCF.deploy(); + + const StakingFactoryUpgradeableCF = await ethers.getContractFactory( + 'StakingFactoryUpgradeable' + ); + const factoryImpl = await StakingFactoryUpgradeableCF.deploy(); + + const ProxyCF = await ethers.getContractFactory('OZERC1967Proxy'); + const factoryInit = factoryImpl.interface.encodeFunctionData('initialize', [ + owner.address, + poolImpl.target, + ]); + const factoryProxy = await ProxyCF.deploy(factoryImpl.target, factoryInit); + const factory = StakingFactoryUpgradeableCF.attach(factoryProxy.target); + + const feeCollectorAddr = feeCollectorEnabled + ? feeCollector.address + : ethers.ZeroAddress; + + const tx = await factory.createPool( + stakingToken.target, + rewardToken.target, + LOCK_PERIOD, + PENALTY_RATE, + feeCollectorAddr + ); + const receipt = await tx.wait(); + const parsedPoolCreated = receipt.logs + .map((l) => { + try { + return factory.interface.parseLog(l); + } catch { + return null; + } + }) + .find((x) => x && x.name === 'PoolCreated') as any; + + expect(parsedPoolCreated, 'PoolCreated event should exist').to.not.equal( + null + ); + const poolId = parsedPoolCreated.args.poolId as bigint; + const poolAddr = parsedPoolCreated.args.pool as string; + + const pool = StakingPoolUpgradeableCF.attach(poolAddr); + + return { + owner, + user1, + user2, + feeCollector, + stakingToken, + rewardToken, + pool, + factory, + poolId, + poolAddr, + }; + } + + describe('Basic staking / withdrawals', function () { + it('rejects stake(0)', async function () { + const { pool, user1 } = await deployFixture(); + await expect(pool.connect(user1).stake(0n)).to.be.revertedWithCustomError( + pool, + 'ZeroAmount' + ); + }); + + it('allows withdrawal after lock period (no penalty)', async function () { + const { pool, stakingToken, user1 } = await deployFixture(); + const stakeAmount = ethers.parseEther('100'); + + await stakingToken.connect(user1).approve(pool.target, stakeAmount); + await pool.connect(user1).stake(stakeAmount); + + const before = await stakingToken.balanceOf(user1.address); + await increaseTime(LOCK_PERIOD + 1); + await pool.connect(user1).withdraw(stakeAmount); + const after = await stakingToken.balanceOf(user1.address); + + expect(after - before).to.equal(stakeAmount); + }); + + it('applies early-withdraw penalty and routes it to feeCollector', async function () { + const { pool, stakingToken, user1, feeCollector } = await deployFixture({ + feeCollectorEnabled: true, + }); + const stakeAmount = ethers.parseEther('100'); + + const expectedPenalty = (stakeAmount * BigInt(PENALTY_RATE)) / 10_000n; + const expectedToUser = stakeAmount - expectedPenalty; + + await stakingToken.connect(user1).approve(pool.target, stakeAmount); + await pool.connect(user1).stake(stakeAmount); + + const userBefore = await stakingToken.balanceOf(user1.address); + const collectorBefore = await stakingToken.balanceOf( + feeCollector.address + ); + + await pool.connect(user1).withdraw(stakeAmount); + + const userAfter = await stakingToken.balanceOf(user1.address); + const collectorAfter = await stakingToken.balanceOf(feeCollector.address); + + expect(userAfter - userBefore).to.equal(expectedToUser); + expect(collectorAfter - collectorBefore).to.equal(expectedPenalty); + }); + + it('factory can deactivate/activate pools (owner-only)', async function () { + const { factory, poolId, owner, user1 } = await deployFixture(); + + await expect( + factory.connect(user1).deactivatePool(poolId) + ).to.be.revertedWithCustomError(factory, 'OwnableUnauthorizedAccount'); + + await factory.connect(owner).deactivatePool(poolId); + expect((await factory.pools(poolId)).active).to.equal(false); + + await factory.connect(owner).activatePool(poolId); + expect((await factory.pools(poolId)).active).to.equal(true); + }); + }); + + describe('Rewards (notifyRewardAmount + claimRewards)', function () { + it('earned() is safe before notifyRewardAmount is called', async function () { + const { pool, stakingToken, user1 } = await deployFixture(); + const stakeAmount = ethers.parseEther('100'); + + await stakingToken.connect(user1).approve(pool.target, stakeAmount); + await pool.connect(user1).stake(stakeAmount); + + expect(await pool.earned(user1.address)).to.equal(0n); + }); + + it('accrues rewards after notifyRewardAmount and allows claiming', async function () { + const { pool, stakingToken, rewardToken, owner, user1 } = + await deployFixture(); + const stakeAmount = ethers.parseEther('100'); + const duration = 1000; + const rewardAmount = REWARD_RATE * BigInt(duration); + + await stakingToken.connect(user1).approve(pool.target, stakeAmount); + await pool.connect(user1).stake(stakeAmount); + + await rewardToken.connect(owner).approve(pool.target, rewardAmount); + await pool.connect(owner).notifyRewardAmount(rewardAmount, duration); + + await increaseTime(123); + + const expected = await pool.earned(user1.address); + // Block timestamp granularity can cause an off-by-one-second drift. + expect( + expected === REWARD_RATE * 123n || expected === REWARD_RATE * 124n + ).to.equal(true); + + const balBefore = await rewardToken.balanceOf(user1.address); + await pool.connect(user1).claimRewards(); + const balAfter = await rewardToken.balanceOf(user1.address); + const paid = balAfter - balBefore; + expect( + paid === REWARD_RATE * 123n || paid === REWARD_RATE * 124n + ).to.equal(true); + + // A second immediate claim may still pick up a small amount due to block timestamp granularity. + const bal2Before = await rewardToken.balanceOf(user1.address); + await pool.connect(user1).claimRewards(); + const bal2After = await rewardToken.balanceOf(user1.address); + const paid2 = bal2After - bal2Before; + expect(paid2 <= REWARD_RATE * 2n).to.equal(true); + }); + + it('reverts notifyRewardAmount for non-owner', async function () { + const { pool } = await deployFixture(); + const signers = await ethers.getSigners(); + const other = signers[4]; + + const duration = 100; + const rewardAmount = REWARD_RATE * BigInt(duration); + + await expect( + pool.connect(other).notifyRewardAmount(rewardAmount, duration) + ).to.be.revertedWithCustomError(pool, 'OwnableUnauthorizedAccount'); + }); + }); + + describe('Pause / emergencyWithdraw', function () { + it('blocks stake/withdraw/claim while paused; emergencyWithdraw still works', async function () { + const { pool, stakingToken, rewardToken, owner, user1, feeCollector } = + await deployFixture({ + feeCollectorEnabled: true, + }); + + const stakeAmount = ethers.parseEther('100'); + await stakingToken.connect(user1).approve(pool.target, stakeAmount); + await pool.connect(user1).stake(stakeAmount); + + await pool.connect(owner).pause(); + + expect(await pool.paused()).to.equal(true); + expect(await pool.owner()).to.equal(owner.address); + + await expect(pool.connect(user1).stake(1n)).to.be.revertedWithCustomError( + pool, + 'EnforcedPause' + ); + // Deposit-only pause: users must still be able to exit while paused. + const stakerBeforeWithdraw = await stakingToken.balanceOf(user1.address); + await pool.connect(user1).withdraw(stakeAmount); + const stakerAfterWithdraw = await stakingToken.balanceOf(user1.address); + expect(stakerAfterWithdraw - stakerBeforeWithdraw).to.equal( + stakeAmount - (stakeAmount * BigInt(PENALTY_RATE)) / 10_000n + ); + + // Re-stake to test claim behavior while paused. + await stakingToken.connect(user1).approve(pool.target, stakeAmount); + await pool.connect(owner).unpause(); + await pool.connect(user1).stake(stakeAmount); + await pool.connect(owner).pause(); + + const duration = 100; + const rewardAmount = REWARD_RATE * BigInt(duration); + await rewardToken.connect(owner).approve(pool.target, rewardAmount); + await pool.connect(owner).notifyRewardAmount(rewardAmount, duration); + await pool.connect(user1).claimRewards(); + + const before = await stakingToken.balanceOf(user1.address); + const expectedPenalty = (stakeAmount * BigInt(PENALTY_RATE)) / 10_000n; + const expectedToUser = stakeAmount - expectedPenalty; + const collectorBefore = await stakingToken.balanceOf( + feeCollector.address + ); + + await pool.connect(user1).emergencyWithdraw(); + + const after = await stakingToken.balanceOf(user1.address); + const collectorAfter = await stakingToken.balanceOf(feeCollector.address); + expect(after - before).to.equal(expectedToUser); + expect(collectorAfter - collectorBefore).to.equal(expectedPenalty); + }); + }); + + describe('UUPS upgrades', function () { + it('allows only owner to upgrade and preserves state', async function () { + const { pool, stakingToken, owner, user1, poolAddr } = + await deployFixture(); + + const StakingPoolV2CF = await ethers.getContractFactory('StakingPoolV2'); + const poolV2Impl = await StakingPoolV2CF.deploy(); + + await expect( + pool.connect(user1).upgradeToAndCall(poolV2Impl.target, '0x') + ).to.be.revertedWithCustomError(pool, 'OwnableUnauthorizedAccount'); + + await pool.connect(owner).upgradeToAndCall(poolV2Impl.target, '0x'); + + const poolV2 = StakingPoolV2CF.attach(poolAddr); + expect(await poolV2.version()).to.equal(2n); + + const stakeAmount = ethers.parseEther('50'); + await stakingToken.connect(user1).approve(pool.target, stakeAmount); + await pool.connect(user1).stake(stakeAmount); + expect(await poolV2.totalStaked()).to.equal(stakeAmount); + }); + }); + + describe('Reentrancy regression', function () { + it('prevents reentrancy during staking token transferFrom', async function () { + const signers = await ethers.getSigners(); + const [owner, user1, , feeCollector] = signers; + + const MockERC20 = await ethers.getContractFactory('MockERC20'); + const ReentrantERC20 = await ethers.getContractFactory('ReentrantERC20'); + const ReentrancyAttackerCF = + await ethers.getContractFactory('ReentrancyAttacker'); + + const reentrantToken = await ReentrantERC20.deploy( + 'Reentrant Staking Token', + 'RSTK', + 18, + owner.address, + INITIAL_SUPPLY + ); + const rewardToken = await MockERC20.deploy( + 'Reward Token', + 'RWD', + 18, + owner.address, + INITIAL_SUPPLY + ); + + const StakingPoolUpgradeableCF = await ethers.getContractFactory( + 'StakingPoolUpgradeable' + ); + const poolImpl = await StakingPoolUpgradeableCF.deploy(); + + const StakingFactoryUpgradeableCF = await ethers.getContractFactory( + 'StakingFactoryUpgradeable' + ); + const factoryImpl = await StakingFactoryUpgradeableCF.deploy(); + + const ProxyCF = await ethers.getContractFactory('OZERC1967Proxy'); + const factoryInit = factoryImpl.interface.encodeFunctionData( + 'initialize', + [owner.address, poolImpl.target] + ); + const factoryProxy = await ProxyCF.deploy( + factoryImpl.target, + factoryInit + ); + const factory = StakingFactoryUpgradeableCF.attach(factoryProxy.target); + + const tx = await factory.createPool( + reentrantToken.target, + rewardToken.target, + LOCK_PERIOD, + PENALTY_RATE, + feeCollector.address + ); + const receipt = await tx.wait(); + const parsedPoolCreated = receipt.logs + .map((l) => { + try { + return factory.interface.parseLog(l); + } catch { + return null; + } + }) + .find((x) => x && x.name === 'PoolCreated') as any; + + expect(parsedPoolCreated, 'PoolCreated event should exist').to.not.equal( + null + ); + const poolAddr = parsedPoolCreated.args.pool as string; + const pool = StakingPoolUpgradeableCF.attach(poolAddr); + + const attacker = await ReentrancyAttackerCF.deploy(poolAddr); + const attackerStake = ethers.parseEther('10'); + + await reentrantToken.transfer(attacker.target, attackerStake); + await reentrantToken.setReenterConfig( + poolAddr, + attacker.target, + attackerStake, + true + ); + + await expect( + attacker.connect(user1).attackStake(attackerStake) + ).to.be.revertedWith('ReentrancyGuard: reentrant call'); + }); + }); +}); diff --git a/sessions/upgradable contracts/Op.Z/tsconfig.json b/sessions/upgradable contracts/Op.Z/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/sessions/upgradable contracts/Op.Z/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} diff --git a/sessions/upgradable contracts/assignments/myname.md b/sessions/upgradable contracts/assignments/myname.md new file mode 100644 index 00000000..4b7498de --- /dev/null +++ b/sessions/upgradable contracts/assignments/myname.md @@ -0,0 +1 @@ +links to aricle, diamond and OZ github links