// SPDX-License-Identifier: -- WISE --
pragma solidity =0.8.25;
interface IERC20 {
function transfer(
address _recipient,
uint256 _amount
)
external
returns (bool);
function transferFrom(
address _sender,
address _recipient,
uint256 _amount
)
external
returns (bool);
function balanceOf(
address _account
)
external
view
returns (uint256);
}
// SPDX-License-Identifier: -- WISE --
pragma solidity =0.8.25;
library MerkleProof {
function verify(
bytes32[] memory _proof,
bytes32 _root,
bytes32 _leaf
)
internal
pure
returns (bool)
{
uint256 i;
uint256 l = _proof.length;
bytes32 computedHash = _leaf;
while (i < l) {
bytes32 proofElement = _proof[i];
computedHash <= proofElement
? computedHash = keccak256(abi.encodePacked(computedHash, proofElement))
: computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
unchecked {
++i;
}
}
return computedHash == _root;
}
}
// SPDX-License-Identifier: -- WISE --
pragma solidity =0.8.25;
import "./IERC20.sol";
import "./MerkleProof.sol";
error InvalidClaim();
error InvalidAmount();
error AlreadyCreated();
error AlreadyClaimed();
/**
* @title Wise Merkle Rewards
* @author Vitally Marinchenko
*/
contract WiseRewards {
uint256 public rewardsCount;
uint256 public totalRequired;
uint256 public totalCollected;
uint256 public latestRootAdded;
address public masterAccount;
address public workerAccount;
struct Reward {
bytes32 root;
uint256 total;
uint256 claimed;
uint256 created;
}
IERC20 public immutable REWARD_TOKEN;
mapping(uint256 => string) public ipfsData;
mapping(bytes32 => Reward) public rewardsData;
mapping(bytes32 => mapping(address => bool)) public hasClaimed;
modifier onlyMaster() {
require(
msg.sender == masterAccount,
"WiseRewards: INVALID_MASTER"
);
_;
}
modifier onlyWorker() {
require(
msg.sender == workerAccount,
"WiseRewards: INVALID_WORKER"
);
_;
}
event Deposit(
address indexed account,
uint256 amount
);
event Withdraw(
address indexed account,
uint256 amount
);
event NewRewards(
bytes32 indexed hash,
address indexed master,
string indexed ipfsAddress,
uint256 total
);
event Claimed(
uint256 indexed index,
address indexed account,
uint256 amount
);
event Thanks(
address indexed account,
uint256 indexed amount
);
receive()
external
payable
{
payable(masterAccount).transfer(
msg.value
);
emit Thanks(
msg.sender,
msg.value
);
}
constructor(
address _rewardToken,
address _masterAccount,
address _workerAccount
) {
REWARD_TOKEN = IERC20(
_rewardToken
);
masterAccount = _masterAccount;
workerAccount = _workerAccount;
}
function createRewards(
bytes32 _root,
uint256 _total,
string calldata _ipfsAddress
)
external
onlyMaster
{
if (_total == 0) {
revert InvalidAmount();
}
bytes32 ipfsHash = getHash(
_ipfsAddress
);
if (rewardsData[ipfsHash].total > 0) {
revert AlreadyCreated();
}
rewardsData[ipfsHash] = Reward({
root: _root,
total: _total,
created: block.timestamp,
claimed: 0
});
rewardsCount =
rewardsCount + 1;
ipfsData[rewardsCount] = _ipfsAddress;
totalRequired =
totalRequired + _total;
latestRootAdded = block.timestamp;
emit NewRewards(
_root,
masterAccount,
_ipfsAddress,
_total
);
}
function getHash(
string calldata _ipfsAddress
)
public
pure
returns (bytes32)
{
return keccak256(
abi.encodePacked(
_ipfsAddress
)
);
}
function isClaimed(
bytes32 _hash,
address _account
)
public
view
returns (bool)
{
return hasClaimed[_hash][_account];
}
function isClaimedBulk(
bytes32[] calldata _hash,
address _account
)
external
view
returns (bool[] memory)
{
uint256 i;
uint256 l = _hash.length;
bool[] memory result = new bool[](l);
while (i < l) {
result[i] = isClaimed(
_hash[i],
_account
);
unchecked {
++i;
}
}
return result;
}
function getClaim(
bytes32 _hash,
uint256 _index,
uint256 _amount,
bytes32[] calldata _merkleProof
)
external
{
_doClaim(
_hash,
_index,
_amount,
msg.sender,
_merkleProof
);
}
function getClaimBulk(
bytes32[] calldata _hash,
uint256[] calldata _index,
uint256[] calldata _amount,
bytes32[][] calldata _merkleProof
)
external
{
uint256 i;
uint256 l = _hash.length;
while (i < l) {
_doClaim(
_hash[i],
_index[i],
_amount[i],
msg.sender,
_merkleProof[i]
);
unchecked {
++i;
}
}
}
function giveClaim(
bytes32 _hash,
uint256 _index,
uint256 _amount,
address _account,
bytes32[] calldata _merkleProof
)
external
onlyWorker
{
_doClaim(
_hash,
_index,
_amount,
_account,
_merkleProof
);
}
function giveClaimBulk(
bytes32[] calldata _hash,
uint256[] calldata _index,
uint256[] calldata _amount,
address[] calldata _account,
bytes32[][] calldata _merkleProof
)
external
onlyWorker
{
uint256 i;
uint256 l = _hash.length;
while (i < l) {
_doClaim(
_hash[i],
_index[i],
_amount[i],
_account[i],
_merkleProof[i]
);
unchecked {
++i;
}
}
}
function _doClaim(
bytes32 _hash,
uint256 _index,
uint256 _amount,
address _account,
bytes32[] calldata _merkleProof
)
private
{
if (isClaimed(_hash, _account) == true) {
revert AlreadyClaimed();
}
bytes32 node = keccak256(
abi.encodePacked(
_index,
_account,
_amount
)
);
require(
MerkleProof.verify(
_merkleProof,
rewardsData[_hash].root,
node
),
"WiseRewards: INVALID_PROOF"
);
totalCollected =
totalCollected + _amount;
rewardsData[_hash].claimed =
rewardsData[_hash].claimed + _amount;
if (rewardsData[_hash].claimed > rewardsData[_hash].total) {
revert InvalidClaim();
}
_setClaimed(
_hash,
_account
);
REWARD_TOKEN.transfer(
_account,
_amount
);
emit Claimed(
_index,
_account,
_amount
);
}
function _setClaimed(
bytes32 _hash,
address _account
)
private
{
hasClaimed[_hash][_account] = true;
}
function donateFunds(
uint256 _donationAmount
)
external
{
if (_donationAmount == 0) {
revert InvalidAmount();
}
REWARD_TOKEN.transferFrom(
msg.sender,
address(this),
_donationAmount
);
emit Deposit(
msg.sender,
_donationAmount
);
}
function withdrawEth(
uint256 _amount
)
external
onlyMaster
{
payable(
masterAccount
).transfer(
_amount
);
emit Withdraw(
masterAccount,
_amount
);
}
function changeMaster(
address _newMaster
)
external
onlyMaster
{
masterAccount = _newMaster;
}
function changeWorker(
address _newWorker
)
external
onlyMaster
{
workerAccount = _newWorker;
}
function getBalance()
public
view
returns (uint256)
{
return REWARD_TOKEN.balanceOf(
address(this)
);
}
function showRemaining(
bytes32 _hash
)
public
view
returns (uint256)
{
return rewardsData[_hash].total - rewardsData[_hash].claimed;
}
function showExcess(
bytes32 _hash
)
external
view
returns (int256)
{
return int256(getBalance()) - int256(showRemaining(_hash));
}
function showRemaining()
public
view
returns (uint256)
{
return totalRequired - totalCollected;
}
function showExcess()
external
view
returns (int256)
{
return int256(getBalance()) - int256(showRemaining());
}
function rescueTokens(
address _token,
address _target,
uint256 _amount
)
external
onlyMaster
{
IERC20(_token).transfer(
_target,
_amount
);
}
}
{
"compilationTarget": {
"WiseRewards.sol": "WiseRewards"
},
"evmVersion": "cancun",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 99999
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_rewardToken","type":"address"},{"internalType":"address","name":"_masterAccount","type":"address"},{"internalType":"address","name":"_workerAccount","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AlreadyClaimed","type":"error"},{"inputs":[],"name":"AlreadyCreated","type":"error"},{"inputs":[],"name":"InvalidAmount","type":"error"},{"inputs":[],"name":"InvalidClaim","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"index","type":"uint256"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Claimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"hash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"master","type":"address"},{"indexed":true,"internalType":"string","name":"ipfsAddress","type":"string"},{"indexed":false,"internalType":"uint256","name":"total","type":"uint256"}],"name":"NewRewards","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Thanks","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[],"name":"REWARD_TOKEN","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newMaster","type":"address"}],"name":"changeMaster","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newWorker","type":"address"}],"name":"changeWorker","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_root","type":"bytes32"},{"internalType":"uint256","name":"_total","type":"uint256"},{"internalType":"string","name":"_ipfsAddress","type":"string"}],"name":"createRewards","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_donationAmount","type":"uint256"}],"name":"donateFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"uint256","name":"_index","type":"uint256"},{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"bytes32[]","name":"_merkleProof","type":"bytes32[]"}],"name":"getClaim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"_hash","type":"bytes32[]"},{"internalType":"uint256[]","name":"_index","type":"uint256[]"},{"internalType":"uint256[]","name":"_amount","type":"uint256[]"},{"internalType":"bytes32[][]","name":"_merkleProof","type":"bytes32[][]"}],"name":"getClaimBulk","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_ipfsAddress","type":"string"}],"name":"getHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"uint256","name":"_index","type":"uint256"},{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"address","name":"_account","type":"address"},{"internalType":"bytes32[]","name":"_merkleProof","type":"bytes32[]"}],"name":"giveClaim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"_hash","type":"bytes32[]"},{"internalType":"uint256[]","name":"_index","type":"uint256[]"},{"internalType":"uint256[]","name":"_amount","type":"uint256[]"},{"internalType":"address[]","name":"_account","type":"address[]"},{"internalType":"bytes32[][]","name":"_merkleProof","type":"bytes32[][]"}],"name":"giveClaimBulk","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"hasClaimed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"ipfsData","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"address","name":"_account","type":"address"}],"name":"isClaimed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"_hash","type":"bytes32[]"},{"internalType":"address","name":"_account","type":"address"}],"name":"isClaimedBulk","outputs":[{"internalType":"bool[]","name":"","type":"bool[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestRootAdded","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"masterAccount","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_target","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"rescueTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rewardsCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"rewardsData","outputs":[{"internalType":"bytes32","name":"root","type":"bytes32"},{"internalType":"uint256","name":"total","type":"uint256"},{"internalType":"uint256","name":"claimed","type":"uint256"},{"internalType":"uint256","name":"created","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_hash","type":"bytes32"}],"name":"showExcess","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"showExcess","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"showRemaining","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_hash","type":"bytes32"}],"name":"showRemaining","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalCollected","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalRequired","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withdrawEth","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"workerAccount","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}]