// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
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);
function approve(address spender, uint256 amount) external returns (bool);
}
contract TokenSplitter {
address[] public angels;
uint256 public angelCount;
address public owner;
// Mapping to track the amount claimed by each angel for each token type
mapping(address => mapping(address => uint256)) public amountClaimed; // angel => token => claimed amount
mapping(address => mapping(address => uint256)) public amountClaimableTotal; // token => angel => total allocated amount
mapping(address => bool) public isClaimActive; // token => active claim flag
// Modifier to restrict access to the contract owner (multisig)
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
// Modifier to restrict access to angels
modifier onlyAngel() {
bool isAngel = false;
for (uint i = 0; i < angels.length; i++) {
if (angels[i] == msg.sender) {
isAngel = true;
break;
}
}
require(isAngel, "Not an angel");
_;
}
constructor(address[] memory _angels, address _owner) {
angels = _angels;
angelCount = _angels.length;
owner = _owner;
}
// Function for the owner (multisig) to create or update the token claim for all angels
function createTokenClaim(address tokenAddress, uint256 totalAmount) external onlyOwner {
require(totalAmount > 0, "Amount must be greater than zero");
require(angelCount > 0, "No angels available");
// Calculate each angel's share
uint256 amountPerAngel = totalAmount / angelCount;
// Set the claim as active for the token type
isClaimActive[tokenAddress] = true;
// Update the total claimable amount for each angel and token
for (uint i = 0; i < angels.length; i++) {
address angel = angels[i];
amountClaimableTotal[tokenAddress][angel] += amountPerAngel;
}
}
// Function for an angel to claim their tokens from the multisig contract
function claim(address tokenAddress) external onlyAngel {
uint256 claimableAmount = 0;
// Check if the claim for this token is active
require(isClaimActive[tokenAddress], "Claim is not active for this token");
// Calculate the claimable amount for the calling angel
claimableAmount = amountClaimableTotal[tokenAddress][msg.sender] - amountClaimed[msg.sender][tokenAddress];
require(claimableAmount > 0, "No tokens available for claim");
// Update the claimed amount for the calling angel
amountClaimed[msg.sender][tokenAddress] += claimableAmount;
// Transfer the claimed tokens from the owner (multisig) to the calling angel
IERC20(tokenAddress).transferFrom(owner, msg.sender, claimableAmount);
}
// Function to cancel a token claim (can only be done by the owner)
function cancelTokenClaim(address tokenAddress) external onlyOwner {
// Set the claim flag to false to deactivate it for the specified token
isClaimActive[tokenAddress] = false;
}
// Function to get the claimable amount for a specific angel and token type
function claimableTokensForAngel(address angel, address tokenAddress) external view returns (uint256) {
require(angel != address(0), "Invalid angel address");
require(tokenAddress != address(0), "Invalid token address");
uint256 totalClaimable = amountClaimableTotal[tokenAddress][angel];
uint256 alreadyClaimed = amountClaimed[angel][tokenAddress];
// Return the difference, which is the claimable amount
return totalClaimable - alreadyClaimed;
}
// Function to get the remaining total claimable amount for a specific token type
function remainingClaimableTokens(address tokenAddress) external view returns (uint256) {
require(tokenAddress != address(0), "Invalid token address");
uint256 totalRemaining = 0;
// Loop through each angel to calculate the remaining claimable tokens for the token type
for (uint i = 0; i < angels.length; i++) {
address angel = angels[i];
uint256 totalClaimable = amountClaimableTotal[tokenAddress][angel];
uint256 alreadyClaimed = amountClaimed[angel][tokenAddress];
// Add the remaining claimable tokens for this angel to the total remaining
totalRemaining += (totalClaimable - alreadyClaimed);
}
return totalRemaining;
}
// Function to execute arbitrary code on another contract (owner-locked)
function executeCall(address target, bytes calldata data, bool isDelegateCall) external onlyOwner returns (bytes memory) {
require(target != address(0), "Target address is zero");
// If isDelegateCall is true, perform a delegatecall, else perform a regular call
if (isDelegateCall) {
// Using delegatecall to call the target contract with the same context (msg.sender, msg.value)
(bool success, bytes memory returnData) = target.delegatecall(data);
require(success, "Delegatecall failed");
return returnData;
} else {
// Using call to invoke the target contract (msg.sender will be the caller)
(bool success, bytes memory returnData) = target.call(data);
require(success, "Call failed");
return returnData;
}
}
}
{
"compilationTarget": {
"OCA_token_splitter.sol": "TokenSplitter"
},
"evmVersion": "cancun",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address[]","name":"_angels","type":"address[]"},{"internalType":"address","name":"_owner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"amountClaimableTotal","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"amountClaimed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"angelCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"angels","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"}],"name":"cancelTokenClaim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"}],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"angel","type":"address"},{"internalType":"address","name":"tokenAddress","type":"address"}],"name":"claimableTokensForAngel","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"totalAmount","type":"uint256"}],"name":"createTokenClaim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"bool","name":"isDelegateCall","type":"bool"}],"name":"executeCall","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isClaimActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"}],"name":"remainingClaimableTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]