From 46fc2b6c6eaae187581e3ed79880a75337b7fa1c Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 25 Apr 2026 15:51:16 +0100 Subject: [PATCH 1/5] feat(contracts): add per-user deposit cap enforcement --- contracts/vault/src/lib.rs | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index 94e9f470..2fbad182 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -59,6 +59,8 @@ pub enum DataKey { ShareBalance(Address), ShipmentByStatus(ShipmentStatus), ShipmentStatusOf(u64), + UserDeposit(Address), + PerUserCap, } #[contracttype] @@ -78,6 +80,7 @@ pub enum VaultError { InsufficientShares = 2, InvalidAmount = 3, ContractPaused = 4, + ExceedsUserCap = 5, } #[contractclient(name = "KoreanDebtStrategyClient")] @@ -136,6 +139,26 @@ impl YieldVault { Self::get_state(&env).is_paused } + pub fn set_per_user_cap(env: Env, cap: i128) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + env.storage().instance().set(&DataKey::PerUserCap, &cap); + } + + pub fn per_user_cap(env: Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::PerUserCap) + .unwrap_or(i128::MAX) + } + + pub fn user_deposit(env: Env, user: Address) -> i128 { + env.storage() + .instance() + .get(&DataKey::UserDeposit(user)) + .unwrap_or(0) + } + fn get_state(env: &Env) -> VaultState { env.storage() .instance() @@ -508,8 +531,23 @@ impl YieldVault { return Err(VaultError::InvalidAmount); } + let deposit_key = DataKey::UserDeposit(user.clone()); + let current_deposit: i128 = env.storage().instance().get(&deposit_key).unwrap_or(0); + let new_deposit = current_deposit + amount; + + let cap: i128 = env + .storage() + .instance() + .get(&DataKey::PerUserCap) + .unwrap_or(i128::MAX); + if new_deposit > cap { + return Err(VaultError::ExceedsUserCap); + } + token_client.transfer(&user, &env.current_contract_address(), &amount); + env.storage().instance().set(&deposit_key, &new_deposit); + // Update idle state let ta = env .storage() @@ -616,6 +654,15 @@ impl YieldVault { .instance() .set(&user_key, &(user_shares - shares)); + let deposit_key = DataKey::UserDeposit(user.clone()); + let current_deposit: i128 = env.storage().instance().get(&deposit_key).unwrap_or(0); + let new_deposit = if current_deposit > assets_to_return { + current_deposit - assets_to_return + } else { + 0 + }; + env.storage().instance().set(&deposit_key, &new_deposit); + env.events().publish( (symbol_short!("withdraw"), user), (assets_to_return, shares), From d78a3a59ae73f59c8e36694967c3af03c18a41f2 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 25 Apr 2026 15:54:29 +0100 Subject: [PATCH 2/5] feat(backend): add prisma and schema drift check to CI --- .github/workflows/backend-governance.yml | 5 ++- backend/package.json | 11 +++++-- .../prisma/migrations/0_init/migration.sql | 27 +++++++++++++++ backend/prisma/schema.prisma | 33 +++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 backend/prisma/migrations/0_init/migration.sql create mode 100644 backend/prisma/schema.prisma diff --git a/.github/workflows/backend-governance.yml b/.github/workflows/backend-governance.yml index 10b1eec6..d0e426b5 100644 --- a/.github/workflows/backend-governance.yml +++ b/.github/workflows/backend-governance.yml @@ -42,4 +42,7 @@ jobs: echo "❌ openapi.json is out of date. Run 'npm run generate:openapi' locally and commit the changes." git diff openapi.json exit 1 - fi \ No newline at end of file + fi + + - name: Check for database schema drift + run: npm run db:check-drift \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 5bcaf60d..df4df30e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,7 +10,10 @@ "test": "jest", "test:smoke": "npm run build && npm run start &", "lint": "eslint src", - "format": "prettier --write src" + "format": "prettier --write src", + "generate:openapi": "tsx scripts/generate-openapi.ts", + "ci:governance": "npm run lint && npm run test && node scripts/check-migrations.js", + "db:check-drift": "prisma migrate diff --from-schema-datamodel prisma/schema.prisma --to-migrations prisma/migrations --exit-code" }, "keywords": [ "stellar", @@ -24,7 +27,8 @@ "express": "^4.18.2", "express-rate-limit": "^7.0.0", "dotenv": "^16.3.1", - "node-cache": "^5.1.2" + "node-cache": "^5.1.2", + "@prisma/client": "^5.10.0" }, "devDependencies": { "@types/express": "^4.17.17", @@ -39,6 +43,7 @@ "eslint": "^8.40.0", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", - "prettier": "^3.0.0" + "prettier": "^3.0.0", + "prisma": "^5.10.0" } } diff --git a/backend/prisma/migrations/0_init/migration.sql b/backend/prisma/migrations/0_init/migration.sql new file mode 100644 index 00000000..3f01700d --- /dev/null +++ b/backend/prisma/migrations/0_init/migration.sql @@ -0,0 +1,27 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "address" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "VaultState" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1, + "totalAssets" TEXT NOT NULL, + "totalShares" TEXT NOT NULL, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "Transaction" ( + "id" TEXT NOT NULL PRIMARY KEY, + "user" TEXT NOT NULL, + "amount" TEXT NOT NULL, + "type" TEXT NOT NULL, + "timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_address_key" ON "User"("address"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 00000000..13242b1e --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,33 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id Int @id @default(autoincrement()) + address String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model VaultState { + id Int @id @default(1) + totalAssets String + totalShares String + updatedAt DateTime @updatedAt +} + +model Transaction { + id String @id @default(uuid()) + user String + amount String + type String + timestamp DateTime @default(now()) +} From 2b29ad916a27dfad019b1e3b269ac6e22eef9577 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 25 Apr 2026 15:55:20 +0100 Subject: [PATCH 3/5] test(frontend): add cypress smoke tests and CI workflow --- .github/workflows/cypress.yml | 47 ++++++++++++++++++++++++++++++++ frontend/cypress.config.ts | 14 ++++++++++ frontend/cypress/e2e/smoke.cy.ts | 32 ++++++++++++++++++++++ frontend/package.json | 5 +++- 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/cypress.yml create mode 100644 frontend/cypress.config.ts create mode 100644 frontend/cypress/e2e/smoke.cy.ts diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml new file mode 100644 index 00000000..41e4c0f2 --- /dev/null +++ b/.github/workflows/cypress.yml @@ -0,0 +1,47 @@ +name: Cypress Smoke Tests + +on: + pull_request: + paths: + - 'frontend/**' + push: + branches: + - main + paths: + - 'frontend/**' + +jobs: + cypress-run: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + working-directory: frontend + start: npm run dev + wait-on: 'http://localhost:5173' + browser: chrome + spec: cypress/e2e/smoke.cy.ts + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + - name: Upload screenshots on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: frontend/cypress/screenshots diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts new file mode 100644 index 00000000..88240b73 --- /dev/null +++ b/frontend/cypress.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:5173', + setupNodeEvents(on, config) { + // implement node event listeners here + }, + viewportWidth: 1280, + viewportHeight: 720, + video: false, + screenshotOnRunFailure: true, + }, +}); diff --git a/frontend/cypress/e2e/smoke.cy.ts b/frontend/cypress/e2e/smoke.cy.ts new file mode 100644 index 00000000..f32bc669 --- /dev/null +++ b/frontend/cypress/e2e/smoke.cy.ts @@ -0,0 +1,32 @@ +describe('YieldVault Smoke Tests', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it('should connect wallet', () => { + // Mock wallet connection + cy.contains('button', 'Connect Wallet').click(); + // Assuming connection shows the address or a disconnect button + cy.contains('button', 'Disconnect').should('be.visible'); + }); + + it('should navigate to deposit flow', () => { + cy.contains('button', 'Connect Wallet').click(); + cy.contains('button', 'Deposit').click(); + cy.contains('Deposit amount').should('be.visible'); + }); + + it('should navigate to withdrawal flow', () => { + cy.contains('button', 'Connect Wallet').click(); + cy.contains('button', 'Withdraw').click(); + cy.contains('Withdrawal amount').should('be.visible'); + }); + + it('should view transaction history', () => { + cy.contains('button', 'Connect Wallet').click(); + // Assuming there's a link to history or it's accessible via URL + cy.visit('/transactions'); + cy.contains('Transaction History').should('be.visible'); + cy.get('table').should('be.visible'); + }); +}); diff --git a/frontend/package.json b/frontend/package.json index c2ae7c15..b9ca2836 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ "test:e2e": "npm run build && playwright test", "test:e2e:ui": "npm run build && playwright test --ui", "test:e2e:debug": "npm run build && playwright test --debug", + "test:cypress": "cypress run", + "test:cypress:open": "cypress open", "docs:api": "typedoc --entryPointStrategy expand --out ../docs/api/frontend src", "check-size": "bundlesize" }, @@ -55,7 +57,8 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.3.1", - "vitest": "^4.1.1" + "vitest": "^4.1.1", + "cypress": "^13.6.0" }, "bundlesize": [ { From d51844715835e9e41d6add797bd1e5dcca863d39 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 25 Apr 2026 15:56:49 +0100 Subject: [PATCH 4/5] feat(frontend): add estimated gas fee display and high fee warning --- frontend/src/components/VaultDashboard.tsx | 57 ++++++++++++++++-- frontend/src/hooks/useFeeEstimate.ts | 69 ++++++++++++++++++++++ frontend/src/lib/vaultApi.ts | 28 +++++++++ 3 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 frontend/src/hooks/useFeeEstimate.ts diff --git a/frontend/src/components/VaultDashboard.tsx b/frontend/src/components/VaultDashboard.tsx index 4ccd5e33..02b43925 100644 --- a/frontend/src/components/VaultDashboard.tsx +++ b/frontend/src/components/VaultDashboard.tsx @@ -19,6 +19,9 @@ import { FormField } from "../forms"; import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations"; import CopyButton from "./CopyButton"; import { copyTextToClipboard } from "../lib/clipboard"; +import { useFeeEstimate } from "../hooks/useFeeEstimate"; +import { AlertTriangle } from "./icons"; +import HelpIcon from "./ui/HelpIcon"; interface VaultDashboardProps { walletAddress: string | null; @@ -154,6 +157,12 @@ const VaultDashboard: React.FC = ({ const depositMutation = useDepositMutation(); const withdrawMutation = useWithdrawMutation(); + const { feeXlm, feeUsd, isEstimating, isHighFee } = useFeeEstimate( + walletAddress, + amount, + activeTab + ); + useEffect(() => { const handleTrigger = () => { setActiveTab("deposit"); @@ -619,6 +628,23 @@ const VaultDashboard: React.FC = ({ {isValidAmount ? `${estimatedFee.toFixed(4)} USDC` : "0.0000 USDC"} +
+ + Estimated network fee + + + {isEstimating ? ( + + ) : ( + <> + {feeXlm.toFixed(6)} XLM + + ≈ ${feeUsd.toFixed(4)} + + + )} + +
{tab === "deposit" ? "Estimated net deposit" : "Estimated net withdrawal"} @@ -627,9 +653,23 @@ const VaultDashboard: React.FC = ({ {isValidAmount ? `${estimatedNetAmount.toFixed(4)} USDC` : "0.0000 USDC"}
-
- Network fee: {summary.networkFeeEstimate} -
+ {isHighFee && ( +
+ +
+ High fee detected: The estimated network fee exceeds 1% of your transaction value. +
+
+ )}