// SPDX-License-Identifier: --BCOM--
pragma solidity =0.8.17;
library MerkleProof {
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
)
internal
pure
returns (bool)
{
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
computedHash = computedHash <= proofElement
? keccak256(abi.encodePacked(computedHash, proofElement))
: keccak256(abi.encodePacked(proofElement, computedHash));
}
return computedHash == root;
}
}
// SPDX-License-Identifier: --BCOM--
pragma solidity =0.8.17;
import "./MerkleProof.sol";
import "./VerseHelper.sol";
contract VerseClaimer is VerseHelper {
bytes32 public immutable merkleRoot;
uint256 public immutable createTime;
uint256 immutable minimumTimeFrame;
struct KeeperInfo {
uint256 keeperRate;
uint256 keeperTill;
uint256 keeperInstant;
uint256 keeperPayouts;
}
mapping(address => KeeperInfo) public keeperList;
constructor(
bytes32 _merkleRoot,
uint256 _minimumTimeFrame,
address _verseTokenAddress
)
VerseHelper(_verseTokenAddress)
{
require(
_minimumTimeFrame > 0,
"VerseClaimer: INVALID_TIMEFRAME"
);
require(
_merkleRoot > 0,
"VerseClaimer: INVALID_MERKLE_ROOT"
);
createTime = getNow();
merkleRoot = _merkleRoot;
minimumTimeFrame = _minimumTimeFrame;
}
function enrollRecipient(
uint256 _index,
address _recipient,
uint256 _tokensLocked,
uint256 _tokensOpened,
uint256 _timeFrame,
bytes32[] calldata _merkleProof
)
external
{
_enrollRecipient(
_index,
_recipient,
_tokensLocked,
_tokensOpened,
_timeFrame,
_merkleProof
);
}
function enrollRecipientBulk(
uint256 _index,
address[] calldata _recipient,
uint256[] calldata _tokensLocked,
uint256[] calldata _tokensOpened,
uint256[] calldata _timeFrame,
bytes32[][] calldata _merkleProof
)
external
{
require(
_recipient.length < 10,
"VerseClaimer: TOO_MANY"
);
for (uint256 i = 0; i < _recipient.length; i++) {
_enrollRecipient(
_index + i,
_recipient[i],
_tokensLocked[i],
_tokensOpened[i],
_timeFrame[i],
_merkleProof[i]
);
}
}
function _enrollRecipient(
uint256 _index,
address _recipient,
uint256 _tokensLocked,
uint256 _tokensOpened,
uint256 _timeFrame,
bytes32[] memory _merkleProof
)
private
{
require(
keeperList[_recipient].keeperTill == 0,
"VerseClaimer: RECIPIENT_ALREADY_ENROLLED"
);
bytes32 node = keccak256(
abi.encodePacked(
_index,
_recipient,
_tokensLocked,
_tokensOpened,
_timeFrame
)
);
require(
MerkleProof.verify(
_merkleProof,
merkleRoot,
node
),
"VerseClaimer: INVALID_PROOF"
);
_allocateTokens(
_recipient,
_tokensLocked,
_tokensOpened,
_timeFrame
);
}
function _allocateTokens(
address _recipient,
uint256 _tokensLocked,
uint256 _tokensOpened,
uint256 _timeFrame
)
private
{
require(
_timeFrame >= minimumTimeFrame,
"VerseClaimer: INVALID_TIME_FRAME"
);
totalRequired = totalRequired
+ _tokensOpened
+ _tokensLocked;
keeperList[_recipient].keeperTill = createTime
+ _timeFrame;
keeperList[_recipient].keeperRate = _tokensLocked
/ _timeFrame;
keeperList[_recipient].keeperInstant = _tokensLocked
% _timeFrame
+ _tokensOpened;
_checkVerseBalance(
totalRequired
);
emit recipientEnrolled(
_recipient,
_timeFrame,
_tokensLocked,
_tokensOpened
);
}
function enrollAndScrape(
uint256 _index,
uint256 _tokensLocked,
uint256 _tokensOpened,
uint256 _timeFrame,
bytes32[] calldata _merkleProof
)
external
{
_enrollRecipient(
_index,
msg.sender,
_tokensLocked,
_tokensOpened,
_timeFrame,
_merkleProof
);
_scrapeTokens(
msg.sender
);
}
function scrapeMyTokens()
external
{
_scrapeTokens(
msg.sender
);
}
function _scrapeTokens(
address _recipient
)
private
{
uint256 scrapeAmount = availableBalance(
_recipient
);
keeperList[_recipient].keeperPayouts += scrapeAmount;
_safeVerseScrape(
_recipient,
scrapeAmount
);
emit tokensScraped(
_recipient,
scrapeAmount,
getNow()
);
}
function availableBalance(
address _recipient
)
public
view
returns (uint256 balance)
{
uint256 timeNow = getNow();
uint256 timeMax = keeperList[_recipient].keeperTill;
if (timeMax == 0) return 0;
uint256 timePassed = timeNow > timeMax
? timeMax - createTime
: timeNow - createTime;
balance = keeperList[_recipient].keeperRate
* timePassed
+ keeperList[_recipient].keeperInstant
- keeperList[_recipient].keeperPayouts;
}
function lockedBalance(
address _recipient
)
external
view
returns (uint256 balance)
{
uint256 timeNow = getNow();
uint256 timeRemaining =
keeperList[_recipient].keeperTill > timeNow ?
keeperList[_recipient].keeperTill - timeNow : 0;
balance = keeperList[_recipient].keeperRate
* timeRemaining;
}
}
// SPDX-License-Identifier: --BCOM--
pragma solidity =0.8.17;
contract VerseHelper {
uint256 public totalRequired;
address public immutable verseToken;
event recipientEnrolled(
address indexed recipient,
uint256 timeFrame,
uint256 tokensLocked,
uint256 tokensOpened
);
event tokensScraped(
address indexed scraper,
uint256 scrapedAmount,
uint256 timestamp
);
constructor(
address _verseTokenAddress
) {
if (_verseTokenAddress == address(0x0)) {
revert("VerseHelper: INVALID_VERSE_TOKEN");
}
verseToken = _verseTokenAddress;
}
bytes4 private constant TRANSFER = bytes4(
keccak256(
bytes(
"transfer(address,uint256)"
)
)
);
bytes4 private constant BALANCEOF = bytes4(
keccak256(
bytes(
"balanceOf(address)"
)
)
);
function _safeVerseScrape(
address _to,
uint256 _scrapeAmount
)
internal
{
totalRequired -= _scrapeAmount;
(bool success, bytes memory data) = verseToken.call(
abi.encodeWithSelector(
TRANSFER,
_to,
_scrapeAmount
)
);
require(
success && (
abi.decode(
data, (bool)
)
),
"VerseHelper: TRANSFER_FAILED"
);
}
function _checkVerseBalance(
uint256 _required
)
internal
{
(bool success, bytes memory data) = verseToken.call(
abi.encodeWithSelector(
BALANCEOF,
address(this)
)
);
require(
success && abi.decode(
data, (uint256)
) >= _required,
"VerseHelper: BALANCE_CHECK_FAILED"
);
}
function getNow()
public
view
returns (uint256 time)
{
time = block.timestamp;
}
}
{
"compilationTarget": {
"VerseClaimer.sol": "VerseClaimer"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"bytes32","name":"_merkleRoot","type":"bytes32"},{"internalType":"uint256","name":"_minimumTimeFrame","type":"uint256"},{"internalType":"address","name":"_verseTokenAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"timeFrame","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tokensLocked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tokensOpened","type":"uint256"}],"name":"recipientEnrolled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"scraper","type":"address"},{"indexed":false,"internalType":"uint256","name":"scrapedAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"tokensScraped","type":"event"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"}],"name":"availableBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"createTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_index","type":"uint256"},{"internalType":"uint256","name":"_tokensLocked","type":"uint256"},{"internalType":"uint256","name":"_tokensOpened","type":"uint256"},{"internalType":"uint256","name":"_timeFrame","type":"uint256"},{"internalType":"bytes32[]","name":"_merkleProof","type":"bytes32[]"}],"name":"enrollAndScrape","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_index","type":"uint256"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_tokensLocked","type":"uint256"},{"internalType":"uint256","name":"_tokensOpened","type":"uint256"},{"internalType":"uint256","name":"_timeFrame","type":"uint256"},{"internalType":"bytes32[]","name":"_merkleProof","type":"bytes32[]"}],"name":"enrollRecipient","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_index","type":"uint256"},{"internalType":"address[]","name":"_recipient","type":"address[]"},{"internalType":"uint256[]","name":"_tokensLocked","type":"uint256[]"},{"internalType":"uint256[]","name":"_tokensOpened","type":"uint256[]"},{"internalType":"uint256[]","name":"_timeFrame","type":"uint256[]"},{"internalType":"bytes32[][]","name":"_merkleProof","type":"bytes32[][]"}],"name":"enrollRecipientBulk","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getNow","outputs":[{"internalType":"uint256","name":"time","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"keeperList","outputs":[{"internalType":"uint256","name":"keeperRate","type":"uint256"},{"internalType":"uint256","name":"keeperTill","type":"uint256"},{"internalType":"uint256","name":"keeperInstant","type":"uint256"},{"internalType":"uint256","name":"keeperPayouts","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"}],"name":"lockedBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"merkleRoot","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"scrapeMyTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"totalRequired","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"verseToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]