Skip to content

Commit 3dce37e

Browse files
authored
Merge pull request #5 from Dharmesh070294/feat-solidity-escrow-contract
feat: add grant escrow smart contract
2 parents 8d5f51d + b3c5a07 commit 3dce37e

9 files changed

Lines changed: 8366 additions & 0 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
BASE_SEPOLIA_RPC_URL=
2+
PRIVATE_KEY=
3+
USDC_ADDRESS=

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
artifacts/
3+
cache/
4+
.env
5+
coverage/

contracts/GrantStreamEscrow.sol

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
7+
contract GrantStreamEscrow {
8+
using SafeERC20 for IERC20;
9+
10+
enum MilestoneStatus {
11+
Pending,
12+
Submitted,
13+
Approved,
14+
Paid,
15+
Rejected
16+
}
17+
18+
struct Milestone {
19+
uint256 amount;
20+
string evidenceURI;
21+
MilestoneStatus status;
22+
}
23+
24+
struct Grant {
25+
address funder;
26+
address grantee;
27+
address verifier;
28+
uint256 totalAmount;
29+
uint256 paidAmount;
30+
bool funded;
31+
bool exists;
32+
}
33+
34+
IERC20 public immutable usdc;
35+
uint256 public nextGrantId;
36+
37+
mapping(uint256 => Grant) public grants;
38+
mapping(uint256 => Milestone[]) private grantMilestones;
39+
40+
event GrantCreated(
41+
uint256 indexed grantId,
42+
address indexed funder,
43+
address indexed grantee,
44+
address verifier,
45+
uint256 totalAmount
46+
);
47+
48+
event GrantFunded(uint256 indexed grantId, uint256 amount);
49+
event MilestoneSubmitted(uint256 indexed grantId, uint256 indexed milestoneId, string evidenceURI);
50+
event MilestoneApproved(uint256 indexed grantId, uint256 indexed milestoneId);
51+
event MilestoneRejected(uint256 indexed grantId, uint256 indexed milestoneId);
52+
event MilestonePaid(uint256 indexed grantId, uint256 indexed milestoneId, address indexed grantee, uint256 amount);
53+
54+
error InvalidAddress();
55+
error InvalidAmount();
56+
error InvalidMilestones();
57+
error GrantNotFound();
58+
error NotFunder();
59+
error NotGrantee();
60+
error NotVerifier();
61+
error GrantAlreadyFunded();
62+
error GrantNotFunded();
63+
error InvalidMilestone();
64+
error InvalidStatus();
65+
error EmptyEvidenceURI();
66+
67+
constructor(address _usdc) {
68+
if (_usdc == address(0)) revert InvalidAddress();
69+
usdc = IERC20(_usdc);
70+
}
71+
72+
function createGrant(
73+
address grantee,
74+
address verifier,
75+
uint256[] calldata milestoneAmounts
76+
) external returns (uint256 grantId) {
77+
if (grantee == address(0) || verifier == address(0)) revert InvalidAddress();
78+
if (milestoneAmounts.length == 0) revert InvalidMilestones();
79+
80+
uint256 totalAmount;
81+
82+
for (uint256 i = 0; i < milestoneAmounts.length; i++) {
83+
if (milestoneAmounts[i] == 0) revert InvalidAmount();
84+
totalAmount += milestoneAmounts[i];
85+
}
86+
87+
grantId = nextGrantId++;
88+
89+
grants[grantId] = Grant({
90+
funder: msg.sender,
91+
grantee: grantee,
92+
verifier: verifier,
93+
totalAmount: totalAmount,
94+
paidAmount: 0,
95+
funded: false,
96+
exists: true
97+
});
98+
99+
for (uint256 i = 0; i < milestoneAmounts.length; i++) {
100+
grantMilestones[grantId].push(
101+
Milestone({
102+
amount: milestoneAmounts[i],
103+
evidenceURI: "",
104+
status: MilestoneStatus.Pending
105+
})
106+
);
107+
}
108+
109+
emit GrantCreated(grantId, msg.sender, grantee, verifier, totalAmount);
110+
}
111+
112+
function fundGrant(uint256 grantId) external {
113+
Grant storage grant = grants[grantId];
114+
115+
if (!grant.exists) revert GrantNotFound();
116+
if (msg.sender != grant.funder) revert NotFunder();
117+
if (grant.funded) revert GrantAlreadyFunded();
118+
119+
grant.funded = true;
120+
usdc.safeTransferFrom(msg.sender, address(this), grant.totalAmount);
121+
122+
emit GrantFunded(grantId, grant.totalAmount);
123+
}
124+
125+
function submitMilestone(
126+
uint256 grantId,
127+
uint256 milestoneId,
128+
string calldata evidenceURI
129+
) external {
130+
Grant storage grant = grants[grantId];
131+
132+
if (!grant.exists) revert GrantNotFound();
133+
if (!grant.funded) revert GrantNotFunded();
134+
if (msg.sender != grant.grantee) revert NotGrantee();
135+
if (bytes(evidenceURI).length == 0) revert EmptyEvidenceURI();
136+
if (milestoneId >= grantMilestones[grantId].length) revert InvalidMilestone();
137+
138+
Milestone storage milestone = grantMilestones[grantId][milestoneId];
139+
140+
if (
141+
milestone.status != MilestoneStatus.Pending &&
142+
milestone.status != MilestoneStatus.Rejected
143+
) revert InvalidStatus();
144+
145+
milestone.evidenceURI = evidenceURI;
146+
milestone.status = MilestoneStatus.Submitted;
147+
148+
emit MilestoneSubmitted(grantId, milestoneId, evidenceURI);
149+
}
150+
151+
function approveMilestone(uint256 grantId, uint256 milestoneId) external {
152+
Grant storage grant = grants[grantId];
153+
154+
if (!grant.exists) revert GrantNotFound();
155+
if (msg.sender != grant.verifier) revert NotVerifier();
156+
if (milestoneId >= grantMilestones[grantId].length) revert InvalidMilestone();
157+
158+
Milestone storage milestone = grantMilestones[grantId][milestoneId];
159+
160+
if (milestone.status != MilestoneStatus.Submitted) revert InvalidStatus();
161+
162+
milestone.status = MilestoneStatus.Approved;
163+
164+
emit MilestoneApproved(grantId, milestoneId);
165+
166+
_releaseMilestone(grantId, milestoneId);
167+
}
168+
169+
function rejectMilestone(uint256 grantId, uint256 milestoneId) external {
170+
Grant storage grant = grants[grantId];
171+
172+
if (!grant.exists) revert GrantNotFound();
173+
if (msg.sender != grant.verifier) revert NotVerifier();
174+
if (milestoneId >= grantMilestones[grantId].length) revert InvalidMilestone();
175+
176+
Milestone storage milestone = grantMilestones[grantId][milestoneId];
177+
178+
if (milestone.status != MilestoneStatus.Submitted) revert InvalidStatus();
179+
180+
milestone.status = MilestoneStatus.Rejected;
181+
182+
emit MilestoneRejected(grantId, milestoneId);
183+
}
184+
185+
function getMilestone(
186+
uint256 grantId,
187+
uint256 milestoneId
188+
) external view returns (Milestone memory) {
189+
if (!grants[grantId].exists) revert GrantNotFound();
190+
if (milestoneId >= grantMilestones[grantId].length) revert InvalidMilestone();
191+
192+
return grantMilestones[grantId][milestoneId];
193+
}
194+
195+
function getMilestoneCount(uint256 grantId) external view returns (uint256) {
196+
if (!grants[grantId].exists) revert GrantNotFound();
197+
return grantMilestones[grantId].length;
198+
}
199+
200+
function _releaseMilestone(uint256 grantId, uint256 milestoneId) internal {
201+
Grant storage grant = grants[grantId];
202+
Milestone storage milestone = grantMilestones[grantId][milestoneId];
203+
204+
if (milestone.status != MilestoneStatus.Approved) revert InvalidStatus();
205+
206+
milestone.status = MilestoneStatus.Paid;
207+
grant.paidAmount += milestone.amount;
208+
209+
usdc.safeTransfer(grant.grantee, milestone.amount);
210+
211+
emit MilestonePaid(grantId, milestoneId, grant.grantee, milestone.amount);
212+
}
213+
}

contracts/MockUSDC.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract MockUSDC is ERC20 {
7+
constructor() ERC20("Mock USDC", "USDC") {}
8+
9+
function decimals() public pure override returns (uint8) {
10+
return 6;
11+
}
12+
13+
function mint(address to, uint256 amount) external {
14+
_mint(to, amount);
15+
}
16+
}

hardhat.config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require("@nomicfoundation/hardhat-toolbox");
2+
require("dotenv").config();
3+
4+
module.exports = {
5+
solidity: "0.8.24",
6+
networks: {
7+
baseSepolia: {
8+
url: process.env.BASE_SEPOLIA_RPC_URL || "",
9+
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
10+
chainId: 84532,
11+
},
12+
},
13+
};

0 commit comments

Comments
 (0)