From 929e7b50a29ad90266ccf6c843f216727d02be8e Mon Sep 17 00:00:00 2001 From: Nihal Rajak Date: Thu, 15 Jan 2026 18:43:51 +0530 Subject: [PATCH 1/2] feat: implement SafeERC20 for secure token transfers - Update Solidity version to ^0.8.20 - Import SafeERC20 from OpenZeppelin contracts - Implement safeTransferFrom in payInvoice function - Implement safeTransferFrom in payInvoicesBatch function - Add allowance validation before ERC20 transfers - Maintain CEI pattern for security --- .gitmodules | 3 +++ contracts/.env.example | 1 - contracts/foundry.lock | 11 +++++++++++ contracts/foundry.toml | 3 ++- contracts/lib/openzeppelin-contracts | 1 + contracts/src/Chainvoice.sol | 28 ++++++++-------------------- contracts/test/Chainvoice.t.sol | 2 +- 7 files changed, 26 insertions(+), 23 deletions(-) delete mode 100644 contracts/.env.example create mode 100644 contracts/foundry.lock create mode 160000 contracts/lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 5c6b89f7..bf4ea1cf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "contracts/lib/chainlink-brownie-contracts"] path = contracts/lib/chainlink-brownie-contracts url = https://github.com/smartcontractkit/chainlink-brownie-contracts +[submodule "contracts/lib/openzeppelin-contracts"] + path = contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/.env.example b/contracts/.env.example deleted file mode 100644 index 8b137891..00000000 --- a/contracts/.env.example +++ /dev/null @@ -1 +0,0 @@ - diff --git a/contracts/foundry.lock b/contracts/foundry.lock new file mode 100644 index 00000000..ec195333 --- /dev/null +++ b/contracts/foundry.lock @@ -0,0 +1,11 @@ +{ + "lib/forge-std": { + "rev": "3b20d60d14b343ee4f908cb8079495c07f5e8981" + }, + "lib\\openzeppelin-contracts": { + "tag": { + "name": "v5.5.0", + "rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565" + } + } +} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 64d81706..6f0d7112 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -2,6 +2,7 @@ src = "src" out = "out" libs = ["lib"] -solc_version = '0.8.13' +solc_version = '0.8.20' + remappings = ['@openzeppelin/=lib/openzeppelin-contracts/'] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options # forge create --rpc-url https://eth-sepolia.g.alchemy.com/v2/nnY0qPUQLYsUvb5BKJM5bh81sI6O0PQG --private-key fba7342ef6879df2c735644c734ea69c140f423d84eb2d53fbdfd53fd5d7c586 src/Token.sol:MyToken --legacy \ No newline at end of file diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts new file mode 160000 index 00000000..fcbae539 --- /dev/null +++ b/contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit fcbae5394ae8ad52d8e580a3477db99814b9d565 diff --git a/contracts/src/Chainvoice.sol b/contracts/src/Chainvoice.sol index 38cb04cd..f6ba0b62 100644 --- a/contracts/src/Chainvoice.sol +++ b/contracts/src/Chainvoice.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.13; +pragma solidity ^0.8.20; + + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -interface IERC20 { - function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); - function balanceOf(address account) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); -} contract Chainvoice { + using SafeERC20 for IERC20; // Errors error MixedTokenBatch(); error InvalidBatchSize(); @@ -70,16 +70,6 @@ contract Chainvoice { // Constants uint256 public constant MAX_BATCH = 50; - // Internal utils - function _isERC20(address token) internal view returns (bool) { - if (token == address(0)) return false; - if (token.code.length == 0) return false; - (bool success, ) = token.staticcall( - abi.encodeWithSignature("balanceOf(address)", address(this)) - ); - return success; - } - // ========== Single-invoice create ========== function createInvoice( address to, @@ -226,12 +216,11 @@ contract Chainvoice { accumulatedFees += fee; - bool transferSuccess = IERC20(invoice.tokenAddress).transferFrom( + IERC20(invoice.tokenAddress).safeTransferFrom( msg.sender, invoice.from, invoice.amountDue ); - require(transferSuccess, "Token transfer failed"); } emit InvoicePaid( @@ -304,8 +293,7 @@ contract Chainvoice { for (uint256 i = 0; i < n; i++) { InvoiceDetails storage inv = invoices[invoiceIds[i]]; - bool ok = erc20.transferFrom(msg.sender, inv.from, inv.amountDue); - require(ok, "Token transfer failed"); + erc20.safeTransferFrom(msg.sender, inv.from, inv.amountDue); emit InvoicePaid(inv.id, inv.from, inv.to, inv.amountDue, token); } } diff --git a/contracts/test/Chainvoice.t.sol b/contracts/test/Chainvoice.t.sol index ecabfd5b..000cce85 100644 --- a/contracts/test/Chainvoice.t.sol +++ b/contracts/test/Chainvoice.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.13; +pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; From ee9abf1e1edb23c8ff78a3243700d0d7de460366 Mon Sep 17 00:00:00 2001 From: Nihal Rajak Date: Wed, 21 Jan 2026 22:07:47 +0530 Subject: [PATCH 2/2] feat: add Docker support for development environment --- .dockerignore | 20 ++++++++++++++++++++ docker-compose.yml | 32 ++++++++++++++++++++++++++++++++ frontend/.dockerignore | 30 ++++++++++++++++++++++++++++++ frontend/Dockerfile | 13 +++++++++++++ frontend/vite.config.js | 12 ++++++++++++ 5 files changed, 107 insertions(+) create mode 100644 .dockerignore create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b6bf9e15 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +node_modules +**/node_modules +npm-debug.log* +frontend/dist +frontend/build +frontend/.env +frontend/.env.local +contracts/out +contracts/cache +contracts/broadcast +contracts/.env +.git +.gitignore +.gitmodules +*.md +docs/ +.vscode +.idea +.DS_Store +Thumbs.db diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3e9655f3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: chainvoice-frontend + ports: + - "5173:5173" + volumes: + - ./frontend:/app + - /app/node_modules + env_file: + - ./frontend/.env + restart: unless-stopped + networks: + - chainvoice-network + + blockchain: + image: ghcr.io/foundry-rs/foundry:latest + container_name: chainvoice-blockchain + command: anvil --host 0.0.0.0 --chain-id 31337 --accounts 10 --balance 10000 + ports: + - "8545:8545" + restart: unless-stopped + networks: + - chainvoice-network + +networks: + chainvoice-network: + driver: bridge diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..23237a5f --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,30 @@ +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +dist +build +.next +out +.env +.env.local +.env.*.local +.git +.gitignore +.gitattributes +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store +Thumbs.db +coverage +.nyc_output +*.test.js +*.spec.js +README.md +*.md +Dockerfile +docker-compose.yml +.dockerignore diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..9ede7b92 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci --legacy-peer-deps + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/frontend/vite.config.js b/frontend/vite.config.js index d7d31e8f..bcd1a943 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -11,5 +11,17 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true, + watch: { + usePolling: true, + }, + hmr: { + host: 'localhost', + port: 5173, + }, + }, })