Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@paulstinchcombe/kaminftlibrary) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
KamiNFTLibrary
A comprehensive Solidity library providing advanced features for ERC721 and ERC1155 contracts, including programmable royalties, ERC20 payment integration, platform commission system, time-based rental system, and mandatory royalty enforcement.
Features
🎯 Core Features
- Programmable Royalties: Multiple receivers with configurable percentages (up to 30%)
- ERC20 Payment Integration: Native support for ERC20 tokens as payment
- Platform Commission System: Built-in platform fee collection (up to 20%)
- Time-based Rental System: NFT rental functionality with automatic expiration
- Mandatory Royalty Enforcement: Ensures royalties are paid before transfers
- Role-based Access Control: Secure permission management
- Storage Slot Pattern: Proxy-compatible storage using dedicated slots
🔧 Advanced Capabilities
- Token-specific Royalties: Set different royalty structures per token
- Dual Royalty Types: Separate mint and transfer royalty configurations
- Rental Management: Extend, end, and track rental periods
- Transfer Validation: Comprehensive transfer validation with royalty checks
- Event Tracking: Extensive event emission for transparency
- Rounding & Dust Handling: Robust handling of rounding errors and dust in royalty splits
- ERC721/ERC1155 Support: Compatible with both token standards
- Gas Optimization: Efficient functions with optimized gas usage
Installation
npm install @paulstinchcombe/kaminftlibraryQuick Start Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@paulstinchcombe/kaminftlibrary/contracts/KamiNFTLibrary.sol";
contract MyNFT is ERC721Enumerable, AccessControl {
using KamiNFTLibrary for *;
using SafeERC20 for IERC20;
IERC20 public paymentToken;
address public contractOwner;
constructor(address _paymentToken) ERC721("MyNFT", "MNFT") {
paymentToken = IERC20(_paymentToken);
contractOwner = msg.sender;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(KamiNFTLibrary.OWNER_ROLE, msg.sender);
// Initialize library configurations
KamiNFTLibrary.initializeRoyaltyConfig();
}
// Platform and royalty config initialization
function initializePlatform(address platform, uint96 commission) external onlyRole(DEFAULT_ADMIN_ROLE) {
KamiNFTLibrary.initializePlatform(platform, commission);
}
function setRoyaltyPercentage(uint96 pct) external onlyRole(DEFAULT_ADMIN_ROLE) {
KamiNFTLibrary.setRoyaltyPercentage(pct, address(this));
}
function setMintRoyalties(KamiNFTLibrary.RoyaltyData[] calldata royalties) external onlyRole(DEFAULT_ADMIN_ROLE) {
KamiNFTLibrary.setMintRoyalties(royalties, address(this));
}
function setTransferRoyalties(KamiNFTLibrary.RoyaltyData[] calldata royalties) external onlyRole(DEFAULT_ADMIN_ROLE) {
KamiNFTLibrary.setTransferRoyalties(royalties, address(this));
}
// Payment distribution
function distributeMintRoyalties(uint256 tokenId, uint256 mintPrice) external {
paymentToken.safeTransferFrom(msg.sender, address(this), mintPrice);
KamiNFTLibrary.distributeMintRoyalties(tokenId, mintPrice, paymentToken);
}
function distributeTransferRoyalties(uint256 tokenId, uint256 salePrice) external returns (uint256) {
paymentToken.safeTransferFrom(msg.sender, address(this), salePrice);
return KamiNFTLibrary.distributeTransferRoyalties(tokenId, salePrice, paymentToken);
}
// Rental system
function rentToken(uint256 tokenId, uint256 duration, uint256 rentalPrice) external {
KamiNFTLibrary.rentToken(paymentToken, tokenId, duration, rentalPrice, ownerOf(tokenId), address(this), false);
}
function endRental(uint256 tokenId) external {
KamiNFTLibrary.endRentalSimple(tokenId, ownerOf(tokenId), false);
}
function extendRental(uint256 tokenId, uint256 additionalDuration, uint256 additionalPayment) external {
KamiNFTLibrary.extendRental(paymentToken, tokenId, additionalDuration, additionalPayment, ownerOf(tokenId), false);
}
function isRented(uint256 tokenId) external view returns (bool) {
return KamiNFTLibrary.isRented(tokenId);
}
// Utility
function owner() external view returns (address) {
return contractOwner;
}
}API Reference
Configuration Functions
Platform Configuration
function initializePlatform(address platformAddress_, uint96 platformCommissionPercentage_) internalInitialize platform configuration with commission percentage and address.
Royalty Configuration
function initializeRoyaltyConfig() internal
function setRoyaltyPercentage(uint96 newRoyaltyPercentage, address accessControl) internal
function setMintRoyalties(RoyaltyData[] calldata royalties, address accessControl) internal
function setTransferRoyalties(RoyaltyData[] calldata royalties, address accessControl) internal
function setTokenMintRoyalties(uint256 tokenId, RoyaltyData[] calldata royalties, address accessControl, function(uint256) view returns (bool) exists) internal
function setTokenTransferRoyalties(uint256 tokenId, RoyaltyData[] calldata royalties, address accessControl, function(uint256) view returns (bool) exists) internalDistribution Functions
Mint Royalties
function distributeMintRoyalties(uint256 tokenId, uint256 amount, IERC20 paymentToken) internalDistribute royalties and platform commission during minting.
Transfer Royalties
function distributeTransferRoyalties(uint256 tokenId, uint256 salePrice, IERC20 paymentToken) internal returns (uint256 totalDistributed)Distribute royalties and platform commission during transfers.
Rental Functions
Core Rental Operations
function rentToken(IERC20 paymentToken, uint256 tokenId, uint256 duration, uint256 rentalPrice, address tokenOwner, address accessControl, bool isERC1155) internal
function endRentalSimple(uint256 tokenId, address tokenOwner, bool isERC1155) internal
function extendRental(IERC20 paymentToken, uint256 tokenId, uint256 additionalDuration, uint256 additionalPayment, address tokenOwner, bool isERC1155) internalRental Queries
function isRented(uint256 tokenId) internal view returns (bool)
function getRentalInfo(mapping(uint256 => Rental) storage rentals, uint256 tokenId, function(uint256) view returns (bool) exists) internal view returns (Rental memory)Transfer Functions
Sale and Transfer
function sellToken(IERC20 paymentToken, uint256 tokenId, address to, uint256 salePrice, address seller) internal
function validateTransfer(uint256 tokenId, address from, address to, address tokenOwner, function(address, address) view returns (bool) isApprovedForAll, function(uint256) view returns (address) getApproved) internal view
function updateRentalOnTransferSimple(uint256 tokenId, address from, address to) internalRoyalty Payment
function initiateTransferWithRoyalty(uint256 tokenId, address to, uint256 salePrice, address owner) internal
function payTransferRoyalty(IERC20 paymentToken, uint256 tokenId, address to, uint256 salePrice, address buyer, address seller) internal
function calculateTransferPayment(uint256 salePrice) internal view returns (uint256 totalRequired)
function isTransferRoyaltyRequired(uint256 tokenId, address from, address to, uint256 salePrice) internal view returns (bool)Getter Functions
function platformAddress() internal view returns (address)
function platformCommission() internal view returns (uint96)
function royaltyPercentage() internal view returns (uint96)
function getMintRoyaltyReceivers(uint256 tokenId) internal view returns (RoyaltyData[] memory)
function getTransferRoyaltyReceivers(uint256 tokenId) internal view returns (RoyaltyData[] memory)Data Structures
struct RoyaltyData {
address receiver; // The address that will receive the royalty payment
uint96 feeNumerator; // The royalty percentage in basis points (e.g., 1000 = 10%)
}
struct Rental {
address renter; // The address currently renting the token
uint256 startTime; // The timestamp when the rental started
uint256 endTime; // The timestamp when the rental will end
uint256 rentalPrice; // The total price paid for the rental
bool active; // Whether the rental is currently active
}
struct PlatformConfig {
uint96 commissionPercentage; // The platform commission percentage in basis points
address platformAddress; // The address that receives platform commission payments
}
struct RoyaltyConfig {
uint96 royaltyPercentage; // The global royalty percentage in basis points
RoyaltyData[] mintRoyaltyReceivers; // Array of royalty receivers for mint operations
RoyaltyData[] transferRoyaltyReceivers; // Array of royalty receivers for transfer operations
mapping(uint256 => RoyaltyData[]) tokenMintRoyalties; // Token-specific mint royalties
mapping(uint256 => RoyaltyData[]) tokenTransferRoyalties; // Token-specific transfer royalties
}Usage Examples
Setting Up Royalties
// Initialize with 10% global royalty and 5% platform commission
KamiNFTLibrary.initializeRoyaltyConfig();
KamiNFTLibrary.initializePlatform(platformAddress, 500); // 5%
// Set mint royalties: 70% to artist, 30% to platform
KamiNFTLibrary.RoyaltyData[] memory mintRoyalties = new KamiNFTLibrary.RoyaltyData[](2);
mintRoyalties[0] = KamiNFTLibrary.RoyaltyData(artistAddress, 7000); // 70%
mintRoyalties[1] = KamiNFTLibrary.RoyaltyData(platformAddress, 3000); // 30%
KamiNFTLibrary.setMintRoyalties(mintRoyalties, address(this));
// Set transfer royalties: 60% to artist, 40% to creator
KamiNFTLibrary.RoyaltyData[] memory transferRoyalties = new KamiNFTLibrary.RoyaltyData[](2);
transferRoyalties[0] = KamiNFTLibrary.RoyaltyData(artistAddress, 6000); // 60%
transferRoyalties[1] = KamiNFTLibrary.RoyaltyData(creatorAddress, 4000); // 40%
KamiNFTLibrary.setTransferRoyalties(transferRoyalties, address(this));Minting with Royalties
function mintWithRoyalties(uint256 tokenId, uint256 mintPrice) external {
// Transfer payment from minter to contract
paymentToken.safeTransferFrom(msg.sender, address(this), mintPrice);
// Distribute royalties and platform commission
KamiNFTLibrary.distributeMintRoyalties(tokenId, mintPrice, paymentToken);
// Mint the token
_mint(msg.sender, tokenId);
}Renting Tokens
function rentToken(uint256 tokenId, uint256 duration, uint256 rentalPrice) external {
// Rent token for specified duration
KamiNFTLibrary.rentToken(
paymentToken,
tokenId,
duration,
rentalPrice,
ownerOf(tokenId),
address(this),
false // isERC1155
);
}
function endRental(uint256 tokenId) external {
// End rental early (only owner or renter can call)
KamiNFTLibrary.endRentalSimple(tokenId, ownerOf(tokenId), false);
}Selling with Royalties
function sellToken(uint256 tokenId, address buyer, uint256 salePrice) external {
// Sell token with automatic royalty distribution
KamiNFTLibrary.sellToken(
paymentToken,
tokenId,
buyer,
salePrice,
msg.sender // seller
);
// Transfer token to buyer
_transfer(msg.sender, buyer, tokenId);
}Testing
The project includes a comprehensive test suite covering all functionality:
Test Coverage
- ✅ 20/20 tests passing
- Platform configuration and validation
- Royalty management and distribution
- Mint and transfer royalty calculations
- Rental system functionality
- Edge cases and rounding handling
- Buyer-as-royalty-receiver scenarios
Gas Optimization Results
distributeMintRoyalties: ~104k gasdistributeTransferRoyalties: ~125k gasrentToken: ~213k gassellToken: ~182k gas
Running Tests
# Run all tests
npx hardhat test
# Run with gas reporting
npx hardhat test --gas
# Run specific test file
npx hardhat test test/KamiNFTLibrary.test.tsDeployment
Prerequisites
- Node.js >= 16.0.0
- npm >= 8.0.0
- Hardhat
- OpenZeppelin Contracts v5
Setup
# Install dependencies
npm install
# Compile contracts
npx hardhat compile
# Run tests
npx hardhat test
# Deploy to network (example)
npx hardhat run scripts/deploy.js --network mainnetEnvironment Configuration
# .env file
PRIVATE_KEY=your_private_key
INFURA_URL=your_infura_url
ETHERSCAN_API_KEY=your_etherscan_keySecurity Considerations
Access Control
- All critical functions are protected by role-based access control
- Maximum commission rate: 20% (2000 basis points)
- Maximum royalty rate: 30% (3000 basis points)
Validation
- Rental and transfer validation ensures royalties are always paid
- Token existence checks prevent invalid operations
- Rental expiration is automatically enforced
Rounding & Dust Handling
- Rounding errors are handled by sending dust to the first royalty receiver
- All calculations use safe math operations
- Comprehensive edge case testing
Troubleshooting
Common Issues
"Caller is not an owner"
- Ensure the caller has the
OWNER_ROLEin the AccessControl contract - Check that the role was granted during contract initialization
"Royalty payment required before transfer"
- Use
initiateTransferWithRoyaltyandpayTransferRoyaltyfunctions - Ensure the buyer has approved sufficient tokens for the contract
"Token is currently rented"
- Check rental status with
isRented(tokenId) - End rental with
endRentalSimpleif needed
Rounding Errors
- The library automatically handles rounding by sending dust to the first receiver
- Use
.to.be.closeTo()in tests for approximate equality checks
Gas Optimization Tips
- Use the simplified rental functions (
endRentalSimple,updateRentalOnTransferSimple) - Batch operations when possible
- Consider using ERC1155 for multiple token types
License
MIT License - see LICENSE file for details.
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
Support
- Documentation: GitHub Wiki
- Issues: GitHub Issues
- Discussions: GitHub Discussions