Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions ASSIGNMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Escrow Contract Assignment

## Overview
This is the Escrow contract assignment (as-w3-d2). The Escrow contract provides functionality for secure transactions.

## Setup
1. Install dependencies: `npm install`
2. Configure hardhat: See `hardhat.config.ts`
3. Run tests: `npm test`

## Contract Details
- **Location**: `Assignment/solidity-assignment7/contracts/escrow/`
- **Language**: Solidity

## Testing
```bash
npm test
```

## Deployment
```bash
npx hardhat run scripts/deploy.ts
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

contract basicEscrow {
address public buyer;
address public seller;
address public owner;

enum EscrowState {
AWAITING_PAYMENT,
AWAITING_DELIVERY,
COMPLETE
}

EscrowState public state;

constructor(address _seller, address _owner) {
buyer = msg.sender;
require(_seller != address(0), 'Seller cannot be zero');
require(_seller != buyer, 'Seller cannot be buyer');
require(_owner != address(0), 'Owner cannot be zero');
require(_owner != buyer, 'Owner cannot be buyer');
seller = _seller;
owner = _owner;
state = EscrowState.AWAITING_PAYMENT;
}

// Buyers Deposit Eth
function deposit() external payable {
require(msg.sender == buyer, 'Only the buyer can deposit Eth');
require(state == EscrowState.AWAITING_PAYMENT, 'Payment already received');
require(msg.value > 0, 'Must send Eth');
state = EscrowState.AWAITING_DELIVERY;
}

// Buyers Confirmation
function confirmDelivery() external {
require(msg.sender == buyer, 'Only the buyer can confirm');
require(state == EscrowState.AWAITING_DELIVERY, 'Not awaiting');
state = EscrowState.COMPLETE;
}

// Owner release funds
function fundsRelease() external {
require(msg.sender == owner, 'Only the owner can release funds');
require(state == EscrowState.COMPLETE, 'Not complete');
require(address(this).balance > 0, 'No funds to release');
payable(seller).transfer(address(this).balance);
}

// Owner refund funds
function fundsRefund() external {
require(msg.sender == owner, 'Only the owner can refund funds');
require(state == EscrowState.COMPLETE, 'Not complete');
require(address(this).balance > 0, 'No funds to refund');
payable(buyer).transfer(address(this).balance);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

contract SimpleEscrow { // One escrow deal: buyer, seller, owner (arbiter).
enum EscrowState {
AWAITING_PAYMENT,
AWAITING_SELLER_CONFIRMATION,
AWAITING_BUYER_CONFIRMATION,
COMPLETE
}

address public buyer;
address public seller;
address public owner;
uint256 public amount;
EscrowState public state;

constructor(address _buyer, address _seller, address _owner) {
require(_buyer != address(0), "Buyer cannot be zero");
require(_seller != address(0), "Seller cannot be zero");
require(_owner != address(0), "Owner cannot be zero");
require(_buyer != _seller, "Buyer cannot be seller");
require(_buyer != _owner, "Buyer cannot be owner");
require(_seller != _owner, "Seller cannot be owner");
buyer = _buyer;
seller = _seller;
owner = _owner;
state = EscrowState.AWAITING_PAYMENT;
}

function deposit() external payable {
require(msg.sender == buyer, "Only buyer can deposit");
require(state == EscrowState.AWAITING_PAYMENT, "Not awaiting payment");
require(msg.value > 0, "Must send ETH");
amount = msg.value;
state = EscrowState.AWAITING_SELLER_CONFIRMATION;
}

function confirmDelivery() external {
require(msg.sender == seller, "Only seller can confirm");
require(state == EscrowState.AWAITING_SELLER_CONFIRMATION, "Not awaiting seller");
state = EscrowState.AWAITING_BUYER_CONFIRMATION;
}

function confirmReceived() external {
require(msg.sender == buyer, "Only buyer can confirm");
require(state == EscrowState.AWAITING_BUYER_CONFIRMATION, "Not awaiting buyer");
state = EscrowState.COMPLETE;
}

function fundsRelease() external {
require(msg.sender == owner, "Only owner can release");
require(state == EscrowState.COMPLETE, "Not complete");
require(amount > 0, "No funds");
uint256 payout = amount;
amount = 0;
payable(seller).transfer(payout);
}

function fundsRefund() external {
require(msg.sender == owner, "Only owner can refund");
require(state == EscrowState.COMPLETE, "Not complete");
require(amount > 0, "No funds");
uint256 payout = amount;
amount = 0;
payable(buyer).transfer(payout);
}

}

contract MultiEscrowFactory { // Deploys many SimpleEscrow contracts and tracks them by id.
uint256 public nextId;
mapping(uint256 => address) public escrowById;
address[] public escrows;

event EscrowCreated(address escrow, address buyer, address seller, address owner);

function createEscrow(address _seller, address _owner) external returns (uint256, address) {
SimpleEscrow escrow = new SimpleEscrow(msg.sender, _seller, _owner);
uint256 id = nextId;
nextId = id + 1;
escrowById[id] = address(escrow);
escrows.push(address(escrow));
emit EscrowCreated(address(escrow), msg.sender, _seller, _owner);
return (id, address(escrow));
}

function escrowsCount() external view returns (uint256) {
return escrows.length;
}

function getEscrows() external view returns (address[] memory) {
return escrows;
}
}
45 changes: 45 additions & 0 deletions Assignment/solidity-assignment7/contracts/escrow/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import dotenv from "dotenv";
dotenv.config({ path: "../../.env" });
import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem";
import { configVariable, defineConfig } from "hardhat/config";

export default defineConfig({
plugins: [hardhatToolboxViemPlugin],
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"),
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

export default buildModule("EscrowFactoryModule", (m) => {
const factory = m.contract("MultiEscrowFactory");

return { factory };
});
17 changes: 17 additions & 0 deletions Assignment/solidity-assignment7/contracts/escrow/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "New Folder",
"version": "1.0.0",
"type": "module",
"devDependencies": {
"@nomicfoundation/hardhat-ignition": "^3.0.7",
"@nomicfoundation/hardhat-toolbox-viem": "^5.0.2",
"@types/node": "^22.19.8",
"forge-std": "github:foundry-rs/forge-std#v1.9.4",
"hardhat": "^3.1.6",
"typescript": "~5.8.0",
"viem": "^2.45.1"
},
"dependencies": {
"dotenv": "^17.2.4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { network } from "hardhat";

describe("MultiEscrowFactory / SimpleEscrow", async function () {
const { viem } = await network.connect();
const publicClient = await viem.getPublicClient();
const [buyer, seller, owner] = await viem.getWalletClients();

async function deployFactory() {
return viem.deployContract("MultiEscrowFactory", [], {
client: { wallet: buyer, public: publicClient },
});
}

it("creates escrow and completes happy path flow", async function () {
const factory = await deployFactory();
const deploymentBlock = await publicClient.getBlockNumber();

await factory.write.createEscrow([
seller.account.address,
owner.account.address,
]);

const events = await publicClient.getContractEvents({
address: factory.address,
abi: factory.abi,
eventName: "EscrowCreated",
fromBlock: deploymentBlock,
strict: true,
});

const escrowAddress = events[0]?.args.escrow;
assert.ok(escrowAddress);

const escrowAsBuyer = await viem.getContractAt(
"SimpleEscrow",
escrowAddress,
{ client: { wallet: buyer, public: publicClient } },
);
const escrowAsSeller = await viem.getContractAt(
"SimpleEscrow",
escrowAddress,
{ client: { wallet: seller, public: publicClient } },
);
const escrowAsOwner = await viem.getContractAt(
"SimpleEscrow",
escrowAddress,
{ client: { wallet: owner, public: publicClient } },
);

await escrowAsBuyer.write.deposit({ value: 2n });
await escrowAsSeller.write.confirmDelivery();
await escrowAsBuyer.write.confirmReceived();
await escrowAsOwner.write.fundsRelease();

const amount = await escrowAsBuyer.read.amount();
assert.equal(amount, 0n);
});

it("rejects deposits from non-buyer", async function () {
const factory = await deployFactory();
const deploymentBlock = await publicClient.getBlockNumber();

await factory.write.createEscrow([
seller.account.address,
owner.account.address,
]);

const events = await publicClient.getContractEvents({
address: factory.address,
abi: factory.abi,
eventName: "EscrowCreated",
fromBlock: deploymentBlock,
strict: true,
});

const escrowAddress = events[0]?.args.escrow;
assert.ok(escrowAddress);

const escrowAsSeller = await viem.getContractAt(
"SimpleEscrow",
escrowAddress,
{ client: { wallet: seller, public: publicClient } },
);

await assert.rejects(async () => {
await escrowAsSeller.write.deposit({ value: 1n });
});
});
});
13 changes: 13 additions & 0 deletions Assignment/solidity-assignment7/contracts/escrow/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
9 changes: 9 additions & 0 deletions template/.github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Summary
What does this PR implement?

## Testing
- [ ] Unit tests added
- [ ] Tests passing locally

## Notes
Any assumptions or trade-offs?
24 changes: 24 additions & 0 deletions template/.github/workflows/.yml
Original file line number Diff line number Diff line change
@@ -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 .
Loading