Skip to content
Merged
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
22 changes: 12 additions & 10 deletions apps/contracts/src/BioVerifyV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ error BioVerify_NotAMember();
error BioVerify_InvalidPublicationId(uint256 pubId);
error BioVerify_ClaimTooMuch();
error BioVerify_ClaimFailTransferTo(address member, uint256 amount);
error BioVerify_InsufficientPublisherFee();
error BioVerify_MustPayPublisherStake();
error BioVerify_InsufficientPayment();
error BioVerify_MustPayReviewerStake();
error BioVerify_AlreadyInReview(uint256 pubId);
error BioVerify_CanNotSettle(uint256 pubId, PublicationStatus from, PublicationStatus to);
Expand Down Expand Up @@ -245,15 +244,18 @@ contract BioVerifyV3 is VRFConsumerBaseV2Plus, ReentrancyGuard {
}

/**
* @notice Submits a new research publication and locks the publisher's stake.
* @notice Submits a new research publication and locks the publisher's fixed protocol stake.
* @dev The stake is always `I_PUBLISHER_STAKE`. The submission fee is derived from `msg.value`
* as `msg.value - I_PUBLISHER_STAKE`, allowing callers to overpay the fee (e.g. to cover VRF
* cost spikes) while enforcing a minimum fee via `I_PUBLISHER_MIN_FEE`.
* @param _cid The IPFS content identifier of the publication.
* @param _paidSubmissionFee The fee paid to cover VRF and AI processing costs.
*/
function submitPublication(string calldata _cid, uint256 _paidSubmissionFee) external payable {
function submitPublication(string calldata _cid) external payable {
// Check
uint256 stake = msg.value - _paidSubmissionFee;
if (_paidSubmissionFee < I_PUBLISHER_MIN_FEE) revert BioVerify_InsufficientPublisherFee();
if (msg.value != I_PUBLISHER_STAKE + _paidSubmissionFee) revert BioVerify_MustPayPublisherStake();
if (msg.value < I_PUBLISHER_STAKE + I_PUBLISHER_MIN_FEE) revert BioVerify_InsufficientPayment();

uint256 stake = I_PUBLISHER_STAKE;
uint256 paidFee = msg.value - stake;

// Effect
uint256 pubId = nextPubId;
Expand All @@ -262,7 +264,7 @@ contract BioVerifyV3 is VRFConsumerBaseV2Plus, ReentrancyGuard {
pub.id = pubId;
pub.publisher = msg.sender;
pub.cid = _cid;
pub.paidSubmissionFee = _paidSubmissionFee;
pub.paidSubmissionFee = paidFee;
pub.status = PublicationStatus.SUBMITTED;
emit NewPublicationStatus(pubId, PublicationStatus.SUBMITTED);

Expand All @@ -278,7 +280,7 @@ contract BioVerifyV3 is VRFConsumerBaseV2Plus, ReentrancyGuard {
// Lock publisher stake on pubId.
_lockStake(msg.sender, pubId, stake);

emit SubmitPublication(msg.sender, pubId, _cid, _paidSubmissionFee);
emit SubmitPublication(msg.sender, pubId, _cid, paidFee);

nextPubId++;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/contracts/test/01_Helpers.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ contract TestHelpers is BaseTest {
function _submitDefaultPub() internal {
vm.deal(publisher, 1 ether);
vm.prank(publisher);
bioVerify.submitPublication{value: validPaidPublisherAmount}(FAKE_CID, publisherMinFee);
bioVerify.submitPublication{value: validPaidPublisherAmount}(FAKE_CID);
}

// Helpers to fill the reviewer pool before testing pickReviewers
Expand Down
47 changes: 10 additions & 37 deletions apps/contracts/test/04_SubmitPublication.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {TestHelpers} from "./01_Helpers.t.sol";
import {

// errors
BioVerify_MustPayPublisherStake,
BioVerify_InsufficientPublisherFee,
BioVerify_InsufficientPayment,

// enum
PublicationStatus
Expand All @@ -27,55 +26,29 @@ contract SubmitPublicationTest is TestHelpers {

// 3. Execute
vm.prank(publisher);
bioVerify.submitPublication{value: validValue}(FAKE_CID, publisherMinFee);
bioVerify.submitPublication{value: validValue}(FAKE_CID);
}

// ===== unhappy path
function test_RevertIf_StakeTooLow() public {
uint256 stakeTooLow = publisherStake - 1;
uint256 wrongTotal = publisherMinFee + stakeTooLow;
function test_RevertIf_InsufficientPayment() public {
// Anything below stake + min fee should revert.
uint256 insufficientTotal = publisherStake + publisherMinFee - 1;

vm.deal(publisher, 1 ether);
vm.prank(publisher);

vm.expectRevert(abi.encodeWithSelector(BioVerify_MustPayPublisherStake.selector));
bioVerify.submitPublication{value: wrongTotal}(FAKE_CID, publisherMinFee);
}

function test_RevertIf_StakeTooHigh() public {
uint256 stakeTooHigh = publisherStake + 1;
uint256 wrongTotal = publisherMinFee + stakeTooHigh;

vm.deal(publisher, 1 ether);
vm.prank(publisher);

vm.expectRevert(abi.encodeWithSelector(BioVerify_MustPayPublisherStake.selector));
bioVerify.submitPublication{value: wrongTotal}(FAKE_CID, publisherMinFee);
}

function test_RevertIf_InsufficientFee() public {
uint256 invalidFee = publisherMinFee - 1;
uint256 validTotal = publisherStake + publisherMinFee;

vm.deal(publisher, 1 ether);
vm.prank(publisher);

vm.expectRevert(abi.encodeWithSelector(BioVerify_InsufficientPublisherFee.selector));
bioVerify.submitPublication{value: validTotal}(FAKE_CID, invalidFee);
vm.expectRevert(abi.encodeWithSelector(BioVerify_InsufficientPayment.selector));
bioVerify.submitPublication{value: insufficientTotal}(FAKE_CID);
}
}

contract FuzzSubmit is TestHelpers {
function testFuzz_SubmitPublication(uint256 fee, uint256 stake) public {
// Bound the inputs to realistic values to avoid overflow in test math
fee = bound(fee, publisherMinFee, 100 ether);
stake = bound(stake, publisherStake, publisherStake);

uint256 total = fee + stake;
function testFuzz_SubmitPublication(uint256 total) public {
total = bound(total, publisherStake + publisherMinFee, 100 ether);
vm.deal(publisher, total);

vm.prank(publisher);
bioVerify.submitPublication{value: total}(FAKE_CID, fee);
bioVerify.submitPublication{value: total}(FAKE_CID);

// Invariant: Contract balance must have increased by exactly 'total'
assertEq(address(bioVerify).balance, DEPLOY_VALUE + total);
Expand Down
2 changes: 1 addition & 1 deletion apps/contracts/test/06_PickReviewers.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ contract PickReviewersTest is TestHelpers {
// Submit publication
vm.deal(publisher, 1 ether);
vm.prank(publisher);
poorContract.submitPublication{value: validPaidPublisherAmount}(FAKE_CID, publisherMinFee);
poorContract.submitPublication{value: validPaidPublisherAmount}(FAKE_CID);

// Fill reviewers pool
for (uint256 i = 0; i < validMinReviewersPoolSize; ++i) {
Expand Down
4 changes: 2 additions & 2 deletions apps/contracts/test/07_FullfillVRF.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ contract FulfillVRFTest is TestHelpers {
// 2. Submit 2 publications
vm.deal(publisher, 2 ether);
vm.startPrank(publisher);
poorContract.submitPublication{value: validPaidPublisherAmount}(FAKE_CID, publisherMinFee);
poorContract.submitPublication{value: validPaidPublisherAmount}(FAKE_CID, publisherMinFee);
poorContract.submitPublication{value: validPaidPublisherAmount}(FAKE_CID);
poorContract.submitPublication{value: validPaidPublisherAmount}(FAKE_CID);
vm.stopPrank();

// 3. Fill reviewers pool
Expand Down
2 changes: 1 addition & 1 deletion apps/contracts/test/12_PoolManagement.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ contract PoolManagementTest is TestHelpers {
// Setup submit publication
vm.deal(publisher, 1 ether);
vm.prank(publisher);
bioVerifyWithRejectorTreasury.submitPublication{value: validPaidPublisherAmount}(FAKE_CID, publisherMinFee);
bioVerifyWithRejectorTreasury.submitPublication{value: validPaidPublisherAmount}(FAKE_CID);

// Early slash publication to get publisherStake into slashPool
vm.prank(aiAgentAddress);
Expand Down
16 changes: 8 additions & 8 deletions apps/fe/_config/viem/ws-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { baseSepolia, sepolia } from "viem/chains"
*/

export const createBaseSepoliaWsClient = () =>
createPublicClient({
chain: baseSepolia,
transport: webSocket(env.NEXT_PUBLIC_ALCHEMY_BASE_SEPOLIA_WSS),
})
createPublicClient({
chain: baseSepolia,
transport: webSocket(env.NEXT_PUBLIC_ALCHEMY_BASE_SEPOLIA_WSS),
})

export const createEthSepoliaWsClient = () =>
createPublicClient({
chain: sepolia,
transport: webSocket(env.NEXT_PUBLIC_ALCHEMY_ETH_SEPOLIA_WSS),
})
createPublicClient({
chain: sepolia,
transport: webSocket(env.NEXT_PUBLIC_ALCHEMY_ETH_SEPOLIA_WSS),
})
15 changes: 8 additions & 7 deletions apps/fe/_config/wagmi/wagmi-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @title Wagmi & Reown Configuration
* @notice This file configures the Web3 provider layer, handling multi-chain
* @notice This file configures the Web3 provider layer, handling multi-chain
* connectivity, replay protection, and server-side state hydration.
* * High-Reliability Features:
* 1. EIP-155 Replay Protection: Ensures transactions are chain-specific.
Expand All @@ -15,7 +15,8 @@ import { cookieStorage, createStorage } from "@wagmi/core"
import { http } from "wagmi"

// --- Alchemy Configuration ---
const ALCHEMY_BASE_SEPOLIA_RPC_URL = env.NEXT_PUBLIC_ALCHEMY_BASE_SEPOLIA_RPC_URL
const ALCHEMY_BASE_SEPOLIA_RPC_URL =
env.NEXT_PUBLIC_ALCHEMY_BASE_SEPOLIA_RPC_URL
const ALCHEMY_ETH_SEPOLIA_RPC_URL = env.NEXT_PUBLIC_ALCHEMY_ETH_SEPOLIA_RPC_URL

const RAINBOWKIT_PROJECT_ID = env.NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID || ""
Expand All @@ -24,10 +25,10 @@ const networks = [sepolia, baseSepolia]
/**
* @notice Custom RPC Map using CAIP-2 (Chain Agnostic Improvement Proposals)
* @dev CAIP-2 uses a `namespace:reference` format (e.g., "eip155:84532").
* * Namespace (eip155): Identifies the EVM ecosystem based on EIP-155 standards,
* which prevent replay attacks by incorporating the Chain ID into the $v$
* * Namespace (eip155): Identifies the EVM ecosystem based on EIP-155 standards,
* which prevent replay attacks by incorporating the Chain ID into the $v$
* value of the ECDSA signature.
* * Mapping these here ensures the Reown/AppKit Modal uses our dedicated Alchemy
* * Mapping these here ensures the Reown/AppKit Modal uses our dedicated Alchemy
* nodes for balance checks instead of slower public RPCs.
*/
export const customRpcUrls = {
Expand All @@ -46,7 +47,7 @@ export const customRpcUrls = {
* @dev This adapter bridges Wagmi hooks with the Reown (formerly WalletConnect) AppKit.
* * SSR & Storage:
* - `ssr: true`: Enables hydration-safe rendering.
* - `cookieStorage`: Persists session state in HTTP headers, allowing the server
* - `cookieStorage`: Persists session state in HTTP headers, allowing the server
* to recognize the user's wallet before the JS bundle loads, preventing UI flicker.
* * Transports:
* - HTTP-only via Alchemy. Real-time event subscriptions (WebSocket) are handled
Expand All @@ -67,4 +68,4 @@ export const wagmiAdapter = new WagmiAdapter({
},
})

export const reownConfig = wagmiAdapter.wagmiConfig
export const reownConfig = wagmiAdapter.wagmiConfig
7 changes: 3 additions & 4 deletions apps/fe/_hooks/cqrs/commands/use-submit-publication.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"use client"

import { reownConfig } from "@/_config/wagmi/wagmi-config"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { waitForTransactionReceipt, writeContract } from "@wagmi/core"
import { toast } from "sonner"
import { reownConfig } from "@/_config/wagmi/wagmi-config"
import { useContractConfig } from "../../use-contract-config"
import { publicationsKeys } from "../query-keys/publications-keys"

type PublicationArgs = {
cid: string
totalWeiValue: bigint
submissionFeeWeiValue: bigint
}

export const useSubmitPublication = () => {
Expand All @@ -19,13 +18,13 @@ export const useSubmitPublication = () => {

return useMutation({
mutationFn: async (args: PublicationArgs) => {
const { cid, totalWeiValue, submissionFeeWeiValue } = args
const { cid, totalWeiValue } = args

// Trigger Wallet
const hash = await writeContract(reownConfig, {
...contractConfig,
functionName: "submitPublication",
args: [cid, submissionFeeWeiValue],
args: [cid],
value: totalWeiValue,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ const INVALIDATION_DELAY_MS = 3_000

const baseSepoliaConfig =
BioVerifyContractConfig[NetworkSchema.enum.base_sepolia]
const ethSepoliaConfig =
BioVerifyContractConfig[NetworkSchema.enum.eth_sepolia]
const ethSepoliaConfig = BioVerifyContractConfig[NetworkSchema.enum.eth_sepolia]

export const useWatchNewPublicationStatusEvent = () => {
const queryClient = useQueryClient()
Expand Down Expand Up @@ -80,9 +79,7 @@ export const useWatchNewPublicationStatusEvent = () => {
})

if (process.env.NODE_ENV === "development") {
console.log(
`[WS] Cache invalidated for pubIds: ${pubIds.join(", ")}`,
)
console.log(`[WS] Cache invalidated for pubIds: ${pubIds.join(", ")}`)
}
}, INVALIDATION_DELAY_MS)
},
Expand Down
Loading
Loading