// SPDX-License-Identifier: MIT pragma solidity 0.8.30; interface IERC20 { function balanceOf(address account) external view returns (uint256); } library ExactToken { function invoke(address token, bytes memory data) private { require(token.code.length > 0, "TOKEN_NOT_CONTRACT"); (bool success, bytes memory result) = token.call(data); require(success && (result.length == 0 || abi.decode(result, (bool))), "TOKEN_CALL_FAILED"); } function pull(address token, address from, uint256 amount) internal { uint256 beforeSender = IERC20(token).balanceOf(from); uint256 beforeReceiver = IERC20(token).balanceOf(address(this)); invoke(token, abi.encodeWithSignature("transferFrom(address,address,uint256)", from, address(this), amount)); uint256 afterSender = IERC20(token).balanceOf(from); uint256 afterReceiver = IERC20(token).balanceOf(address(this)); require(beforeSender >= amount && afterSender == beforeSender - amount, "NON_STANDARD_SENDER_BALANCE"); require(afterReceiver >= beforeReceiver && afterReceiver - beforeReceiver == amount, "TRANSFER_TAX_NOT_SUPPORTED"); } function pay(address token, address recipient, uint256 amount) internal { uint256 beforeSender = IERC20(token).balanceOf(address(this)); uint256 beforeReceiver = IERC20(token).balanceOf(recipient); invoke(token, abi.encodeWithSignature("transfer(address,uint256)", recipient, amount)); uint256 afterSender = IERC20(token).balanceOf(address(this)); uint256 afterReceiver = IERC20(token).balanceOf(recipient); require(beforeSender >= amount && afterSender == beforeSender - amount, "NON_STANDARD_SENDER_BALANCE"); require(afterReceiver >= beforeReceiver && afterReceiver - beforeReceiver == amount, "TRANSFER_TAX_NOT_SUPPORTED"); } } contract RewardCampaign { address public owner; address public token; uint256 public deadline; uint256 public total; uint256 public paid; uint256 public recipientCount; bool public direct; bool public funded; bool public reclaimed; bytes32 public manifestHash; mapping(address => uint256) public allocation; mapping(address => uint256) public owed; uint256 private entered = 1; event Registered(address indexed recipient, uint256 amount); event Funded(uint256 amount); event RewardPaid(address indexed recipient, uint256 amount); event Recovered(uint256 amount); modifier onlyOwner() { require(msg.sender == owner, "OWNER_ONLY"); _; } modifier nonReentrant() { require(entered == 1, "REENTRANCY"); entered = 2; _; entered = 1; } modifier live() { require(funded, "NOT_FUNDED"); require(!reclaimed, "RECLAIMED"); require(block.timestamp < deadline, "CAMPAIGN_EXPIRED"); _; } constructor(address rewardToken, uint256 expiresAt, bool directDistribution, bytes32 campaignManifestHash) { require(rewardToken.code.length > 0, "TOKEN_NOT_CONTRACT"); require(expiresAt > block.timestamp && expiresAt <= block.timestamp + 366 days, "INVALID_DEADLINE"); owner = msg.sender; token = rewardToken; deadline = expiresAt; direct = directDistribution; manifestHash = campaignManifestHash; } function register(address[] calldata recipients, uint256[] calldata amounts) external onlyOwner nonReentrant { require(!funded, "ALREADY_FUNDED"); require(block.timestamp < deadline, "CAMPAIGN_EXPIRED"); require(recipients.length > 0 && recipients.length <= 100 && recipients.length == amounts.length, "INVALID_BATCH"); for (uint256 i = 0; i < recipients.length; i++) { address recipient = recipients[i]; uint256 amount = amounts[i]; require(recipient != address(0) && recipient != address(this) && recipient != owner && recipient != token && recipient != address(0xdead), "INVALID_RECIPIENT"); require(amount > 0, "ZERO_AMOUNT"); if (allocation[recipient] != 0) { require(allocation[recipient] == amount, "ALLOCATION_MISMATCH"); continue; } allocation[recipient] = amount; owed[recipient] = amount; total += amount; recipientCount++; emit Registered(recipient, amount); } } function fund() external onlyOwner nonReentrant { require(!funded, "ALREADY_FUNDED"); require(!reclaimed, "RECLAIMED"); require(block.timestamp < deadline, "CAMPAIGN_EXPIRED"); require(total > 0, "EMPTY_CAMPAIGN"); funded = true; ExactToken.pull(token, owner, total); emit Funded(total); } function pendingIn(address[] calldata recipients) external view returns (uint256 amount) { for (uint256 i = 0; i < recipients.length; i++) amount += owed[recipients[i]]; } function distribute(address[] calldata recipients) external onlyOwner nonReentrant live { require(direct, "CLAIM_MODE"); require(recipients.length > 0 && recipients.length <= 100, "INVALID_BATCH"); for (uint256 i = 0; i < recipients.length; i++) { address recipient = recipients[i]; uint256 amount = owed[recipient]; if (amount == 0) continue; owed[recipient] = 0; paid += amount; ExactToken.pay(token, recipient, amount); emit RewardPaid(recipient, amount); } } function claim() external nonReentrant live { require(!direct, "DIRECT_MODE"); uint256 amount = owed[msg.sender]; require(amount > 0, "NO_REWARD"); owed[msg.sender] = 0; paid += amount; ExactToken.pay(token, msg.sender, amount); emit RewardPaid(msg.sender, amount); } function recover() external onlyOwner nonReentrant { require(block.timestamp >= deadline, "NOT_EXPIRED"); require(!reclaimed, "ALREADY_RECOVERED"); reclaimed = true; uint256 balance = IERC20(token).balanceOf(address(this)); if (balance > 0) ExactToken.pay(token, owner, balance); emit Recovered(balance); } }