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
41 changes: 39 additions & 2 deletions src/CommunityRewardsCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,58 @@ import "./ICommunityRewardsCalculator.sol";
*/
contract CommunityRewardsCalculator is ICommunityRewardsCalculator {

/**
* @notice Legacy method for backwards compatibility
*/
function getRewardsToDistribute(
address /*token*/,
address[] calldata recipients,
IPurchaseTracker purchaseTracker
IPurchaseTracker purchaseTracker,
uint256[] calldata claimedRewards
) external view returns (uint256[] memory) {
require(recipients.length == claimedRewards.length, "Array lengths must match");
uint256[] memory amounts = new uint256[](recipients.length);

// for every purchase or sale made by the recipient, distribute 1 loot token
for (uint i=0; i<recipients.length; i++) {
uint256 totalPurchase = purchaseTracker.getPurchaseCount(recipients[i]);
uint256 totalSales = purchaseTracker.getSalesCount(recipients[i]);
uint256 totalRewards = totalPurchase + totalSales;
amounts[i] = totalRewards;

// Subtract already claimed rewards to prevent double claiming
if (totalRewards > claimedRewards[i]) {
amounts[i] = totalRewards - claimedRewards[i];
} else {
amounts[i] = 0; // No new rewards to claim
}
}

return amounts;
}

/**
* @notice Calculate rewards for a single user based on their purchase/sales activity and checkpoint
* @dev This is the newer, more gas-efficient implementation that works with checkpoints
*/
function calculateUserRewards(
address /*token*/,
address user,
IPurchaseTracker purchaseTracker,
uint256 lastClaimedPurchases,
uint256 lastClaimedSales
) external view returns (uint256) {
// Get current purchase and sales counts
uint256 currentPurchases = purchaseTracker.getPurchaseCount(user);
uint256 currentSales = purchaseTracker.getSalesCount(user);

// Calculate new (unclaimed) purchases and sales
uint256 newPurchases = currentPurchases > lastClaimedPurchases ?
currentPurchases - lastClaimedPurchases : 0;

uint256 newSales = currentSales > lastClaimedSales ?
currentSales - lastClaimedSales : 0;

// Return the total rewards (1 token per purchase/sale)
return newPurchases + newSales;
}
}
80 changes: 67 additions & 13 deletions src/CommunityVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ import "./ICommunityRewardsCalculator.sol";
contract CommunityVault is HasSecurityContext {
using SafeERC20 for IERC20;

// Keeps a count of already distributed rewards
mapping(address => mapping(address => uint256)) public rewardsDistributed;
// Store the last claim checkpoint for each user/token combination
// Instead of storing accumulated rewards, we store the "checkpoint" values
// This allows us to only distribute rewards that have occurred since the last claim
struct ClaimCheckpoint {
uint256 lastClaimedPurchases; // Last claimed purchase count
uint256 lastClaimedSales; // Last claimed sales count
}

// Mapping: token => user => checkpoint
mapping(address => mapping(address => ClaimCheckpoint)) public lastClaimCheckpoints;

// Governance staking contract address
address public governanceVault;
Expand All @@ -30,7 +38,7 @@ contract CommunityVault is HasSecurityContext {
event Deposit(address indexed token, address indexed from, uint256 amount);
event Withdraw(address indexed token, address indexed to, uint256 amount);
event Distribute(address indexed token, address indexed to, uint256 amount);
event RewardDistributed(address indexed token, address indexed recipient, uint256 amount);
event RewardDistributed(address indexed token, address indexed recipient, uint256 amount, uint256 newPurchaseCheckpoint, uint256 newSalesCheckpoint);

/**
* @dev Constructor to initialize the security context.
Expand Down Expand Up @@ -155,15 +163,64 @@ contract CommunityVault is HasSecurityContext {
return IERC20(token).balanceOf(address(this));
}

function _distributeRewards(address token, address[] memory recipients) internal {
if (address(rewardsCalculator) != address(0) && address(purchaseTracker) != address(0)) {
/**
* @dev For backward compatibility - returns the total rewards distributed to a recipient
* @param token The token address
* @param recipient The recipient address
* @return The sum of all previous rewards
*/
function rewardsDistributed(address token, address recipient) external view returns (uint256) {
if (address(purchaseTracker) == address(0)) return 0;

ClaimCheckpoint memory checkpoint = lastClaimCheckpoints[token][recipient];
return checkpoint.lastClaimedPurchases + checkpoint.lastClaimedSales;
}

//get rewards to distribute
uint256[] memory amounts = rewardsCalculator.getRewardsToDistribute(
token, recipients, IPurchaseTracker(purchaseTracker)
function _distributeRewards(address token, address[] memory recipients) internal {
if (address(rewardsCalculator) == address(0) || address(purchaseTracker) == address(0)) {
return;
}

for (uint256 i = 0; i < recipients.length; i++) {
address recipient = recipients[i];

// Get the current checkpoint for this user/token
ClaimCheckpoint memory checkpoint = lastClaimCheckpoints[token][recipient];

// Get current totals from purchase tracker
uint256 currentPurchases = IPurchaseTracker(purchaseTracker).getPurchaseCount(recipient);
uint256 currentSales = IPurchaseTracker(purchaseTracker).getSalesCount(recipient);

// Calculate reward using the calculator
uint256 totalReward = rewardsCalculator.calculateUserRewards(
token,
recipient,
IPurchaseTracker(purchaseTracker),
checkpoint.lastClaimedPurchases,
checkpoint.lastClaimedSales
);

_distribute(token, recipients, amounts);

if (totalReward > 0) {
// Update the checkpoint before distribution to prevent reentrancy
lastClaimCheckpoints[token][recipient] = ClaimCheckpoint(
currentPurchases,
currentSales
);

// Distribute rewards
if (token == address(0)) {
// ETH distribution
require(address(this).balance >= totalReward, "Insufficient ETH balance");
(bool success, ) = recipient.call{value: totalReward}("");
require(success, "ETH transfer failed");
} else {
// ERC20 distribution
require(IERC20(token).balanceOf(address(this)) >= totalReward, "Insufficient token balance");
IERC20(token).safeTransfer(recipient, totalReward);
}

emit RewardDistributed(token, recipient, totalReward, currentPurchases, currentSales);
}
}
}

Expand All @@ -186,9 +243,6 @@ contract CommunityVault is HasSecurityContext {
IERC20(token).safeTransfer(recipients[i], amounts[i]);
}

// record the distribution
rewardsDistributed[token][recipients[i]] += amounts[i];

emit Distribute(token, recipients[i], amounts[i]);
}
}
Expand Down
26 changes: 24 additions & 2 deletions src/ICommunityRewardsCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,31 @@ import "@hamza-escrow/IPurchaseTracker.sol";
* distributed through the CommunityVault.
*/
interface ICommunityRewardsCalculator {
/**
* @notice Legacy function for backwards compatibility
* @dev This function will be kept for compatibility but not used in newer versions
*/
function getRewardsToDistribute(
address token,
address[] calldata recipients,
IPurchaseTracker purchaseTracker
) external returns (uint256[] memory);
IPurchaseTracker purchaseTracker,
uint256[] calldata claimedRewards
) external view returns (uint256[] memory);

/**
* @notice Calculate rewards for a specific user based on a checkpoint
* @param token The token being distributed
* @param user The user to calculate rewards for
* @param purchaseTracker The tracker for purchase data
* @param lastClaimedPurchases The number of purchases already claimed
* @param lastClaimedSales The number of sales already claimed
* @return The amount of rewards to distribute
*/
function calculateUserRewards(
address token,
address user,
IPurchaseTracker purchaseTracker,
uint256 lastClaimedPurchases,
uint256 lastClaimedSales
) external view returns (uint256);
}
Loading