From d9d1a13040717fb8b1fab183135c33a9c71fe6bb Mon Sep 17 00:00:00 2001 From: samuel Date: Mon, 16 Feb 2026 18:00:39 +0100 Subject: [PATCH 1/4] feat: submitting miletone assignment --- assignments/milestone/.gitignore | 20 + assignments/milestone/README.md | 57 +++ assignments/milestone/contracts/Counter.sol | 19 + assignments/milestone/contracts/Counter.t.sol | 32 ++ assignments/milestone/contracts/milestone.sol | 345 ++++++++++++++++++ assignments/milestone/hardhat.config.ts | 38 ++ .../milestone/ignition/modules/Counter.ts | 9 + assignments/milestone/package.json | 20 + assignments/milestone/scripts/send-op-tx.ts | 22 ++ assignments/milestone/test/Counter.ts | 36 ++ assignments/milestone/tsconfig.json | 13 + 11 files changed, 611 insertions(+) create mode 100644 assignments/milestone/.gitignore create mode 100644 assignments/milestone/README.md create mode 100644 assignments/milestone/contracts/Counter.sol create mode 100644 assignments/milestone/contracts/Counter.t.sol create mode 100644 assignments/milestone/contracts/milestone.sol create mode 100644 assignments/milestone/hardhat.config.ts create mode 100644 assignments/milestone/ignition/modules/Counter.ts create mode 100644 assignments/milestone/package.json create mode 100644 assignments/milestone/scripts/send-op-tx.ts create mode 100644 assignments/milestone/test/Counter.ts create mode 100644 assignments/milestone/tsconfig.json diff --git a/assignments/milestone/.gitignore b/assignments/milestone/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/milestone/.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/milestone/README.md b/assignments/milestone/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/milestone/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/assignments/milestone/contracts/Counter.sol b/assignments/milestone/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/milestone/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + 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/assignments/milestone/contracts/Counter.t.sol b/assignments/milestone/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/milestone/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/milestone/contracts/milestone.sol b/assignments/milestone/contracts/milestone.sol new file mode 100644 index 00000000..cc9bada4 --- /dev/null +++ b/assignments/milestone/contracts/milestone.sol @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + + +contract MilestoneEscrow { + + + // STATE VARIABLES + + + address public client; + address public freelancer; + uint256 public totalMilestones; + uint256 public paymentPerMilestone; + uint256 public milestonesCompleted; + uint256 public milestonesPaid; + uint256 public contractCreationTime; + + // Timeout periods + uint256 public constant CLIENT_APPROVAL_TIMEOUT = 14 days; // Client has 14 days to approve + uint256 public constant FREELANCER_CANCEL_TIMEOUT = 30 days; // Freelancer can cancel if client inactive + + // Track milestone statuses + mapping(uint256 => MilestoneStatus) public milestones; + + enum State { + AWAITING_FUNDING, + ACTIVE, + COMPLETE, + CANCELLED + } + + State public currentState; + + struct MilestoneStatus { + bool completed; // Freelancer marked as complete + bool approved; // Client approved + bool paid; // Payment released + uint256 completionTime; // When freelancer marked complete + } + + + // EVENTS + + event ContractCreated(address indexed client, address indexed freelancer, uint256 totalMilestones, uint256 paymentPerMilestone); + event ContractFunded(uint256 totalAmount); + event MilestoneCompleted(uint256 indexed milestoneId, uint256 timestamp); + event MilestoneApproved(uint256 indexed milestoneId); + event MilestonePaymentReleased(uint256 indexed milestoneId, uint256 amount); + event ContractComplete(); + event ContractCancelled(address indexed cancelledBy, uint256 refundAmount); + event TimeoutClaim(uint256 indexed milestoneId, address indexed claimant); + + // ERRORS + + error OnlyClient(); + error OnlyFreelancer(); + error InvalidState(); + error AlreadyFunded(); + error InsufficientFunding(); + error InvalidMilestone(); + error MilestoneNotCompleted(); + error MilestoneAlreadyCompleted(); + error MilestoneAlreadyApproved(); + error MilestoneAlreadyPaid(); + error ApprovalTimeoutNotReached(); + error CancelTimeoutNotReached(); + error TransferFailed(); + error ReentrancyDetected(); + error NoMilestonesCompleted(); + error CannotCancelWithCompletedMilestones(); + + + // MODIFIERS + + + modifier onlyClient() { + if (msg.sender != client) revert OnlyClient(); + _; + } + + modifier onlyFreelancer() { + if (msg.sender != freelancer) revert OnlyFreelancer(); + _; + } + + modifier inState(State _state) { + if (currentState != _state) revert InvalidState(); + _; + } + + // Reentrancy guard + uint256 private locked = 1; + modifier noReentrancy() { + if (locked != 1) revert ReentrancyDetected(); + locked = 2; + _; + locked = 1; + } + + + // CONSTRUCTOR + + + constructor( + address _freelancer, + uint256 _totalMilestones, + uint256 _paymentPerMilestone + ) { + require(_freelancer != address(0), "Invalid freelancer address"); + require(_totalMilestones > 0, "Must have at least 1 milestone"); + require(_paymentPerMilestone > 0, "Payment must be greater than 0"); + + client = msg.sender; + freelancer = _freelancer; + totalMilestones = _totalMilestones; + paymentPerMilestone = _paymentPerMilestone; + currentState = State.AWAITING_FUNDING; + contractCreationTime = block.timestamp; + + emit ContractCreated(client, freelancer, _totalMilestones, _paymentPerMilestone); + } + + + // FUNDING + + // Client funds the contract with full amount upfront + + function fundContract() external payable onlyClient inState(State.AWAITING_FUNDING) { + uint256 requiredAmount = totalMilestones * paymentPerMilestone; + + if (msg.value != requiredAmount) revert InsufficientFunding(); + + currentState = State.ACTIVE; + + emit ContractFunded(msg.value); + } + + // MILESTONE WORKFLOW + + //Freelancer marks a milestone as completed + + function completeMilestone(uint256 _milestoneId) + external + onlyFreelancer + inState(State.ACTIVE) + { + if (_milestoneId >= totalMilestones) revert InvalidMilestone(); + if (milestones[_milestoneId].completed) revert MilestoneAlreadyCompleted(); + + milestones[_milestoneId].completed = true; + milestones[_milestoneId].completionTime = block.timestamp; + milestonesCompleted++; + + emit MilestoneCompleted(_milestoneId, block.timestamp); + } + + // Client approves a completed milestone + + + function approveMilestone(uint256 _milestoneId) + external + onlyClient + inState(State.ACTIVE) + { + if (_milestoneId >= totalMilestones) revert InvalidMilestone(); + if (!milestones[_milestoneId].completed) revert MilestoneNotCompleted(); + if (milestones[_milestoneId].approved) revert MilestoneAlreadyApproved(); + + milestones[_milestoneId].approved = true; + + emit MilestoneApproved(_milestoneId); + + // Automatically release payment upon approval + _releaseMilestonePayment(_milestoneId); + } + + // Internal function to release payment for a milestone + + function _releaseMilestonePayment(uint256 _milestoneId) + internal + noReentrancy + { + if (milestones[_milestoneId].paid) revert MilestoneAlreadyPaid(); + + milestones[_milestoneId].paid = true; + milestonesPaid++; + + (bool success, ) = freelancer.call{value: paymentPerMilestone}(""); + if (!success) revert TransferFailed(); + + emit MilestonePaymentReleased(_milestoneId, paymentPerMilestone); + + // Check if all milestones are paid + if (milestonesPaid == totalMilestones) { + currentState = State.COMPLETE; + emit ContractComplete(); + } + } + + + + // Freelancer can claim payment if client doesn't approve within timeout + // _milestoneId Milestone number (0-indexed) + // Prevents client from holding payment indefinitely after work is delivered + + function claimMilestoneTimeout(uint256 _milestoneId) + external + onlyFreelancer + inState(State.ACTIVE) + { + if (_milestoneId >= totalMilestones) revert InvalidMilestone(); + if (!milestones[_milestoneId].completed) revert MilestoneNotCompleted(); + if (milestones[_milestoneId].paid) revert MilestoneAlreadyPaid(); + + uint256 completionTime = milestones[_milestoneId].completionTime; + if (block.timestamp < completionTime + CLIENT_APPROVAL_TIMEOUT) { + revert ApprovalTimeoutNotReached(); + } + + // Auto-approve and pay + milestones[_milestoneId].approved = true; + + emit TimeoutClaim(_milestoneId, freelancer); + + _releaseMilestonePayment(_milestoneId); + } + + + // Freelancer can cancel if client is inactive (no milestones approved in 30 days) + // Only allowed if no milestones have been completed yet + + function cancelDueToInactivity() + external + onlyFreelancer + inState(State.ACTIVE) + noReentrancy + { + if (milestonesCompleted > 0) revert CannotCancelWithCompletedMilestones(); + + if (block.timestamp < contractCreationTime + FREELANCER_CANCEL_TIMEOUT) { + revert CancelTimeoutNotReached(); + } + + currentState = State.CANCELLED; + + uint256 refundAmount = address(this).balance; + + (bool success, ) = client.call{value: refundAmount}(""); + if (!success) revert TransferFailed(); + + emit ContractCancelled(freelancer, refundAmount); + } + + // Client can cancel before any work is completed + // Only allowed if freelancer hasn't marked any milestones complete + + function cancelByClient() + external + onlyClient + inState(State.ACTIVE) + noReentrancy + { + if (milestonesCompleted > 0) revert CannotCancelWithCompletedMilestones(); + + currentState = State.CANCELLED; + + uint256 refundAmount = address(this).balance; + + (bool success, ) = client.call{value: refundAmount}(""); + if (!success) revert TransferFailed(); + + emit ContractCancelled(client, refundAmount); + } + + + + // Get milestone details + // _milestoneId Milestone number (0-indexed) + + function getMilestoneStatus(uint256 _milestoneId) + external + view + returns ( + bool completed, + bool approved, + bool paid, + uint256 completionTime + ) + { + MilestoneStatus memory milestone = milestones[_milestoneId]; + return ( + milestone.completed, + milestone.approved, + milestone.paid, + milestone.completionTime + ); + } + + // Get contract summary + + function getContractSummary() + external + view + returns ( + address _client, + address _freelancer, + uint256 _totalMilestones, + uint256 _paymentPerMilestone, + uint256 _milestonesCompleted, + uint256 _milestonesPaid, + State _currentState, + uint256 _balance + ) + { + return ( + client, + freelancer, + totalMilestones, + paymentPerMilestone, + milestonesCompleted, + milestonesPaid, + currentState, + address(this).balance + ); + } + + // Calculate remaining balance + + function getRemainingBalance() external view returns (uint256) { + return address(this).balance; + } + + + // Check if milestone can be claimed via timeout + + function canClaimMilestoneTimeout(uint256 _milestoneId) external view returns (bool) { + if (_milestoneId >= totalMilestones) return false; + if (!milestones[_milestoneId].completed) return false; + if (milestones[_milestoneId].paid) return false; + + uint256 completionTime = milestones[_milestoneId].completionTime; + return block.timestamp >= completionTime + CLIENT_APPROVAL_TIMEOUT; + } +} \ No newline at end of file diff --git a/assignments/milestone/hardhat.config.ts b/assignments/milestone/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/milestone/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/assignments/milestone/ignition/modules/Counter.ts b/assignments/milestone/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/milestone/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/assignments/milestone/package.json b/assignments/milestone/package.json new file mode 100644 index 00000000..1ea8f23f --- /dev/null +++ b/assignments/milestone/package.json @@ -0,0 +1,20 @@ +{ + "name": "milestone", + "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" + } +} diff --git a/assignments/milestone/scripts/send-op-tx.ts b/assignments/milestone/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/milestone/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/milestone/test/Counter.ts b/assignments/milestone/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/milestone/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/milestone/tsconfig.json b/assignments/milestone/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/milestone/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" + } +} From 79fbd581ee743fc8ad7403ec5595501767f81e5a Mon Sep 17 00:00:00 2001 From: samuel Date: Mon, 16 Feb 2026 18:09:19 +0100 Subject: [PATCH 2/4] feat: submitting work --- assignments/fre/.gitignore | 20 +++++ assignments/fre/README.md | 57 ++++++++++++ assignments/fre/contracts/Counter.sol | 19 ++++ assignments/fre/contracts/Counter.t.sol | 32 +++++++ assignments/fre/contracts/fre.sol | 97 +++++++++++++++++++++ assignments/fre/hardhat.config.ts | 38 ++++++++ assignments/fre/ignition/modules/Counter.ts | 9 ++ assignments/fre/package.json | 20 +++++ assignments/fre/scripts/send-op-tx.ts | 22 +++++ assignments/fre/test/Counter.ts | 36 ++++++++ assignments/fre/tsconfig.json | 13 +++ 11 files changed, 363 insertions(+) create mode 100644 assignments/fre/.gitignore create mode 100644 assignments/fre/README.md create mode 100644 assignments/fre/contracts/Counter.sol create mode 100644 assignments/fre/contracts/Counter.t.sol create mode 100644 assignments/fre/contracts/fre.sol create mode 100644 assignments/fre/hardhat.config.ts create mode 100644 assignments/fre/ignition/modules/Counter.ts create mode 100644 assignments/fre/package.json create mode 100644 assignments/fre/scripts/send-op-tx.ts create mode 100644 assignments/fre/test/Counter.ts create mode 100644 assignments/fre/tsconfig.json diff --git a/assignments/fre/.gitignore b/assignments/fre/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/fre/.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/fre/README.md b/assignments/fre/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/fre/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/assignments/fre/contracts/Counter.sol b/assignments/fre/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/fre/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + 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/assignments/fre/contracts/Counter.t.sol b/assignments/fre/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/fre/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/fre/contracts/fre.sol b/assignments/fre/contracts/fre.sol new file mode 100644 index 00000000..6bd57a49 --- /dev/null +++ b/assignments/fre/contracts/fre.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract TimelockedVault { + + uint unlockedTime; + uint withdraw; + address owner; + uint depositAmount; + + enum State{ + Depositing, + COMPLETED, + withdrawing + } + + State public currentState; + + bool public locked; + + + + + // error mustDeposit(); + // error notOwner(); + // error vaultLocked(); + // error insufficientBalance(); + // error withdrawFailed(); + // error ReentryAttack(); + // error InvalidState(); + + + + // modifier deposits() { + // if(msg.sender != owner) revert notOwner(); + // _; + // } + + // modifier withdraws(){ + // if(msg.sender != owner) revert notOwner(); + // _; + // } + + // modifier noreentrying { + // if(locked) revert ReentryAttack(); + // locked = true; + // _; + // locked = false; + // } + + // modifier instate(State _state) { + // if(currentState != _state) revert InvalidState(); + // _; + // } + + + + event deposited(uint _depositAmount); + event withdrawned(uint _withdrawAmount, address _owner); + + function deposit (uint256 _depositAmount) external payable{ + + if(msg.value != _depositAmount) revert("failed"); + + + depositAmount = msg.value; + + + emit deposited(_depositAmount); + + + } + + function withdrawfund (uint _amount) external { + + if(msg.sender == owner) revert ("notOwner"); + if(block.timestamp + 600 > unlockedTime) revert ("vaultLocked"); + if(_amount > depositAmount) revert ("insufficientBalance"); + + + + uint withdraww = _amount; + _amount = 0; + + + (bool success,) = owner.call{value: withdraww}(""); + if(!success) revert ("withdrawFailed"); + + + emit withdrawned(withdraw, owner); + + + } + + + +} \ No newline at end of file diff --git a/assignments/fre/hardhat.config.ts b/assignments/fre/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/fre/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/assignments/fre/ignition/modules/Counter.ts b/assignments/fre/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/fre/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/assignments/fre/package.json b/assignments/fre/package.json new file mode 100644 index 00000000..30aa0c8f --- /dev/null +++ b/assignments/fre/package.json @@ -0,0 +1,20 @@ +{ + "name": "fre", + "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" + } +} diff --git a/assignments/fre/scripts/send-op-tx.ts b/assignments/fre/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/fre/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/fre/test/Counter.ts b/assignments/fre/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/fre/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/fre/tsconfig.json b/assignments/fre/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/fre/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" + } +} From 88ae5c9c1587784b6c07622eefe574e59438a3cd Mon Sep 17 00:00:00 2001 From: samuel Date: Mon, 16 Feb 2026 18:18:43 +0100 Subject: [PATCH 3/4] feat: crownfunding --- assignments/crownfunding/.gitignore | 20 +++ assignments/crownfunding/README.md | 57 ++++++++ .../crownfunding/contracts/Counter.sol | 19 +++ .../crownfunding/contracts/Counter.t.sol | 32 +++++ .../crownfunding/contracts/Crowdfunding.sol | 124 ++++++++++++++++++ assignments/crownfunding/hardhat.config.ts | 38 ++++++ .../crownfunding/ignition/modules/Counter.ts | 9 ++ assignments/crownfunding/package.json | 20 +++ .../crownfunding/scripts/send-op-tx.ts | 22 ++++ assignments/crownfunding/test/Counter.ts | 36 +++++ assignments/crownfunding/tsconfig.json | 13 ++ 11 files changed, 390 insertions(+) create mode 100644 assignments/crownfunding/.gitignore create mode 100644 assignments/crownfunding/README.md create mode 100644 assignments/crownfunding/contracts/Counter.sol create mode 100644 assignments/crownfunding/contracts/Counter.t.sol create mode 100644 assignments/crownfunding/contracts/Crowdfunding.sol create mode 100644 assignments/crownfunding/hardhat.config.ts create mode 100644 assignments/crownfunding/ignition/modules/Counter.ts create mode 100644 assignments/crownfunding/package.json create mode 100644 assignments/crownfunding/scripts/send-op-tx.ts create mode 100644 assignments/crownfunding/test/Counter.ts create mode 100644 assignments/crownfunding/tsconfig.json diff --git a/assignments/crownfunding/.gitignore b/assignments/crownfunding/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/crownfunding/.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/crownfunding/README.md b/assignments/crownfunding/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/crownfunding/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/assignments/crownfunding/contracts/Counter.sol b/assignments/crownfunding/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/crownfunding/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + 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/assignments/crownfunding/contracts/Counter.t.sol b/assignments/crownfunding/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/crownfunding/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/crownfunding/contracts/Crowdfunding.sol b/assignments/crownfunding/contracts/Crowdfunding.sol new file mode 100644 index 00000000..34b01682 --- /dev/null +++ b/assignments/crownfunding/contracts/Crowdfunding.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + + +contract crownfunding{ + + + enum State { + AWAITING_PAYMENT, + AWAITING_DELIVERY, + COMPLETE, + DISPUTED + } + + + uint amountcontributed; + address contributors; + address owner; + bool locked; + + + State public currentstate; + + + + event deposited(uint _amountcontributed, address _contributors); + event withdrawned(uint _amountwithdrawn, address _owner); + event sendback(uint _amountcontributed, address _contributors); + + + error mustDeposit(); + error notContributor(); + error insufficientBalance(); + error withdrawFailed(); + error sendbackFailed(); + error notOwner(); + error ReentryAttack(); + error InvalidState(); + error InvalidAdress(); + error buyerAndSellerCannotBeSame(); + + modifier depositedFund { + if(contributors != msg.sender) revert notContributor(); + _; + } + + modifier withdrawFund { + if(msg.sender != owner) revert notOwner(); + _; + } + modifier noreentry { + if(locked) revert ReentryAttack(); + locked = true; + _; + locked = false; + } + + modifier instate(State state){ + if(currentstate != state) revert InvalidState(); + _; + } + + constructor(address _contributors, address _owner) { + if(contributors != address(0) && owner != address(0)) revert InvalidAdress(); + if(contributors != owner) revert buyerAndSellerCannotBeSame(); + + contributors = _contributors; + owner = _owner; + currentstate = State.AWAITING_PAYMENT; + + } + + + + + + function deposit(uint _amountcontributed, address _contributors) external depositedFund instate(State.AWAITING_PAYMENT) payable { + + + if(msg.value != _amountcontributed) revert mustDeposit(); + if(msg.sender != contributors) revert notContributor(); + + + amountcontributed = msg.value; + contributors = _contributors; + + emit deposited(amountcontributed, contributors); + + } + + function withdrawneder(uint _amountwithdrawn) external withdrawFund noreentry instate(State.AWAITING_DELIVERY) { + + if(msg.sender != owner) revert notOwner(); + if(_amountwithdrawn >= amountcontributed) revert insufficientBalance(); + + uint payout = amountcontributed; + amountcontributed = 0; + + (bool success,) = owner.call{value:payout}(""); + if (!success) revert withdrawFailed(); + emit withdrawned(_amountwithdrawn, owner); + + } + + function sendbackfund(uint _amountcontributed) external instate(State.DISPUTED) noreentry{ + + + if(msg.sender != contributors) revert notContributor(); + if(amountcontributed != _amountcontributed) revert insufficientBalance(); + + uint amountcontributedSave = amountcontributed; + amountcontributed = 0; + + + (bool success,) = contributors.call{value: _amountcontributed}(""); + if (!success) revert sendbackFailed(); + + emit sendback(amountcontributedSave, contributors); + + + } + + +} \ No newline at end of file diff --git a/assignments/crownfunding/hardhat.config.ts b/assignments/crownfunding/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/crownfunding/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/assignments/crownfunding/ignition/modules/Counter.ts b/assignments/crownfunding/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/crownfunding/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/assignments/crownfunding/package.json b/assignments/crownfunding/package.json new file mode 100644 index 00000000..28269fd8 --- /dev/null +++ b/assignments/crownfunding/package.json @@ -0,0 +1,20 @@ +{ + "name": "crownfunding", + "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" + } +} diff --git a/assignments/crownfunding/scripts/send-op-tx.ts b/assignments/crownfunding/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/crownfunding/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/crownfunding/test/Counter.ts b/assignments/crownfunding/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/crownfunding/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/crownfunding/tsconfig.json b/assignments/crownfunding/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/crownfunding/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" + } +} From aa1143a455bec5d16e9665eb926f91375dd2ddf5 Mon Sep 17 00:00:00 2001 From: samuel Date: Mon, 16 Feb 2026 18:29:54 +0100 Subject: [PATCH 4/4] feat: Auction --- assignments/auction/.gitignore | 20 ++++++ assignments/auction/README.md | 57 +++++++++++++++ assignments/auction/contracts/Counter.sol | 19 +++++ assignments/auction/contracts/Counter.t.sol | 32 +++++++++ assignments/auction/contracts/auction.sol | 71 +++++++++++++++++++ assignments/auction/hardhat.config.ts | 38 ++++++++++ .../auction/ignition/modules/Counter.ts | 9 +++ assignments/auction/package.json | 20 ++++++ assignments/auction/scripts/send-op-tx.ts | 22 ++++++ assignments/auction/test/Counter.ts | 36 ++++++++++ assignments/auction/tsconfig.json | 13 ++++ 11 files changed, 337 insertions(+) create mode 100644 assignments/auction/.gitignore create mode 100644 assignments/auction/README.md create mode 100644 assignments/auction/contracts/Counter.sol create mode 100644 assignments/auction/contracts/Counter.t.sol create mode 100644 assignments/auction/contracts/auction.sol create mode 100644 assignments/auction/hardhat.config.ts create mode 100644 assignments/auction/ignition/modules/Counter.ts create mode 100644 assignments/auction/package.json create mode 100644 assignments/auction/scripts/send-op-tx.ts create mode 100644 assignments/auction/test/Counter.ts create mode 100644 assignments/auction/tsconfig.json diff --git a/assignments/auction/.gitignore b/assignments/auction/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/auction/.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/auction/README.md b/assignments/auction/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/auction/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/assignments/auction/contracts/Counter.sol b/assignments/auction/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/auction/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + 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/assignments/auction/contracts/Counter.t.sol b/assignments/auction/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/auction/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/auction/contracts/auction.sol b/assignments/auction/contracts/auction.sol new file mode 100644 index 00000000..4a7fa7f0 --- /dev/null +++ b/assignments/auction/contracts/auction.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + + +contract auction { + + + address user; + address owner; + uint startingPriceEth; + uint auctionDuration; + + + + event bidPricePlaced(address user, uint startingPriceEth); + event bidPriceEnded(address user, uint endingPriceEth); + + + + error notUser(); + error InvalidBidPrice(); + error withdrawFailed(); + error InvalidAmount(); + + + + + + function bid(address _user, uint _startingPriceEth) external payable { + if(msg.sender != _user) revert notUser(); + if(msg.value != _startingPriceEth) revert InvalidBidPrice(); + + startingPriceEth = _startingPriceEth; + user = _user; + + + emit bidPricePlaced(user, startingPriceEth); + } + + function endBid(uint _endingPriceEth) external { + if(_endingPriceEth < startingPriceEth) revert InvalidBidPrice(); + + uint256 endingprice = _endingPriceEth; + _endingPriceEth = 0; + + (bool success,) = user.call{value: endingprice}(""); + if(!success) revert withdrawFailed(); + + emit bidPriceEnded(user, endingprice); + + } + + function getRefund(uint _amount) external { + if(msg.sender != user) revert notUser(); + if(_amount > startingPriceEth) revert InvalidAmount(); + + uint256 refundAmont = _amount; + _amount = 0; + + startingPriceEth -= refundAmont; + + (bool success,) = user.call{value: refundAmont}(""); + if(!success) revert withdrawFailed(); + + emit bidPriceEnded(user, refundAmont); + + + } + + +} \ No newline at end of file diff --git a/assignments/auction/hardhat.config.ts b/assignments/auction/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/auction/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/assignments/auction/ignition/modules/Counter.ts b/assignments/auction/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/auction/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/assignments/auction/package.json b/assignments/auction/package.json new file mode 100644 index 00000000..2b7993e9 --- /dev/null +++ b/assignments/auction/package.json @@ -0,0 +1,20 @@ +{ + "name": "auction", + "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" + } +} diff --git a/assignments/auction/scripts/send-op-tx.ts b/assignments/auction/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/auction/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/auction/test/Counter.ts b/assignments/auction/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/auction/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/auction/tsconfig.json b/assignments/auction/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/auction/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" + } +}