Repository navigation
Expand file tree
/
Copy pathDistributor.sol
More file actions
124 lines (103 loc) · 4.38 KB
/
Copy pathDistributor.sol
File metadata and controls
124 lines (103 loc) · 4.38 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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title Distributor
* @notice A contract for distributing rewards to users based on Merkle proofs.
* @dev Implements fee collection and reentrancy protection.
*/
contract Distributor is Ownable, ReentrancyGuard {
address public constant PLATFORM_WALLET = 0xcf74BbBDDBB7ed5129a715F20d1cC34Fe1124fe4;
uint256 public constant PLATFORM_FEE_BPS = 1500; // 15% (1500 / 10000)
struct Campaign {
address creator;
bytes32 merkleRoot;
uint256 totalBudget;
uint256 remainingBudget;
bool isActive;
mapping(address => bool) hasClaimed;
}
mapping(uint256 => Campaign) public campaigns;
uint256 public nextCampaignId = 1; // Start IDs at 1
event CampaignCreated(uint256 indexed id, address indexed creator, uint256 totalBudget, uint256 netBudget);
event RewardClaimed(uint256 indexed campaignId, address indexed claimer, uint256 amount);
error NoBudgetProvided();
error PlatformFeeTransferFailed();
error OnlyCreator();
error CampaignNotActive();
error AlreadyClaimed();
error InvalidMerkleProof();
error InsufficientFunds();
error RewardTransferFailed();
constructor() Ownable(msg.sender) {}
/**
* @dev Create a new campaign. msg.value is the total budget.
* Merkle Root can be set 0x0 initially and updated later.
*/
function createCampaign(bytes32 _merkleRoot) external payable nonReentrant {
if (msg.value == 0) revert NoBudgetProvided();
uint256 fee = (msg.value * PLATFORM_FEE_BPS) / 10000;
uint256 netBudget = msg.value - fee;
(bool sent, ) = payable(PLATFORM_WALLET).call{value: fee}("");
if (!sent) revert PlatformFeeTransferFailed();
Campaign storage c = campaigns[nextCampaignId];
c.creator = msg.sender;
c.merkleRoot = _merkleRoot;
c.totalBudget = msg.value;
c.remainingBudget = netBudget;
c.isActive = true;
emit CampaignCreated(nextCampaignId, msg.sender, msg.value, netBudget);
nextCampaignId++;
}
/**
* @dev Set the Merkle Root after campaign ends (for dynamic participation).
* Only the creator can set this.
*/
function setMerkleRoot(uint256 _campaignId, bytes32 _merkleRoot) external {
Campaign storage c = campaigns[_campaignId];
if (msg.sender != c.creator) revert OnlyCreator();
c.merkleRoot = _merkleRoot;
}
/**
* @dev Claim a reward from a specific campaign.
* @param _campaignId The ID of the campaign.
* @param _amount The amount to claim (wei).
* @param _proof The Merkle Proof confirming the claim.
*/
function claim(uint256 _campaignId, uint256 _amount, bytes32[] calldata _proof) external nonReentrant {
Campaign storage c = campaigns[_campaignId];
if (!c.isActive) revert CampaignNotActive();
if (c.hasClaimed[msg.sender]) revert AlreadyClaimed();
// Verify Merkle Proof (Leaf: keccak256(abi.encodePacked(address, amount)))
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _amount));
if (!MerkleProof.verify(_proof, c.merkleRoot, leaf)) revert InvalidMerkleProof();
if (c.remainingBudget < _amount) revert InsufficientFunds();
// Update state before transfer
c.hasClaimed[msg.sender] = true;
c.remainingBudget -= _amount;
(bool sent, ) = payable(msg.sender).call{value: _amount}("");
if (!sent) revert RewardTransferFailed();
emit RewardClaimed(_campaignId, msg.sender, _amount);
}
/**
* @dev Get campaign details (helper for frontend)
*/
function getCampaign(uint256 _id) external view returns (
address creator,
bytes32 merkleRoot,
uint256 totalBudget,
uint256 remainingBudget,
bool isActive
) {
Campaign storage c = campaigns[_id];
return (c.creator, c.merkleRoot, c.totalBudget, c.remainingBudget, c.isActive);
}
/**
* @dev Check if a user has claimed from a campaign
*/
function hasClaimed(uint256 _campaignId, address _user) external view returns (bool) {
return campaigns[_campaignId].hasClaimed[_user];
}
}