// SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@le7el/web3_crs/contracts/resolver/BaseResolver.sol"; import "../../interface/IERC20.sol"; import "../../interface/IERC1155Burnable.sol"; import "../../interface/IERC20BurnableV1.sol"; import "../../interface/IERC20BurnableV2.sol"; import "./ILevelingFormula.sol"; import "./ILevelResolver.sol"; /** * @dev Any owner of a node can configure own leveling system for other NFT owners. * He configures his node as project for external usage, defining experience token and leveling formula. * NFT owner needs to burn experience tokens set for such a project to level up. * Leveling formula can be set as an oracle conract, or default formula * 500*level^2-500*level=exp will be used. */ abstract contract LevelResolver is ILevelResolver, BaseResolver { // parabolic equation coeficients for the default leveling formula. uint256 public constant DEF_LEVELING_COF1 = 500; uint256 public constant DEF_LEVELING_COF2 = 250000; uint256 public constant DEF_LEVELING_COF3 = 2000; uint256 public constant DEF_LEVELING_COF4 = 1000; bytes4 public constant IERC1155_BURNABLE = 0xf5298aca; bytes4 public constant IERC20_BURNABLE_V1 = 0x79cc6790; bytes4 public constant IERC20_BURNABLE_V2 = 0x9dc29fac; struct Project{ address formula; address experienceToken; uint256 experienceTokenId; bytes4 burnInterface; } mapping(bytes32=>mapping(bytes32=>uint256)) internal levelingExperience; mapping(bytes32=>Project) public levelingProjects; /** * @dev Burn experience tokens to advance in leveling. * @param _project node for a project which issue experience. * @param _node the node to update. * @param _burnExperienceTokenAmount amount of experience tokens to burn. * @return updated experience. */ function advanceToNextLevel( bytes32 _project, bytes32 _node, uint256 _burnExperienceTokenAmount ) virtual external authorised(_node) returns (uint256) { require(_burnExperienceTokenAmount > 0, "no advance in leveling"); Project memory _levelingProject = levelingProjects[_project]; require(_levelingProject.experienceToken != address(0), "unregistered project"); if (_levelingProject.burnInterface == IERC1155_BURNABLE) { IERC1155Burnable(_levelingProject.experienceToken).burn(msg.sender, _levelingProject.experienceTokenId, _burnExperienceTokenAmount); } else if (_levelingProject.burnInterface == IERC20_BURNABLE_V2) { IERC20BurnableV2(_levelingProject.experienceToken).burn(msg.sender, _burnExperienceTokenAmount); } else { IERC20BurnableV1(_levelingProject.experienceToken).burnFrom(msg.sender, _burnExperienceTokenAmount); } uint256 _currentExp = levelingExperience[_node][_project]; uint256 _newExp = _currentExp + _burnExperienceTokenAmount; levelingExperience[_node][_project] = _newExp; emit AdvancedToNextLevel(_project, _node, _burnExperienceTokenAmount, _newExp); return _newExp; } /** * @dev Project controller can update leveling system and experience token. * @param _project node for a project which issue experience. * @param _levelingFormulaProxy address of proxy contract which implements ILevelingFormula, pass address(0) for default formula. * @param _experienceToken address of experience token. * @param _experienceTokenId experience token id in case of ERC1155, pass 0 for ERC20. * @param _burnInterface signature of burning function: 0xf5298aca, 0x9dc29fac or 0x79cc6790 (default). */ function setProjectLevelingRules( bytes32 _project, address _levelingFormulaProxy, address _experienceToken, uint256 _experienceTokenId, bytes4 _burnInterface ) virtual external authorised(_project) { levelingProjects[_project] = Project(_levelingFormulaProxy, _experienceToken, _experienceTokenId, _burnInterface); emit ProjectLevelingRulesChanged(_project, _burnInterface, _experienceToken, _experienceTokenId, _levelingFormulaProxy); } /** * @dev Level based on experience. * @param _project node for a project which issue experience. * @param _node the node to query. * @return level based on experience */ function level(bytes32 _project, bytes32 _node) virtual override external view returns (uint256) { uint256 _exp = levelingExperience[_node][_project]; if (_exp == 0) return 1; if (levelingProjects[_project].formula == address(0)) { if (levelingProjects[_project].burnInterface != IERC1155_BURNABLE) { return _defaultLevelingFormula(_exp, IERC20(levelingProjects[_project].experienceToken).decimals()); } return _defaultLevelingFormula(_exp, 0); } else { return ILevelingFormula(levelingProjects[_project].formula).expToLevel(_exp); } } /** * @dev Experience in scope of project. * @param _project node for a project which issue experience. * @param _node the node to query. * @return project experience */ function experience(bytes32 _project, bytes32 _node) virtual override external view returns (uint256) { return levelingExperience[_node][_project]; } /** * @dev Check if specific interface is implemented. * @param interfaceID Keccak of matched interface. * @return true if implemented. */ function supportsInterface(bytes4 interfaceID) virtual override public pure returns (bool) { return interfaceID == type(ILevelResolver).interfaceId || super.supportsInterface(interfaceID); } /** * @dev Parabolic leveling formula similar to DnD 3.5 leveling system: 500*level^2-500*level=exp. * @param _exp total experience. * @param _decimals experience token decimals. * @return level based on experience */ function _defaultLevelingFormula(uint256 _exp, uint256 _decimals) internal pure returns (uint256) { if (_decimals > 0) _exp = _exp / (10 ** _decimals); return (DEF_LEVELING_COF1 + _sqrt(DEF_LEVELING_COF2 + (DEF_LEVELING_COF3 * _exp))) / DEF_LEVELING_COF4; } /** * @dev Square root, taken from Uniswap 2.0. * @param y argument for square root. * @return z a rounded square root result. */ function _sqrt(uint256 y) internal pure returns (uint256 z) { if (y > 3) { z = y; uint256 x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } }