Contract Name:
DividendTracker
Contract Source Code:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IDividendTracker} from "./IDividendTracker.sol";
import {IterableMapping} from "./IterableMapping.sol";
/**
* @title Official gemlabs dividend tracker contract
* @author The gemlabs crew | https://www.gemlabs.wtf | X: https://twitter.com/gemlabs_wtf | Telegram: https://t.me/gemlabs_wtf
*/
contract DividendTracker is Ownable, IDividendTracker {
uint256 private constant MAGNITUDE = 2 ** 128;
using IterableMapping for IterableMapping.Map;
IterableMapping.Map private tokenHoldersMap;
uint256 public lastProcessedIndex;
uint256 public totalSupply;
uint256 public magnifiedDividendPerShare;
uint256 public totalDividendsDistributed;
uint256 public claimWait;
uint256 public minimumTokenBalanceForDividends;
mapping(address => bool) public excludedFromDividends;
mapping(address => uint256) public lastClaimTimes;
mapping(address => uint256) public tokenHolderBalances;
mapping(address => int256) private magnifiedDividendCorrections;
mapping(address => uint256) private withdrawnDividends;
constructor(uint256 minimumTokenBalanceForDividends_) Ownable(msg.sender) {
claimWait = 3600;
minimumTokenBalanceForDividends = minimumTokenBalanceForDividends_;
}
/**
* @notice Excludes an account from receiving dividends.
* @param account The address of the account to be excluded.
* @dev Only callable by the owner (Dividend Token).
*/
function excludeFromDividends(address account) external onlyOwner {
excludedFromDividends[account] = true;
_setBalance(account, 0);
tokenHoldersMap.remove(account);
emit ExcludeFromDividends(account);
}
/**
* @notice Updates the waiting time between claims.
* @param newClaimWait The new claim wait time in seconds.
* @dev Only callable by the owner (Dividend Token).
*/
function updateClaimWait(uint256 newClaimWait) external onlyOwner {
if (newClaimWait < 3600 || newClaimWait > 86400) {
revert InvalidClaimWait(newClaimWait, 3600, 86400);
}
if (newClaimWait == claimWait) {
revert ClaimWaitAlreadySet(newClaimWait);
}
claimWait = newClaimWait;
emit ClaimWaitUpdated(newClaimWait, claimWait);
}
/**
* @notice Retrieves the number of token holders.
* @return uint256 The number of token holders.
*/
function getNumberOfTokenHolders() external view returns (uint256) {
return tokenHoldersMap.keys.length;
}
/**
* @notice Retrieves the token balance of a specific account.
* @param account The address of the account.
* @return uint256 The balance of the account.
*/
function getTokenHolderBalance(address account) external view returns (uint256) {
return tokenHolderBalances[account];
}
/**
* @notice Retrieves account information for a specific account.
* @param _account The address of the account.
* @return account The address of the account.
* @return index The index of the account in the token holders map.
* @return iterationsUntilProcessed The number of iterations until the account is processed.
* @return withdrawableDividends The amount of dividends that can be withdrawn by the account.
* @return totalDividends The total amount of dividends earned by the account.
* @return lastClaimTime The last time the account claimed dividends.
* @return nextClaimTime The next time the account can claim dividends.
* @return secondsUntilAutoClaimAvailable The number of seconds until the account can automatically claim dividends.
*/
function getAccount(
address _account
)
public
view
returns (
address account,
int256 index,
int256 iterationsUntilProcessed,
uint256 withdrawableDividends,
uint256 totalDividends,
uint256 lastClaimTime,
uint256 nextClaimTime,
uint256 secondsUntilAutoClaimAvailable
)
{
account = _account;
index = tokenHoldersMap.getIndexOfKey(account);
iterationsUntilProcessed = -1;
if (index >= 0) {
if (uint256(index) > lastProcessedIndex) {
iterationsUntilProcessed = index - int256(lastProcessedIndex);
} else {
uint256 processesUntilEndOfArray = tokenHoldersMap.keys.length > lastProcessedIndex
? tokenHoldersMap.keys.length - lastProcessedIndex
: 0;
iterationsUntilProcessed = index + int256(processesUntilEndOfArray);
}
}
withdrawableDividends = withdrawableDividendOf(account);
totalDividends = accumulativeDividendOf(account);
lastClaimTime = lastClaimTimes[account];
nextClaimTime = lastClaimTime > 0 ? lastClaimTime + claimWait : 0;
secondsUntilAutoClaimAvailable = nextClaimTime > block.timestamp ? nextClaimTime - block.timestamp : 0;
}
/**
* @notice Retrieves account information at a specific index.
* @param index The index in the token holders map.
* @return (see getAccount)
*/
function getAccountAtIndex(
uint256 index
) external view returns (address, int256, int256, uint256, uint256, uint256, uint256, uint256) {
if (index >= tokenHoldersMap.size()) {
return (address(0), -1, -1, 0, 0, 0, 0, 0);
}
address account = tokenHoldersMap.getKeyAtIndex(index);
return getAccount(account);
}
function canAutoClaim(uint256 lastClaimTime) private view returns (bool) {
if (lastClaimTime > block.timestamp) {
return false;
}
return block.timestamp - lastClaimTime >= claimWait;
}
/**
* @notice Processes dividend claims for token holders within the specified gas limit.
* @param gas The maximum amount of gas to be used for processing.
* @return (uint256, uint256, uint256) Returns the number of iterations, the number of claims, and the last processed index.
*/
function process(uint256 gas) public returns (uint256, uint256, uint256) {
uint256 numberOfTokenHolders = tokenHoldersMap.keys.length;
if (numberOfTokenHolders == 0) {
return (0, 0, lastProcessedIndex);
}
uint256 _lastProcessedIndex = lastProcessedIndex;
uint256 gasUsed = 0;
uint256 gasLeft = gasleft();
uint256 iterations = 0;
uint256 claims = 0;
while (gasUsed < gas && iterations < numberOfTokenHolders) {
_lastProcessedIndex++;
if (_lastProcessedIndex >= tokenHoldersMap.keys.length) {
_lastProcessedIndex = 0;
}
address account = tokenHoldersMap.keys[_lastProcessedIndex];
if (canAutoClaim(lastClaimTimes[account])) {
if (processAccount(payable(account), true)) {
claims++;
}
}
iterations++;
uint256 newGasLeft = gasleft();
if (gasLeft > newGasLeft) {
gasUsed = gasUsed + gasLeft - newGasLeft;
}
gasLeft = newGasLeft;
}
lastProcessedIndex = _lastProcessedIndex;
return (iterations, claims, lastProcessedIndex);
}
/**
* @notice Processes the account for dividend withdrawal.
* @param account The address of the account to process.
* @param automatic A boolean indicating if the process was triggered automatically.
* @return bool Returns true if dividends were withdrawn successfully, otherwise false.
* @dev Only callable by the owner (Dividend Token).
*/
function processAccount(address payable account, bool automatic) public onlyOwner returns (bool) {
uint256 amount = _withdrawDividendOfUser(account);
if (amount > 0) {
lastClaimTimes[account] = block.timestamp;
emit Claim(account, amount, automatic);
return true;
}
return false;
}
/**
* @notice Distributes the specified amount of dividends to token holders.
* @param amount The amount of dividends to distribute.
* @dev Only callable by the owner (Dividend Token).
*/
function distributeDividends(uint256 amount) public onlyOwner {
if (totalSupply == 0) {
revert NoTotalSupply();
}
if (amount > 0) {
magnifiedDividendPerShare = magnifiedDividendPerShare + ((amount * MAGNITUDE) / totalSupply);
emit DividendsDistributed(msg.sender, amount);
totalDividendsDistributed = totalDividendsDistributed + amount;
}
}
/**
* @notice Withdraws the dividend for the caller.
* @dev Calls the internal function to handle the dividend withdrawal process.
*/
function withdrawDividend() external {
_withdrawDividendOfUser(payable(msg.sender));
}
function _withdrawDividendOfUser(address payable user) internal returns (uint256) {
uint256 _withdrawableDividend = withdrawableDividendOf(user);
if (_withdrawableDividend > 0) {
withdrawnDividends[user] = withdrawnDividends[user] + _withdrawableDividend;
bool success = _safeTransferETH(user, _withdrawableDividend);
emit DividendWithdrawn(user, _withdrawableDividend);
if (!success) {
withdrawnDividends[user] = withdrawnDividends[user] - _withdrawableDividend;
return 0;
}
return _withdrawableDividend;
}
return 0;
}
/**
* @notice View the amount of dividend in wei that an address can withdraw.
* @param _owner The address of a token holder.
* @return The amount of dividend in wei that `_owner` can withdraw.
*/
function withdrawableDividendOf(address _owner) public view returns (uint256) {
return accumulativeDividendOf(_owner) - (withdrawnDividends[_owner]);
}
/**
* @notice View the amount of dividend in wei that an address has withdrawn.
* @param _owner The address of a token holder.
* @return The amount of dividend in wei that `_owner` has withdrawn.
*/
function withdrawnDividendOf(address _owner) public view returns (uint256) {
return withdrawnDividends[_owner];
}
/**
* @notice View the amount of dividend in wei that an address has earned in total.
* @dev accumulativeDividendOf(_owner) = withdrawableDividendOf(_owner) + withdrawnDividendOf(_owner)
* @param _owner The address of a token holder.
* @return The amount of dividend in wei that `_owner` has earned in total.
*/
function accumulativeDividendOf(address _owner) public view returns (uint256) {
uint256 balance = tokenHolderBalances[_owner];
int256 correction = magnifiedDividendCorrections[_owner];
int256 accumulatedDividend = int256(magnifiedDividendPerShare) * int256(balance) + correction;
if (accumulatedDividend < 0) {
return 0;
} else {
return uint256(accumulatedDividend) / MAGNITUDE;
}
}
/**
* @notice Gets the largest holder and their balance.
* @return largestHolder The address of the largest holder.
* @return largestBalance The balance of the largest holder.
*/
function getLargestHolder() external view returns (address largestHolder, uint256 largestBalance) {
uint256 numberOfTokenHolders = tokenHoldersMap.keys.length;
largestBalance = 0;
for (uint256 i = 0; i < numberOfTokenHolders; i++) {
address account = tokenHoldersMap.keys[i];
uint256 balance = tokenHolderBalances[account];
if (balance > largestBalance) {
largestBalance = balance;
largestHolder = account;
}
}
return (largestHolder, largestBalance);
}
/**
* @notice Set the balance of an account and update dividend eligibility.
* @param account The address of the account.
* @param newBalance The new balance for the account.
* @dev Only callable by the owner (Dividend Token).
*/
function setBalance(address payable account, uint256 newBalance) external onlyOwner {
if (excludedFromDividends[account]) {
return;
}
if (newBalance >= minimumTokenBalanceForDividends) {
_setBalance(account, newBalance);
tokenHoldersMap.set(account, newBalance);
} else {
_setBalance(account, 0);
tokenHoldersMap.remove(account);
}
processAccount(account, true);
}
function _setBalance(address account, uint256 newBalance) private {
uint256 currentBalance = tokenHolderBalances[account];
if (newBalance > currentBalance) {
uint256 increaseAmount = newBalance - currentBalance;
totalSupply += increaseAmount;
tokenHolderBalances[account] += increaseAmount;
magnifiedDividendCorrections[account] -= int256(increaseAmount * magnifiedDividendPerShare);
} else if (newBalance < currentBalance) {
uint256 decreaseAmount = currentBalance - newBalance;
totalSupply -= decreaseAmount;
tokenHolderBalances[account] -= decreaseAmount;
magnifiedDividendCorrections[account] += int256(decreaseAmount * magnifiedDividendPerShare);
}
}
function _safeTransferETH(address to, uint256 value) private returns (bool) {
(bool success, ) = to.call{value: value, gas: 30_000}(new bytes(0));
return success;
}
receive() external payable {}
fallback() external payable {}
}
// SPDX-License-Identifier: MIT
// Factory: gemlabs
pragma solidity ^0.8.24;
interface IDividendTracker {
event ExcludeFromDividends(address indexed account);
event ClaimWaitUpdated(uint256 indexed newValue, uint256 indexed oldValue);
event Claim(address indexed account, uint256 amount, bool indexed automatic);
event DividendsDistributed(address indexed from, uint256 weiAmount);
event DividendWithdrawn(address indexed to, uint256 weiAmount);
error InvalidClaimWait(uint256 provided, uint256 min, uint256 max);
error ClaimWaitAlreadySet(uint256 provided);
error NoTotalSupply();
function excludeFromDividends(address account) external;
function updateClaimWait(uint256 newClaimWait) external;
function getNumberOfTokenHolders() external view returns (uint256);
function getTokenHolderBalance(address account) external view returns (uint256);
function getAccount(
address _account
)
external
view
returns (
address account,
int256 index,
int256 iterationsUntilProcessed,
uint256 withdrawableDividends,
uint256 totalDividends,
uint256 lastClaimTime,
uint256 nextClaimTime,
uint256 secondsUntilAutoClaimAvailable
);
function getAccountAtIndex(
uint256 index
) external view returns (address, int256, int256, uint256, uint256, uint256, uint256, uint256);
function process(uint256 gas) external returns (uint256, uint256, uint256);
function processAccount(address payable account, bool automatic) external returns (bool);
function distributeDividends(uint256 amount) external;
function withdrawDividend() external;
function withdrawableDividendOf(address _owner) external view returns (uint256);
function withdrawnDividendOf(address _owner) external view returns (uint256);
function accumulativeDividendOf(address _owner) external view returns (uint256);
function getLargestHolder() external view returns (address largestHolder, uint256 largestBalance);
function setBalance(address payable account, uint256 newBalance) external;
}
// SPDX-License-Identifier: MIT
// Factory: gemlabs
pragma solidity ^0.8.24;
library IterableMapping {
struct Map {
address[] keys;
mapping(address => uint) values;
mapping(address => uint) indexOf;
mapping(address => bool) inserted;
}
function get(Map storage map, address key) internal view returns (uint) {
return map.values[key];
}
function getIndexOfKey(Map storage map, address key) internal view returns (int) {
if (!map.inserted[key]) {
return -1;
}
return int(map.indexOf[key]);
}
function getKeyAtIndex(Map storage map, uint index) internal view returns (address) {
return map.keys[index];
}
function size(Map storage map) internal view returns (uint) {
return map.keys.length;
}
function set(Map storage map, address key, uint val) internal {
if (map.inserted[key]) {
map.values[key] = val;
} else {
map.inserted[key] = true;
map.values[key] = val;
map.indexOf[key] = map.keys.length;
map.keys.push(key);
}
}
function remove(Map storage map, address key) internal {
if (!map.inserted[key]) {
return;
}
delete map.inserted[key];
delete map.values[key];
uint index = map.indexOf[key];
uint lastIndex = map.keys.length - 1;
address lastKey = map.keys[lastIndex];
map.indexOf[lastKey] = index;
delete map.indexOf[key];
map.keys[index] = lastKey;
map.keys.pop();
}
}