Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 42 additions & 19 deletions contracts/CATFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import "./ContributionAccountingToken.sol";
contract CATFactory is Ownable {
uint256 private _nextTokenId;

// Mapping from owner address to token addresses
mapping(address => address[]) public administerableTokens;
mapping(address => address[]) public mintableTokens;
mapping(address => address[]) public creatorTokens; // Mapping from owner address to token addresses
mapping(address => address[]) public minterTokens;

mapping(address => mapping(address => bool)) public isMinterForCAT;

// Event emitted when a new CAT is created
event CATCreated(address indexed owner, address catAddress, uint256 tokenId);

constructor() Ownable(msg.sender) {}
Expand All @@ -34,6 +34,7 @@ contract CATFactory is Ownable {
) public returns (address) {
ContributionAccountingToken newCAT = new ContributionAccountingToken(
msg.sender,
address(this),
maxSupply,
thresholdSupply,
maxExpansionRate,
Expand All @@ -42,32 +43,54 @@ contract CATFactory is Ownable {
);

address catAddress = address(newCAT);
administerableTokens[msg.sender].push(catAddress);
creatorTokens[msg.sender].push(catAddress);
emit CATCreated(msg.sender, catAddress, _nextTokenId);
_nextTokenId++; // Increment tokenId for the next contract
_nextTokenId++;

return catAddress;
}

/**
* @dev Grants minter role to an address in the CAT contract.
* @param catAddress The address of the CAT contract.
* @param minter The address to grant the minter role.
* @dev Notifies the factory that a minter role has been granted in a CAT contract.
* This function is called by CAT contracts when their admins grant minter roles.
*/
function grantMinterRole(address catAddress, address minter) public onlyOwner {
ContributionAccountingToken(catAddress).grantMinterRole(minter);
mintableTokens[minter].push(catAddress); // Update mintable tokens mapping
function onMinterRoleGranted(address minter) external {
if (!isMinterForCAT[minter][msg.sender]) {
isMinterForCAT[minter][msg.sender] = true;
minterTokens[minter].push(msg.sender);
}
}

/**
* @dev Returns the total number of CATs created.
* @return The total number of CATs.
* @dev Internal function to get a subarray from any address array with pagination.
* @param tokens The storage reference to the array of token addresses. start and end are the indexes of the subarray.
* @return An array of addresses for the specified range.
*/
function totalCATs() public view returns (uint256) {
return _nextTokenId;
function _getSubArray(address[] storage tokens, uint256 start, uint256 end) internal view returns (address[] memory) {
require(start <= end, "Start index must be less than or equal to end index");
require(start <= tokens.length, "Start index out of bounds");

if (end >= tokens.length) {
end = tokens.length;
}

uint256 resultLength = end - start;
address[] memory result = new address[](resultLength);

for (uint256 i = 0; i < resultLength; i++) {
result[i] = tokens[start + i];
}
return result;
}

function getCATAddresses(address _creator) external view returns (address[] memory) {
return administerableTokens[_creator];
function getCreatorCATAddresses(address _creator, uint256 start, uint256 end) external view returns (address[] memory) {
return _getSubArray(creatorTokens[_creator], start, end);
}
function getMinterCATAddresses(address _minter, uint256 start, uint256 end) external view returns (address[] memory) {
return _getSubArray(minterTokens[_minter], start, end);
}
}

function totalCATs() public view returns (uint256) { return _nextTokenId; }
function getCreatorCATCount(address _creator) external view returns (uint256) { return creatorTokens[_creator].length; }
function getMinterCATCount(address _minter) external view returns (uint256) { return minterTokens[_minter].length; }
}
40 changes: 22 additions & 18 deletions contracts/ContributionAccountingToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract ContributionAccountingToken is ERC20, ERC20Permit, AccessControl {
interface ICATFactory {
function grantMinterRole(address catAddress, address minter) external;
function onMinterRoleGranted(address minter) external;
}

contract ContributionAccountingToken is ERC20Burnable, ERC20Permit, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant ADMIN_ROLE = DEFAULT_ADMIN_ROLE;

Expand All @@ -15,16 +21,17 @@ contract ContributionAccountingToken is ERC20, ERC20Permit, AccessControl {
bool public transferRestricted = true;
uint256 public constant clowderFee = 500; // 0.5% fee
address public immutable clowderTreasury = 0x355e559BCA86346B82D58be0460d661DB481E05e; // Address to receive minting fees
address public immutable factory; // Reference to the factory contract that created this CAT

uint256 public lastMintTimestamp;
string public tokenName; // Token name
string public tokenSymbol; // Token symbol

// Constant denominator for fee calculations
uint256 constant denominator = 100000;
uint256 constant denominator = 100000; // Constant denominator for fee calculations

constructor(
address defaultAdmin,
address _factory,
uint256 _maxSupply,
uint256 _thresholdSupply,
uint256 _maxExpansionRate,
Expand All @@ -34,6 +41,7 @@ contract ContributionAccountingToken is ERC20, ERC20Permit, AccessControl {
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
_grantRole(MINTER_ROLE, defaultAdmin);

factory = _factory;
maxSupply = _maxSupply;
thresholdSupply = _thresholdSupply;
maxExpansionRate = _maxExpansionRate;
Expand All @@ -45,31 +53,27 @@ contract ContributionAccountingToken is ERC20, ERC20Permit, AccessControl {
function maxMintableAmount() public view returns (uint256) {
uint256 currentSupply = totalSupply();

// If current supply is less than threshold, return remaining amount to threshold
if (currentSupply < thresholdSupply) {
return thresholdSupply - currentSupply;
}

// Calculate based on expansion rate
uint256 elapsedTime = block.timestamp - lastMintTimestamp;
uint256 maxMintableAmount = (currentSupply * maxExpansionRate * elapsedTime) / (365 days * 100);

// Also check against remaining max supply
uint256 remainingSupply = maxSupply - currentSupply;

return maxMintableAmount < remainingSupply ? maxMintableAmount : remainingSupply;
}

function userAmountAfterFees(uint256 amount) public pure returns (uint256 userAmount, uint256 feeAmount) {
feeAmount = (amount * clowderFee) / denominator;
userAmount = amount - feeAmount;
}

function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
require(amount <= maxMintableAmount(), "Exceeds maximum mintable amount");

// Minting fee calculation
uint256 feeAmount = (amount * clowderFee) / denominator;

// Check against max mintable amount
require(amount + feeAmount <= maxMintableAmount(), "Exceeds maximum mintable amount");
(uint256 userAmount, uint256 feeAmount) = userAmountAfterFees(amount);

// Perform the actual minting
_mint(to, amount);
_mint(to, userAmount);
_mint(clowderTreasury, feeAmount);
lastMintTimestamp = block.timestamp;
}
Expand Down Expand Up @@ -101,10 +105,10 @@ contract ContributionAccountingToken is ERC20, ERC20Permit, AccessControl {
}

function grantMinterRole(address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
grantRole(MINTER_ROLE, account);
_grantRole(MINTER_ROLE, account);
ICATFactory(factory).onMinterRoleGranted(account);
}

function revokeMinterRole(address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
revokeRole(MINTER_ROLE, account);
}
}
}