Repository navigation
Expand file tree
/
Copy pathBountyManagerPure.sol
More file actions
214 lines (186 loc) · 8.14 KB
/
Copy pathBountyManagerPure.sol
File metadata and controls
214 lines (186 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/*─────────── OpenZeppelin v5.3 Upgradeables ──────────*/
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
/*─────────────────────── Contract ───────────────────────*/
contract BountyManagerPure is Initializable, ReentrancyGuardUpgradeable, ContextUpgradeable {
using SafeERC20 for IERC20;
/*─────────────── Custom Errors ───────────────*/
error ZeroAddress();
error EmptyDescription();
error InvalidPayout();
error UnknownBounty();
error NotCreator();
error InvalidStateTransition();
error InvalidRecipient();
error InvalidExpiry();
error BountyExpired();
/*─────────────── Constants ──────────────────*/
uint256 public constant MAX_PAYOUT = 1e24; // 1,000,000 tokens (18 dec)
uint256 public constant MIN_EXPIRY = 1 days;
uint256 public constant MAX_EXPIRY = 30 days;
bytes4 public constant MODULE_ID = 0x42594e33; // "BN3"
/*─────────────── Data Types ─────────────────*/
enum Status {
ACTIVE,
COMPLETED,
EXPIRED
}
struct Bounty {
uint248 payout;
Status status;
address creator;
IERC20 token;
string description;
uint256 expiry;
}
/*─────────────── Storage ─────────────────────*/
mapping(uint256 => Bounty) private _bounties;
uint256 public nextBountyId;
uint256 public activeBountyCount;
/*─────────────── Events ─────────────────────*/
event BountyCreated(
uint256 indexed id,
address indexed token,
uint256 payout,
string description,
address indexed creator,
uint256 expiry
);
event BountyUpdated(uint256 indexed id, uint256 payout, string description);
event BountyCompleted(uint256 indexed id, address indexed recipient, address indexed completer);
event BountyCancelled(uint256 indexed id, address indexed canceller);
/*──────────────── Initialiser ───────────────*/
function initialize() external initializer {
__ReentrancyGuard_init();
__Context_init();
}
/*─────────────────── Bounty Logic ──────────────────*/
function createBounty(
IERC20 token,
uint256 payout,
string calldata description,
uint256 expiry
) external {
if (address(token) == address(0)) revert ZeroAddress();
if (payout == 0 || payout > MAX_PAYOUT) revert InvalidPayout();
if (bytes(description).length == 0) revert EmptyDescription();
if (expiry < block.timestamp + MIN_EXPIRY || expiry > block.timestamp + MAX_EXPIRY) revert InvalidExpiry();
// transfer funds into escrow
token.safeTransferFrom(_msgSender(), address(this), payout);
uint256 id = nextBountyId++;
_bounties[id] = Bounty({
payout: SafeCast.toUint248(payout),
status: Status.ACTIVE,
creator: _msgSender(),
token: token,
description: description,
expiry: expiry
});
activeBountyCount++;
emit BountyCreated(id, address(token), payout, description, _msgSender(), expiry);
}
function updateBounty(uint256 id, uint256 newPayout, string calldata newDescription) external {
Bounty storage b = _bounty(id);
if (b.creator != _msgSender()) revert NotCreator();
if (b.status != Status.ACTIVE) revert InvalidStateTransition();
if (block.timestamp >= b.expiry) revert BountyExpired();
if (newPayout == 0 || newPayout > MAX_PAYOUT) revert InvalidPayout();
if (bytes(newDescription).length == 0) revert EmptyDescription();
// adjust escrow if payout changed
if (newPayout > b.payout) {
uint256 diff = newPayout - b.payout;
b.token.safeTransferFrom(_msgSender(), address(this), diff);
} else if (newPayout < b.payout) {
revert InvalidPayout();
}
b.payout = SafeCast.toUint248(newPayout);
b.description = newDescription;
emit BountyUpdated(id, newPayout, newDescription);
}
function completeBounty(uint256 id, address recipient) external nonReentrant {
Bounty storage b = _bounty(id);
if (b.status != Status.ACTIVE) revert InvalidStateTransition();
if (b.creator != _msgSender()) revert NotCreator();
if (recipient == address(0) || recipient == b.creator) revert InvalidRecipient();
if (block.timestamp >= b.expiry) revert BountyExpired();
b.token.safeTransfer(recipient, b.payout);
b.status = Status.COMPLETED;
activeBountyCount--;
emit BountyCompleted(id, recipient, _msgSender());
}
/*──────────── View Helpers ───────────*/
function getBounty(uint256 id)
external
view
returns (
uint256 payout,
Status status,
address creator,
IERC20 token,
string memory description,
uint256 expiry
)
{
Bounty storage b = _bounty(id);
return (b.payout, b.status, b.creator, b.token, b.description, b.expiry);
}
function getActiveBounties()
external
view
returns (
uint256[] memory ids,
uint256[] memory payouts,
address[] memory creators,
IERC20[] memory tokens,
string[] memory descriptions,
uint256[] memory expiries
)
{
// First pass: count active bounties
uint256 activeCount = 0;
for (uint256 i = 0; i < nextBountyId; i++) {
Bounty storage b = _bounties[i];
if (b.status == Status.ACTIVE && block.timestamp < b.expiry) {
activeCount++;
}
}
// Initialize arrays with the correct size
ids = new uint256[](activeCount);
payouts = new uint256[](activeCount);
creators = new address[](activeCount);
tokens = new IERC20[](activeCount);
descriptions = new string[](activeCount);
expiries = new uint256[](activeCount);
// Second pass: fill arrays with active bounty data
uint256 index = 0;
for (uint256 i = 0; i < nextBountyId; i++) {
Bounty storage b = _bounties[i];
if (b.status == Status.ACTIVE && block.timestamp < b.expiry) {
ids[index] = i;
payouts[index] = b.payout;
creators[index] = b.creator;
tokens[index] = b.token;
descriptions[index] = b.description;
expiries[index] = b.expiry;
index++;
}
}
return (ids, payouts, creators, tokens, descriptions, expiries);
}
/*──────────── Internal Utils ───────────*/
function _bounty(uint256 id) internal view returns (Bounty storage b) {
if (id >= nextBountyId) revert UnknownBounty();
b = _bounties[id];
}
/*──────────── Version & Gap ───────────*/
function version() external pure returns (string memory) {
return "v1";
}
uint256[100] private __gap;
}