From af1f217af0001f3f6c0c1236bc36148513e2f137 Mon Sep 17 00:00:00 2001 From: "nicoacosta.eth" Date: Fri, 1 Nov 2024 00:33:41 -0300 Subject: [PATCH] feat(stamps): non quadratic multiple follower since stamp + top holders view --- src/points/MultipleFollowerSincePoints.sol | 124 +++++++++++++++++- .../MultipleFollowerSincePointsQuadratic.sol | 118 +++++++++++++++++ 2 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 src/points/MultipleFollowerSincePointsQuadratic.sol diff --git a/src/points/MultipleFollowerSincePoints.sol b/src/points/MultipleFollowerSincePoints.sol index 460b810..d68b1a8 100644 --- a/src/points/MultipleFollowerSincePoints.sol +++ b/src/points/MultipleFollowerSincePoints.sol @@ -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; } @@ -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; + } + } } } diff --git a/src/points/MultipleFollowerSincePointsQuadratic.sol b/src/points/MultipleFollowerSincePointsQuadratic.sol new file mode 100644 index 0000000..460b810 --- /dev/null +++ b/src/points/MultipleFollowerSincePointsQuadratic.sol @@ -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); + } +}