Skip to content

Commit

Permalink
feat(stamps): non quadratic multiple follower since stamp + top holde…
Browse files Browse the repository at this point in the history
…rs view
  • Loading branch information
NicoAcosta committed Nov 1, 2024
1 parent 3fb2508 commit af1f217
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 7 deletions.
124 changes: 117 additions & 7 deletions src/points/MultipleFollowerSincePoints.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,12 @@ contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints {
uint256 totalPoints;
uint256 stampTotalSupply = stampInfo.stamp.totalSupply();

if (stampTotalSupply == 0) return 0;

IFollowerSinceStamp stamp = stampInfo.stamp;

for (uint256 j = 1; j <= stampTotalSupply; ) {
uint256 followerSince = stampInfo.stamp.followStartTimestamp(j);
uint256 followerSince = stamp.followStartTimestamp(j);
if (followerSince != 0 && followerSince <= timestamp) {
totalPoints += _calculatePointsAtTimestamp(followerSince, timestamp) * stampInfo.multiplier;
}
Expand All @@ -103,16 +107,122 @@ contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints {
return totalPoints;
}

/// @notice Calculates points based on the duration of following using a square root formula
/// @dev Uses a square root calculation for non-linear growth curve:
/// - 1 day (86400 seconds) 1 point
/// - 4 days (345600 seconds) 2 points
/// - Formula: sqrt(durationInSeconds) / sqrt(86400)
/// @notice Calculates points based on the duration of following using a linear formula
/// @dev Uses a linear calculation:
/// - 1 day (86400 seconds) = 1 point
/// - 2 days (172800 seconds) = 2 points
/// - Formula: durationInSeconds / 86400
/// @param followerSince Timestamp when the user started following
/// @param timestamp Current timestamp for calculation
/// @return uint256 Calculated points, scaled to 18 decimal places
function _calculatePointsAtTimestamp(uint256 followerSince, uint256 timestamp) private pure returns (uint256) {
uint256 durationInSeconds = timestamp - followerSince;
return (Math.sqrt(durationInSeconds) * 1e18) / Math.sqrt(86400);
// Convert duration to days with 18 decimal precision
// 1e18 * duration / seconds_per_day
return (durationInSeconds * 1e18) / 86400;
}

/// @notice Gets the top holders between specified indices
/// @param start The starting index (inclusive)
/// @param end The ending index (exclusive)
/// @return addresses Array of addresses sorted by point balance
/// @return balances Array of corresponding point balances
function getTopHolders(
uint256 start,
uint256 end
) external view returns (address[] memory addresses, uint256[] memory balances) {
if (start >= end) revert IndexOutOfBounds();

// Get total unique holders
uint256 totalHolders = 0;
uint256 stampTotalSupply;
address[] memory tempHolders = new address[](_getTotalUniqueHolders());

// Collect unique holders across all stamps
for (uint256 i; i < _stampCount; ) {
stampTotalSupply = _stamps[i].stamp.totalSupply();
for (uint256 j = 1; j <= stampTotalSupply; ) {
address holder = _stamps[i].stamp.ownerOf(j);
if (!_isAddressInArray(tempHolders, holder, totalHolders)) {
tempHolders[totalHolders++] = holder;
}
unchecked {
++j;
}
}
unchecked {
++i;
}
}

// Validate indices
if (end > totalHolders) {
end = totalHolders;
}
uint256 length = end - start;

// Create return arrays
addresses = new address[](length);
balances = new uint256[](length);

// Create temporary arrays for sorting
address[] memory sortedAddresses = new address[](totalHolders);
uint256[] memory sortedBalances = new uint256[](totalHolders);

// Get balances and sort
for (uint256 i = 0; i < totalHolders; ) {
sortedAddresses[i] = tempHolders[i];
sortedBalances[i] = balanceOf(tempHolders[i]);
unchecked {
++i;
}
}

// Sort using insertion sort (efficient for smaller arrays)
for (uint256 i = 1; i < totalHolders; ) {
uint256 j = i;
while (j > 0 && sortedBalances[j - 1] < sortedBalances[j]) {
// Swap balances
(sortedBalances[j], sortedBalances[j - 1]) = (sortedBalances[j - 1], sortedBalances[j]);
// Swap addresses
(sortedAddresses[j], sortedAddresses[j - 1]) = (sortedAddresses[j - 1], sortedAddresses[j]);
unchecked {
--j;
}
}
unchecked {
++i;
}
}

// Copy requested range to return arrays
for (uint256 i = 0; i < length; ) {
addresses[i] = sortedAddresses[start + i];
balances[i] = sortedBalances[start + i];
unchecked {
++i;
}
}
}

/// @dev Helper function to check if address exists in array
function _isAddressInArray(address[] memory array, address addr, uint256 length) private pure returns (bool) {
for (uint256 i = 0; i < length; ) {
if (array[i] == addr) return true;
unchecked {
++i;
}
}
return false;
}

/// @dev Helper function to get total unique holders across all stamps
function _getTotalUniqueHolders() private view returns (uint256 maxHolders) {
for (uint256 i; i < _stampCount; ) {
maxHolders += _stamps[i].stamp.totalSupply();
unchecked {
++i;
}
}
}
}
118 changes: 118 additions & 0 deletions src/points/MultipleFollowerSincePointsQuadratic.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { Points } from "./Points.sol";
import { IFollowerSinceStamp } from "../stamps/interfaces/IFollowerSinceStamp.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { IMultipleFollowerSincePoints } from "./interfaces/IMultipleFollowerSincePoints.sol";

/// @title MultipleFollowerSincePoints - A non-transferable token based on multiple follower durations
/// @notice This contract calculates points based on how long a user has been a follower across multiple accounts
/// @dev Inherits from Points and implements IMultipleFollowerSincePoints, using multiple IFollowerSinceStamp for duration calculation
contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints {
StampInfo[] private _stamps;
uint256 private immutable _stampCount;

constructor(
address[] memory _stampAddresses,
uint256[] memory _multipliers,
string memory _name,
string memory _symbol
) Points(_name, _symbol, 18) {
if (_stampAddresses.length != _multipliers.length) revert ArrayLengthMismatch();
for (uint256 i = 0; i < _stampAddresses.length; ++i) {
_stamps.push(StampInfo(IFollowerSinceStamp(_stampAddresses[i]), _multipliers[i]));
}
_stampCount = _stampAddresses.length;
}

/// @inheritdoc IMultipleFollowerSincePoints
function stamps() external view returns (StampInfo[] memory) {
return _stamps;
}

/// @inheritdoc IMultipleFollowerSincePoints
function stampByIndex(uint256 index) external view returns (StampInfo memory) {
if (index >= _stamps.length) revert IndexOutOfBounds();
return _stamps[index];
}

/// @inheritdoc Points
function _balanceAtTimestamp(address account, uint256 timestamp) internal view override returns (uint256) {
uint256 totalPoints;

for (uint256 i; i < _stampCount; ) {
totalPoints += _calculatePointsForStamp(_stamps[i], account, timestamp);
unchecked {
++i;
}
}
return totalPoints;
}

/// @inheritdoc Points
function _totalSupplyAtTimestamp(uint256 timestamp) internal view override returns (uint256) {
uint256 totalPoints;

for (uint256 i; i < _stampCount; ) {
totalPoints += _calculateTotalPointsForStamp(_stamps[i], timestamp);
unchecked {
++i;
}
}
return totalPoints;
}

/// @dev Calculates points for a specific stamp and account at a given timestamp
/// @param stampInfo The StampInfo struct containing stamp and multiplier information
/// @param account The address of the account to calculate points for
/// @param timestamp The timestamp at which to calculate points
/// @return The calculated points for the stamp and account
function _calculatePointsForStamp(
StampInfo storage stampInfo,
address account,
uint256 timestamp
) private view returns (uint256) {
uint256 followerSince = stampInfo.stamp.getFollowerSinceTimestamp(account);
if (followerSince != 0 && followerSince <= timestamp) {
return _calculatePointsAtTimestamp(followerSince, timestamp) * stampInfo.multiplier;
}
return 0;
}

/// @dev Calculates total points for a specific stamp at a given timestamp
/// @param stampInfo The StampInfo struct containing stamp and multiplier information
/// @param timestamp The timestamp at which to calculate total points
/// @return The calculated total points for the stamp
function _calculateTotalPointsForStamp(
StampInfo storage stampInfo,
uint256 timestamp
) private view returns (uint256) {
uint256 totalPoints;
uint256 stampTotalSupply = stampInfo.stamp.totalSupply();

for (uint256 j = 1; j <= stampTotalSupply; ) {
uint256 followerSince = stampInfo.stamp.followStartTimestamp(j);
if (followerSince != 0 && followerSince <= timestamp) {
totalPoints += _calculatePointsAtTimestamp(followerSince, timestamp) * stampInfo.multiplier;
}
unchecked {
++j;
}
}
return totalPoints;
}

/// @notice Calculates points based on the duration of following using a square root formula
/// @dev Uses a square root calculation for non-linear growth curve:
/// - 1 day (86400 seconds) ≈ 1 point
/// - 4 days (345600 seconds) ≈ 2 points
/// - Formula: sqrt(durationInSeconds) / sqrt(86400)
/// @param followerSince Timestamp when the user started following
/// @param timestamp Current timestamp for calculation
/// @return uint256 Calculated points, scaled to 18 decimal places
function _calculatePointsAtTimestamp(uint256 followerSince, uint256 timestamp) private pure returns (uint256) {
uint256 durationInSeconds = timestamp - followerSince;
return (Math.sqrt(durationInSeconds) * 1e18) / Math.sqrt(86400);
}
}

0 comments on commit af1f217

Please sign in to comment.