Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c13ad8b
Feat: Implement the todo contract
Mac-5 Feb 16, 2026
5ab0b38
feat: Initialize the Milestone escrow contract
Mac-5 Feb 18, 2026
c5dbade
feat: Initialize the TimeLock contract
Mac-5 Feb 18, 2026
302755d
feat: Initialize the Escrow contract
Mac-5 Feb 18, 2026
51ef79c
Remove fallback function from Escrow contract
Mac-5 Feb 24, 2026
c8be813
chore: merge PR #140 from Mac-5/submission/assignment-02/Mac-5
sprtd Feb 24, 2026
9197110
chore: Update .gitignore to exclude artifacts and non-code files
Mac-5 Feb 24, 2026
7ec99cd
chor: merge pull request #166 from BlockheaderWeb3-Community/as-w3-d3
sprtd Feb 25, 2026
f34db7b
Add MarketPlace NFT marketplace implementation
Mac-5 Feb 25, 2026
a529767
Change payment transfer to use call method
Mac-5 Feb 25, 2026
b5e8f2e
Fix call syntax for transferring sellerAmount
Mac-5 Feb 25, 2026
8a99712
chore: add test in package.json
sprtd Feb 25, 2026
6892733
chore: merge PR #168 from Mac-5/features/tokens
sprtd Feb 25, 2026
e532bef
chore: merge PR #174 from BlockheaderWeb3-Community/tokens
sprtd Feb 25, 2026
1da58f1
chore: set up for testing
sprtd Feb 25, 2026
7d751f3
chore: merge PR #178 from BlockheaderWeb3-Community/intro-to-testing
sprtd Feb 25, 2026
0a3ce5c
test: validate Timelock deployment
sprtd Feb 25, 2026
06c1a7b
test: revert 0 ETH deposit
sprtd Feb 25, 2026
0beb97a
test: validate reverts
sprtd Feb 25, 2026
5047801
test: revert past unlock time
sprtd Feb 25, 2026
ebcee19
test: validate multiple deposits
sprtd Feb 25, 2026
72c3a67
chore: merge PR #183 from BlockheaderWeb3-Community/intro-to-test-vault
sprtd Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,18 @@ node_modules
# Rust build artifacts
/target
**/target
artifacts/
cache/
typechain-types/

# Logs
*.log

# OS files
.DS_Store
Thumbs.db

# IDE
.idea/
*.swp
*.swo
20 changes: 20 additions & 0 deletions assignments/Class/.gitignore
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions assignments/Class/README.md
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions assignments/Class/contracts/Factory.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}

198 changes: 198 additions & 0 deletions assignments/Class/contracts/MilestoneEscrow.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
44 changes: 44 additions & 0 deletions assignments/Class/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -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"),
},
},
});
Loading