ScholarChain is a decentralized grant management protocol. It combines smart contracts and a web client to manage the full grant lifecycle on-chain:
- A creator deploys a new grant pool.
- Donors fund the pool with USDT.
- Applicants submit proposals whose metadata is stored on IPFS.
- Assigned reviewers vote on submissions.
- Approved applicants become winners once quorum is reached.
- The pool enters distribution, sends a 10% protocol fee to treasury, and lets winners claim their grants.
- Each winner receives a Soul-Bound Token as a permanent proof of grant receipt.
The project is split into two workspaces:
contract/: Solidity contracts, Foundry deployment scripts, and testsfrontend/: Vite + React application for end users, reviewers, creators, donors, and treasury signers
- Solidity
^0.8.24 - Foundry
- OpenZeppelin Contracts
- React
19 - TypeScript
- Vite
- Ethers
v6 - Reown AppKit
- Tailwind CSS
v4
- IPFS for criteria and proposal metadata
- Pinata for file pinning
- Sepolia RPC for read and write access
contract/
src/
ScholarChainFactory.sol
GrantPool.sol
ScholarChainSBT.sol
TreasuryMultisig.sol
MultiSig.sol
ERC20MultiSig.sol
mocks/MockUSDT.sol
Interfaces/
Types/
script/
Deploy.s.sol
DeployMockUSDT.s.sol
test/
GrantPool.t.sol
ScholarChainFactory.t.sol
ScholarChainSBT.t.sol
TreasuryMultisig.t.sol
foundry.toml
frontend/
src/
components/
connection/
constants/
hooks/
pages/
utils/
public/
package.json
File: contract/src/ScholarChainFactory.sol
Responsibility:
- Deploys new
GrantPoolcontracts - Stores the treasury address used by every pool
- Stores the SBT contract address used by every pool
- Tracks all created pools and pools created by each address
- Exposes aggregate protocol statistics for the frontend
- Grants each new pool the
MINTER_ROLEneeded to mint winner SBTs
Important rules:
- Treasury fee is fixed to
1000basis points, which equals10% - A pool must have at least
3signers - A pool can have at most
20signers - A pool must define at least
1application field and at most10 - Submission start must be in the future
- Submission window must be longer than
1day - Review duration must be at least
1day
Key frontend-facing view functions:
getAllPools()getPoolsByCreator(address)getTotalPoolCount()getAllPoolSummaries()getProtocolStats()
File: contract/src/GrantPool.sol
Responsibility:
- Stores pool metadata and configuration
- Accepts USDT donations
- Accepts applicant submissions
- Records signer votes
- Selects winners based on quorum
- Handles distribution, claiming, refunds, and unclaimed fund recovery
The pool state is derived from timestamps and flags, not stored as a mutable enum.
PENDING: before submission startACTIVE: submission window is openREVIEW: submission window is closed and review is still ongoingDISTRIBUTING: review ended and winners can claimCLOSED: all winner claims completed or zero-winner refunds settledCANCELLED: creator cancelled the pool before submissions opened
Donations:
donate(uint256)accepts USDT from any donor duringPENDINGorACTIVEtopUpPool(uint256)lets the creator add more funds during the same period
Pool administration:
editCriteria(bytes32)lets the creator update the criteria CID only inPENDINGaddSigner(address)lets the creator add reviewers beforesubmissionStartcancelPool()lets the creator cancel only while the pool is stillPENDING
Applications:
submitProposal(bytes32 docCID, address payoutAddr)allows one proposal per benefactor duringACTIVE- Each proposal stores the applicant document CID and payout wallet
Review:
vote(address benefactor, bool approve)is restricted to signers- Each signer can vote only once per applicant
- A winner is approved once they reach
ceil(signers * 70 / 100)
Distribution:
enterDistributionPhase()can be called by anyone after review ends- 10% of
totalDepositedis transferred to the treasury - The remaining 90% is divided equally among winners
- If there are no winners, donors become eligible for proportional refunds from the remaining 90%
Claiming:
claimGrant()transfers the winner share to the winner’s declared payout address- After transfer, the pool mints a winner SBT through the SBT contract
- When the last claim finishes, remaining dust is swept to treasury
Refunds and recovery:
claimRefund()lets donors recover funds if the pool is cancelledclaimRefund()also supports the zero-winner path after distributionrecoverUnclaimedFunds()lets an admin recover stuck funds after90 days
getSigners()getWinners()quorumThreshold()getProposal(address)getApprovalCount(address)hasVoted(address,address)getProposalCount()getClaimedCount()getFieldDefinitions()getPoolSummary()
File: contract/src/ScholarChainSBT.sol
Responsibility:
- Mints non-transferable ERC-721 award tokens to grant winners
- Stores award metadata fully on-chain as a Base64 JSON data URI
- Prevents duplicate awards for the same recipient in the same pool
Important properties:
- Only accounts with
MINTER_ROLEcan mint - Transfers are disabled
- Approvals are disabled
- The token records:
- pool address
- grant amount
- payout address
- award timestamp
Files:
Responsibility:
- Controls protocol treasury funds
- Controls admin actions after deployment
- Requires multiple signatures before executing proposals
Supported proposal patterns include:
- ERC-20 withdrawal
- add signer
- remove signer
- change required signature threshold
- arbitrary admin or contract calls
Deployment script:
Deployment order:
- Resolve USDT address
- Deploy
TreasuryMultisig - Deploy
ScholarChainSBT - Deploy
ScholarChainFactory - Grant factory admin access on the SBT contract
- Transfer factory and SBT admin rights to treasury multisig
- Renounce deployer admin rights
Result:
- Treasury multisig becomes the protocol governor
- Factory becomes able to authorize new pool contracts as SBT minters
- The deployer does not remain as persistent protocol admin
Mock token script:
File: frontend/src/App.tsx
Main routes:
/: public landing page/dashbar/dashboard: wallet-protected dashboard/dashbar/explore: public pool explorer/dashbar/create: pool creation page/dashbar/review: wallet-protected signer review page/dashbar/pool/:address: pool detail page/treasury: treasury multisig administration page
Files:
- frontend/src/connection/AppkitWrapper.tsx
- frontend/src/hooks/useWallet.ts
- frontend/src/hooks/useRunners.ts
Behavior:
- Reown AppKit powers wallet connection
- Ethers powers signing and contract reads
- Sepolia is the configured network
- Missing
VITE_PROJECT_IDdisables wallet connection gracefully
Files:
Behavior:
- Uses a JSON-RPC provider for read-only access
- Creates signer-backed contracts when wallet actions are needed
- Resolves factory address from environment variables, with a Sepolia fallback
Landing page:
- frontend/src/pages/LandingPage.tsx
- Shows live protocol metrics
- Shows featured pool data
- Routes users into pool creation or exploration
Create pool page:
- frontend/src/pages/CreatePoolPage.tsx
- Uploads the criteria PDF to Pinata
- Converts the returned CID to
bytes32 - Calls factory
createPool - Optionally sends an initial USDT donation after deployment
Review page:
- frontend/src/pages/ReviewPage.tsx
- Lists pools where the connected wallet is a signer
- Reads
ProposalSubmittedevents to discover applicants - Fetches proposal metadata from IPFS
- Sends approve or reject votes on-chain
Treasury page:
- frontend/src/pages/TreasuryPage.tsx
- Shows treasury stats and proposals
- Lets signers create withdrawal and admin proposals
- Supports signing and executing multisig actions
Pool explorer and detail views:
ExplorerPage.tsx,DashboardPage.tsx, andPoolDetailPage.tsx- Surface pool summaries, balances, timelines, proposals, and donation flows
Files:
Flow:
- The user uploads a PDF criteria document in the create pool page.
- The frontend uploads the file to Pinata using
VITE_PINATA_JWT. - Pinata returns a CIDv0 hash.
- The frontend converts the CID to
bytes32before sending it to the contract. - When data is read back from the chain,
bytes32values are converted to CIDs again for gateway access.
Important detail:
- The app expects CIDv0 or a raw
0xbytes32digest for contract storage.
Template file:
Variables:
PRIVATE_KEY: deployer private keyRPC_URL: RPC endpoint for the target networkUSDT_ADDRESS: real or mock USDT addressTREASURY_SIGNER_1TREASURY_SIGNER_2TREASURY_SIGNER_3REQUIRED_SIGSTEAM_MULTISIGETHERSCAN_API_KEY
Template file:
Variables used by source code:
VITE_PROJECT_IDVITE_SEPOLIA_RPC_URLVITE_FACTORY_CONTRACT_ADDRESSVITE_TREASURY_MULTISIG_ADDRESSVITE_SCHOLAR_CHAIN_SBT_ADDRESSVITE_MOCK_USDT_ADDRESSVITE_PINATA_JWTVITE_PINATA_GATEWAYVITE_BLOCK_EXPLORER_BASEVITE_APP_URLVITE_APP_ICON
Note:
- The committed local frontend
.envcontains populated values. Replace exposed credentials before reuse.
These values appear in the current frontend configuration and frontend README:
- Treasury Multisig:
0x55dd331Fc1c894D7EC74C931A586aA05fE694d6A - ScholarChain SBT:
0xEBD33E6F07bE36e3A8c260Be4665d5e48cD1a20c - ScholarChain Factory:
0x0Ac0cBF23279be96A31618B45A7EA65C603e0825 - Mock USDT:
0xf328F1b428748710687A0d275AF939eA100aA29c
cd contract
cp .env.example .env
forge build
forge testcd frontend
cp .env.example .env
npm install
npm run devcd frontend
npm run buildBuild output is written to frontend/dist.
Contract test files:
- contract/test/GrantPool.t.sol
- contract/test/ScholarChainFactory.t.sol
- contract/test/ScholarChainSBT.t.sol
- contract/test/TreasuryMultisig.t.sol
Verified in this workspace:
forge test:197/197tests passednpm run build: frontend build artifacts were generated successfully infrontend/dist
Areas covered by tests include:
- factory validation and deployment behavior
- pool state transitions
- donations and creator top-ups
- proposal submission rules
- signer voting and quorum
- distribution math
- refunds and unclaimed fund recovery
- SBT minting and transfer restrictions
- multisig proposal, signing, execution, and signer management
- The treasury fee is fixed to
10%. - Winner payout uses each applicant’s declared payout address, not necessarily the submitting wallet.
- Zero-winner pools refund only the post-fee remainder proportionally to donors.
- The review page discovers proposals from emitted events, so RPC log availability matters.
- The treasury page currently contains hardcoded Sepolia addresses in source for treasury and mock USDT views.
- Pool creation depends on a working Pinata JWT with file pinning permissions.
- Remove secrets from committed
.envfiles - Move all contract addresses into
.env.exampleand consume them consistently from environment variables - Add a root README badge or quick-start section if the project will be shared externally
- Add explicit backend-free architecture notes if this will be used for demos or judging
ScholarChain is a two-part Web3 application:
- a contract system for transparent grant funding, review, and payout
- a React frontend that exposes those flows to creators, donors, reviewers, and treasury signers
Its strongest implementation points are:
- strict contract validation at pool creation
- on-chain lifecycle enforcement
- multisig-controlled treasury governance
- permanent SBT award records
- a tested Foundry suite covering the main protocol behaviors