├── README.MD ├── interface ├── IBridgeToken.sol ├── IBridge.sol ├── IEXOToken.sol ├── IGCREDToken.sol ├── IGovernance.sol └── IStakingReward.sol ├── Migrations.sol ├── bridge └── Bridge.sol ├── ProxyAdmin.sol ├── TransparentUpgradeableProxy.sol ├── token ├── EXOToken.sol └── GCREDToken.sol ├── governance └── Governance.sol └── staking └── StakingReward.sol /README.MD: -------------------------------------------------------------------------------- 1 | # CryptoBank 2 | -------------------------------------------------------------------------------- /interface/IBridgeToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | /// @title interface of simple ERC20 token for bridge 5 | /// @author Tamer Fouad 6 | interface IBridgeToken { 7 | /// @dev Mint EXO/GCRED via bridge 8 | /// @param to Address to mint 9 | /// @param amount Token amount to mint 10 | function bridgeMint(address to, uint256 amount) external; 11 | 12 | /// @dev Burn EXO/GCRED via bridge 13 | /// @param owner Address to burn 14 | /// @param amount Token amount to burn 15 | function bridgeBurn(address owner, uint256 amount) external; 16 | } 17 | -------------------------------------------------------------------------------- /interface/IBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | /// @title interface for bridge 5 | /// @author Tamer Fouad 6 | interface IBridge { 7 | enum Step { 8 | Burn, 9 | Mint 10 | } 11 | 12 | /// @dev Emitted when token transfered via bridge 13 | /// @param from address 14 | /// @param to address 15 | /// @param amount token amount 16 | /// @param nonce random number for each transfer 17 | /// @param step whether mint or burn 18 | event Transfer( 19 | address indexed from, 20 | address indexed to, 21 | uint256 amount, 22 | uint256 nonce, 23 | Step indexed step 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint256 public lastCompletedMigration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint256 completed) public restricted { 17 | lastCompletedMigration = completed; 18 | } 19 | 20 | function upgrade(address new_address) public restricted { 21 | Migrations upgraded = Migrations(new_address); 22 | upgraded.setCompleted(lastCompletedMigration); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /interface/IEXOToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | /// @title interface for Governance token EXO 5 | /// @author Tamer Fouad 6 | interface IEXOToken { 7 | /// @dev Mint EXO 8 | /// @param to Address to min 9 | /// @param amount mint amount 10 | function mint(address to, uint256 amount) external; 11 | 12 | /// @dev Mint EXO via bridge 13 | /// @param to Address to mint 14 | /// @param amount Token amount to mint 15 | function bridgeMint(address to, uint256 amount) external; 16 | 17 | /// @dev Burn EXO via bridge 18 | /// @param owner Address to burn 19 | /// @param amount Token amount to burn 20 | function bridgeBurn(address owner, uint256 amount) external; 21 | 22 | /// @notice Set bridge contract address 23 | /// @dev Grant `BRIDGE_ROLE` to bridge contract 24 | /// @param _bridge Bridge contract address 25 | function setBridge(address _bridge) external; 26 | 27 | /// @notice Set staking contract address 28 | /// @dev Grant `MINTER_ROLE` to staking contract 29 | /// @param _staking Staking contract address 30 | function setStakingReward(address _staking) external; 31 | } 32 | -------------------------------------------------------------------------------- /bridge/Bridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../interface/IBridge.sol"; 5 | import "../interface/IBridgeToken.sol"; 6 | 7 | import "@openzeppelin/contracts/security/Pausable.sol"; 8 | import "@openzeppelin/contracts/access/Ownable.sol"; 9 | 10 | contract Bridge is IBridge, Pausable, Ownable { 11 | address public TOKEN_ADDRESS; 12 | 13 | uint256 private _nonce; 14 | mapping(uint256 => bool) private _processedNonces; 15 | 16 | constructor(address _TOKEN_ADDRESS) { 17 | TOKEN_ADDRESS = _TOKEN_ADDRESS; 18 | } 19 | 20 | function mint( 21 | address from, 22 | address to, 23 | uint256 amount, 24 | uint256 otherChainNonce 25 | ) public onlyOwner whenNotPaused { 26 | require( 27 | !_processedNonces[otherChainNonce], 28 | "Bridge: transfer already processed" 29 | ); 30 | _processedNonces[otherChainNonce] = true; 31 | 32 | IBridgeToken(TOKEN_ADDRESS).bridgeMint(to, amount); 33 | 34 | emit Transfer(from, to, amount, otherChainNonce, Step.Mint); 35 | } 36 | 37 | function burn(address to, uint256 amount) public whenNotPaused { 38 | IBridgeToken(TOKEN_ADDRESS).bridgeBurn(msg.sender, amount); 39 | 40 | emit Transfer(msg.sender, to, amount, _nonce, Step.Burn); 41 | _nonce++; 42 | } 43 | 44 | function pause() public onlyOwner { 45 | _pause(); 46 | } 47 | 48 | function unpause() public onlyOwner { 49 | _unpause(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /interface/IGCREDToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | /// @title interface for game token GCRED 5 | /// @author Tamer Fouad 6 | interface IGCREDToken { 7 | /// @dev Emitted when staking contract address updated 8 | /// @param _STAKING_REWARD staking contract address 9 | event StakingRewardUpdated(address _STAKING_REWARD); 10 | 11 | /// @dev Emitted when the owner update MD(metaverse development) wallet address 12 | /// @param MD_ADDRESS new MD wallet address 13 | event MDAddressUpdated(address MD_ADDRESS); 14 | 15 | /// @dev Emitted when the owner update DAO wallet address 16 | /// @param DAO_ADDRESS new DAO wallet address 17 | event DAOAddressUpdated(address DAO_ADDRESS); 18 | 19 | /// @dev Mint GCRED via bridge 20 | /// @param to Address to mint 21 | /// @param amount Token amount to mint 22 | function bridgeMint(address to, uint256 amount) external; 23 | 24 | /// @dev Burn GCRED via bridge 25 | /// @param owner Address to burn 26 | /// @param amount Token amount to burn 27 | function bridgeBurn(address owner, uint256 amount) external; 28 | 29 | /// @dev Mint GCRED via EXO for daily reward 30 | /// @param to Address to mint 31 | /// @param amount Token amount to mint 32 | function mintForReward(address to, uint256 amount) external; 33 | 34 | /** 35 | * @dev Set Staking reward contract address 36 | * @param _STAKING_REWARD staking contract address 37 | * 38 | * Emits a {StakingRewardUpdated} event 39 | */ 40 | function setStakingReward(address _STAKING_REWARD) external; 41 | 42 | /** 43 | * @dev Set MD(Metaverse Development) wallet address 44 | * @param _MD_ADDRESS MD wallet address 45 | * 46 | * Emits a {MDAddressUpdated} event 47 | */ 48 | function setMDAddress(address _MD_ADDRESS) external; 49 | 50 | /** 51 | * @dev Set DAO wallet address 52 | * @param _DAO_ADDRESS DAO wallet address 53 | * 54 | * Emits a {DAOAddressUpdated} event 55 | */ 56 | function setDAOAddress(address _DAO_ADDRESS) external; 57 | } 58 | -------------------------------------------------------------------------------- /ProxyAdmin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "./TransparentUpgradeableProxy.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | /** 8 | * @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an 9 | * explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}. 10 | */ 11 | contract ProxyAdmin is Ownable { 12 | /** 13 | * @dev Returns the current implementation of `proxy`. 14 | * 15 | * Requirements: 16 | * 17 | * - This contract must be the admin of `proxy`. 18 | */ 19 | function getProxyImplementation(TransparentUpgradeableProxy proxy) 20 | public 21 | view 22 | virtual 23 | returns (address) 24 | { 25 | // We need to manually run the static call since the getter cannot be flagged as view 26 | // bytes4(keccak256("implementation()")) == 0x5c60da1b 27 | (bool success, bytes memory returndata) = address(proxy).staticcall( 28 | hex"5c60da1b" 29 | ); 30 | require(success); 31 | return abi.decode(returndata, (address)); 32 | } 33 | 34 | /** 35 | * @dev Returns the current admin of `proxy`. 36 | * 37 | * Requirements: 38 | * 39 | * - This contract must be the admin of `proxy`. 40 | */ 41 | function getProxyAdmin(TransparentUpgradeableProxy proxy) 42 | public 43 | view 44 | virtual 45 | returns (address) 46 | { 47 | // We need to manually run the static call since the getter cannot be flagged as view 48 | // bytes4(keccak256("admin()")) == 0xf851a440 49 | (bool success, bytes memory returndata) = address(proxy).staticcall( 50 | hex"f851a440" 51 | ); 52 | require(success); 53 | return abi.decode(returndata, (address)); 54 | } 55 | 56 | /** 57 | * @dev Changes the admin of `proxy` to `newAdmin`. 58 | * 59 | * Requirements: 60 | * 61 | * - This contract must be the current admin of `proxy`. 62 | */ 63 | function changeProxyAdmin( 64 | TransparentUpgradeableProxy proxy, 65 | address newAdmin 66 | ) public virtual onlyOwner { 67 | proxy.changeAdmin(newAdmin); 68 | } 69 | 70 | /** 71 | * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. 72 | * 73 | * Requirements: 74 | * 75 | * - This contract must be the admin of `proxy`. 76 | */ 77 | function upgrade(TransparentUpgradeableProxy proxy, address implementation) 78 | public 79 | virtual 80 | onlyOwner 81 | { 82 | proxy.upgradeTo(implementation); 83 | } 84 | 85 | /** 86 | * @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. 87 | * 88 | * Requirements: 89 | * 90 | * - This contract must be the admin of `proxy`. 91 | */ 92 | function upgradeAndCall( 93 | TransparentUpgradeableProxy proxy, 94 | address implementation, 95 | bytes memory data 96 | ) public payable virtual onlyOwner { 97 | proxy.upgradeToAndCall{ value: msg.value }(implementation, data); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /TransparentUpgradeableProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 5 | 6 | contract TransparentUpgradeableProxy is ERC1967Proxy { 7 | /** 8 | * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and 9 | * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. 10 | */ 11 | constructor( 12 | address _logic, 13 | address admin_, 14 | bytes memory _data 15 | ) payable ERC1967Proxy(_logic, _data) { 16 | assert( 17 | _ADMIN_SLOT == 18 | bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1) 19 | ); 20 | _changeAdmin(admin_); 21 | } 22 | 23 | /** 24 | * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. 25 | */ 26 | modifier ifAdmin() { 27 | if (msg.sender == _getAdmin()) { 28 | _; 29 | } else { 30 | _fallback(); 31 | } 32 | } 33 | 34 | /** 35 | * @dev Returns the current admin. 36 | * 37 | * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}. 38 | * 39 | */ 40 | function admin() external ifAdmin returns (address admin_) { 41 | admin_ = _getAdmin(); 42 | } 43 | 44 | /** 45 | * @dev Returns the current implementation. 46 | * 47 | * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}. 48 | * 49 | */ 50 | function implementation() 51 | external 52 | ifAdmin 53 | returns (address implementation_) 54 | { 55 | implementation_ = _implementation(); 56 | } 57 | 58 | /** 59 | * @dev Changes the admin of the proxy. 60 | * 61 | * Emits an {AdminChanged} event. 62 | * 63 | * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. 64 | */ 65 | function changeAdmin(address newAdmin) external virtual ifAdmin { 66 | _changeAdmin(newAdmin); 67 | } 68 | 69 | /** 70 | * @dev Upgrade the implementation of the proxy. 71 | * 72 | * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. 73 | */ 74 | function upgradeTo(address newImplementation) external ifAdmin { 75 | _upgradeToAndCall(newImplementation, bytes(""), false); 76 | } 77 | 78 | /** 79 | * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified 80 | * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the 81 | * proxied contract. 82 | * 83 | * NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}. 84 | */ 85 | function upgradeToAndCall(address newImplementation, bytes calldata data) 86 | external 87 | payable 88 | ifAdmin 89 | { 90 | _upgradeToAndCall(newImplementation, data, true); 91 | } 92 | 93 | /** 94 | * @dev Returns the current admin. 95 | */ 96 | function _admin() internal view virtual returns (address) { 97 | return _getAdmin(); 98 | } 99 | 100 | /** 101 | * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}. 102 | */ 103 | function _beforeFallback() internal virtual override { 104 | require( 105 | msg.sender != _getAdmin(), 106 | "TransparentUpgradeableProxy: admin cannot fallback to proxy target" 107 | ); 108 | super._beforeFallback(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /interface/IGovernance.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | /// @title interface for Governance logic 5 | /// @author Tamer Fouad 6 | interface IGovernance { 7 | /// @notice List: sub list of the vote 8 | /// @param title Vote list title 9 | /// @param voteCount Vote count 10 | struct Proposal { 11 | string title; 12 | uint256 voteCount; 13 | } 14 | 15 | /// @notice Struct for the vote 16 | /// @param index Vote index 17 | /// @param startDate Vote start date 18 | /// @param endDate Vote end date 19 | /// @param subject Vote subject 20 | /// @param proposalCount Proposal count 21 | /// @param lists Proposal array 22 | struct Vote { 23 | uint256 index; 24 | uint256 startDate; 25 | uint256 endDate; 26 | string subject; 27 | } 28 | 29 | /// @dev Emitted when a new vote created 30 | /// @param subject Subject string 31 | /// @param start Start date of the voting period 32 | /// @param end End date of the voting period 33 | /// @param timestamp created time 34 | event NewVote( 35 | string subject, 36 | uint256 start, 37 | uint256 end, 38 | uint256 timestamp 39 | ); 40 | 41 | /// @dev Emitted when voter cast a vote 42 | /// @param voter voter address 43 | /// @param voteId vote id 44 | /// @param proposalId proposal id 45 | /// @param weight voting weight 46 | event VoteCast( 47 | address indexed voter, 48 | uint256 indexed voteId, 49 | uint256 weight, 50 | uint8 indexed proposalId 51 | ); 52 | 53 | /// @dev Emitted when exo address updated 54 | /// @param _EXO_ADDRESS EXO token address 55 | event EXOAddressUpdated(address _EXO_ADDRESS); 56 | 57 | /// @dev Emitted when staking contract address updated 58 | /// @param _STAKING_REWARD staking contract address 59 | event StakingAddressUpdated(address _STAKING_REWARD); 60 | 61 | /** 62 | * @notice Create a new vote 63 | * @param _subject string subject 64 | * @param _startDate Start date of the voting period 65 | * @param _endDate End date of the voting period 66 | * @param _proposals Proposal list 67 | * 68 | * Requirements 69 | * 70 | * - Only owner can create a new vote 71 | * - Validate voting period with `_startDate` and `_endDate` 72 | * 73 | * Emits a {NewVote} event 74 | */ 75 | function createVote( 76 | uint256 _startDate, 77 | uint256 _endDate, 78 | string memory _subject, 79 | string[] memory _proposals 80 | ) external; 81 | 82 | /** 83 | * @notice Cast a vote 84 | * @dev Returns a vote weight 85 | * @param _voteId Vote Id 86 | * @param _proposalId Proposal Id 87 | * @return _weight Vote weight 88 | * 89 | * Requirements 90 | * 91 | * - Validate `_voteId` 92 | * - Validate `_proposalId` 93 | * - Check the voting period 94 | * 95 | * Emits a {VoteCast} event 96 | */ 97 | function castVote(uint256 _voteId, uint8 _proposalId) 98 | external 99 | returns (uint256); 100 | 101 | /** 102 | * @dev Set EXO token address 103 | * @param _EXO_ADDRESS EXO token address 104 | * 105 | * Emits a {EXOAddressUpdated} event 106 | */ 107 | function setEXOAddress(address _EXO_ADDRESS) external; 108 | 109 | /** 110 | * @dev Set staking contract address 111 | * @param _STAKING_REWARD staking contract address 112 | * 113 | * Emits a {StakingAddressUpdated} event 114 | */ 115 | function setStakingAddress(address _STAKING_REWARD) external; 116 | 117 | /// @dev Returns all votes in array 118 | /// @return allVotes All vote array 119 | function getAllVotes() external view returns (Vote[] memory); 120 | 121 | /// @dev Returns all active votes in array 122 | /// @return activeVotes Active vote array 123 | function getActiveVotes() external view returns (Vote[] memory); 124 | 125 | /// @dev Returns all future votes in array 126 | /// @return futureVotes Future array 127 | function getFutureVotes() external view returns (Vote[] memory); 128 | 129 | /// @dev Returns a specific proposal with `voteId` and `proposalId` 130 | /// @param _voteId Vote id 131 | /// @param _proposalId Proposal id 132 | /// @return proposal Proposal 133 | function getProposal(uint256 _voteId, uint256 _proposalId) 134 | external 135 | view 136 | returns (Proposal memory); 137 | } 138 | -------------------------------------------------------------------------------- /token/EXOToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../interface/IStakingReward.sol"; 5 | import "../interface/IEXOToken.sol"; 6 | 7 | import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 10 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 11 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 12 | 13 | contract EXOToken is 14 | Initializable, 15 | IEXOToken, 16 | ERC20Upgradeable, 17 | ERC20BurnableUpgradeable, 18 | PausableUpgradeable, 19 | AccessControlUpgradeable 20 | { 21 | bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); 22 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 23 | bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE_ROLE"); 24 | 25 | uint256 constant decimal = 1e18; 26 | uint256 _totalSupply; 27 | 28 | address public STAKING_REWARD; 29 | 30 | // Bridge contract address that can mint or burn 31 | address public bridge; 32 | 33 | /// @custom:oz-upgrades-unsafe-allow constructor 34 | constructor() { 35 | _disableInitializers(); 36 | } 37 | 38 | function initialize() public initializer { 39 | __ERC20_init("EXO Token", "EXO"); 40 | __ERC20Burnable_init(); 41 | __Pausable_init(); 42 | __AccessControl_init(); 43 | 44 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 45 | _grantRole(PAUSER_ROLE, msg.sender); 46 | _grantRole(MINTER_ROLE, msg.sender); 47 | 48 | _totalSupply = 1e28; 49 | } 50 | 51 | function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { 52 | _mint(to, amount); 53 | } 54 | 55 | 56 | /// @inheritdoc IEXOToken 57 | function bridgeMint(address to, uint256 amount) 58 | external 59 | override 60 | onlyRole(BRIDGE_ROLE) 61 | { 62 | _mint(to, amount); 63 | } 64 | 65 | /// @inheritdoc IEXOToken 66 | function bridgeBurn(address owner, uint256 amount) 67 | external 68 | override 69 | onlyRole(BRIDGE_ROLE) 70 | { 71 | _burn(owner, amount); 72 | } 73 | 74 | /// @inheritdoc IEXOToken 75 | function setBridge(address _bridge) 76 | external 77 | override 78 | onlyRole(DEFAULT_ADMIN_ROLE) 79 | { 80 | require(_bridge != address(0x0), "Can not be zero address"); 81 | _revokeRole(BRIDGE_ROLE, bridge); 82 | bridge = _bridge; 83 | _grantRole(BRIDGE_ROLE, bridge); 84 | } 85 | 86 | /// @inheritdoc IEXOToken 87 | function setStakingReward(address _STAKING_REWARD) 88 | external 89 | override 90 | onlyRole(DEFAULT_ADMIN_ROLE) 91 | { 92 | require(_STAKING_REWARD != address(0x0), "Can not be zero address"); 93 | _revokeRole(MINTER_ROLE, STAKING_REWARD); 94 | STAKING_REWARD = _STAKING_REWARD; 95 | _grantRole(MINTER_ROLE, STAKING_REWARD); 96 | } 97 | 98 | function pause() public onlyRole(PAUSER_ROLE) { 99 | _pause(); 100 | } 101 | 102 | function unpause() public onlyRole(PAUSER_ROLE) { 103 | _unpause(); 104 | } 105 | 106 | function totalSupply() public view virtual override returns (uint256) { 107 | return _totalSupply; 108 | } 109 | 110 | function _beforeTokenTransfer( 111 | address from, 112 | address to, 113 | uint256 amount 114 | ) internal override whenNotPaused { 115 | super._beforeTokenTransfer(from, to, amount); 116 | } 117 | 118 | function _afterTokenTransfer( 119 | address from, 120 | address to, 121 | uint256 amount 122 | ) internal override whenNotPaused { 123 | super._afterTokenTransfer(from, to, amount); 124 | 125 | address user = _msgSender(); 126 | uint8 tier = IStakingReward(STAKING_REWARD).getTier(user); 127 | uint256 stakingCount = IStakingReward(STAKING_REWARD).getStakingCount( 128 | user 129 | ); 130 | // Check if should downgrade user's tier 131 | if (tier > 0) { 132 | uint88[4] memory minimumAmount = IStakingReward(STAKING_REWARD) 133 | .getTierMinAmount(); 134 | uint256 balance = balanceOf(user); 135 | if ( 136 | balance < uint256(minimumAmount[tier]) * decimal && 137 | stakingCount < 1 138 | ) IStakingReward(STAKING_REWARD).setTier(user, tier - 1); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /interface/IStakingReward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | /// @title interface for Staking reward logic 5 | /// @author Tamer Fouad 6 | interface IStakingReward { 7 | /// @notice Struct for staker's info 8 | /// @param holder Staking holder address 9 | /// @param amount Staked amount 10 | /// @param startDate start date of staking 11 | /// @param expireDate expire date of staking 12 | /// @param duration Stake duration 13 | /// @param latestClaimDate Timestamp for the latest claimed date 14 | /// @param interestRate Interest rate 15 | struct StakingInfo { 16 | address holder; 17 | uint256 amount; 18 | uint256 startDate; 19 | uint256 expireDate; 20 | uint256 duration; 21 | uint256 latestClaimDate; 22 | uint8 interestRate; 23 | } 24 | 25 | /// @dev Emitted when user stake 26 | /// @param from Staker address 27 | /// @param amount Staking token amount 28 | /// @param timestamp Staking time 29 | event Stake(address indexed from, uint256 amount, uint256 timestamp); 30 | 31 | /// @dev Emitted when a stake holder unstakes 32 | /// @param from address of the unstaking holder 33 | /// @param amount token amount 34 | /// @param timestamp unstaked time 35 | event UnStake(address indexed from, uint256 amount, uint256 timestamp); 36 | 37 | /// @notice Claim EXO Rewards by staking EXO 38 | /// @dev Emitted when the user claim reward 39 | /// @param to address of the claimant 40 | /// @param timestamp timestamp for the claim 41 | event Claim(address indexed to, uint256 timestamp); 42 | 43 | /// @notice Claim GCRED by holding EXO 44 | /// @dev Emitted when the user claim GCRED reward per day 45 | /// @param to address of the claimant 46 | /// @param amount a parameter just like in doxygen (must be followed by parameter name) 47 | /// @param timestamp a parameter just like in doxygen (must be followed by parameter name) 48 | event ClaimGCRED(address indexed to, uint256 amount, uint256 timestamp); 49 | 50 | /// @notice Claim EXO which is releasing from Foundation Node to prevent inflation 51 | /// @dev Emitted when the user claim FN reward 52 | /// @param to address of the claimant 53 | /// @param amount a parameter just like in doxygen (must be followed by parameter name) 54 | /// @param timestamp a parameter just like in doxygen (must be followed by parameter name) 55 | event ClaimFN(address indexed to, uint256 amount, uint256 timestamp); 56 | 57 | /// @dev Emitted when the owner update EXO token address 58 | /// @param EXO_ADDRESS new EXO token address 59 | event EXOAddressUpdated(address EXO_ADDRESS); 60 | 61 | /// @dev Emitted when the owner update GCRED token address 62 | /// @param GCRED_ADDRESS new GCRED token address 63 | event GCREDAddressUpdated(address GCRED_ADDRESS); 64 | 65 | /// @dev Emitted when the owner update FN wallet address 66 | /// @param FOUNDATION_NODE new foundation node wallet address 67 | event FoundationNodeUpdated(address FOUNDATION_NODE); 68 | 69 | /** 70 | * @notice Stake EXO tokens 71 | * @param _amount Token amount 72 | * @param _duration staking lock-up period type 73 | * 74 | * Requirements 75 | * 76 | * - Validate the balance of EXO holdings 77 | * - Validate lock-up duration type 78 | * 0: Soft lock 79 | * 1: 30 days 80 | * 2: 60 days 81 | * 3: 90 days 82 | * 83 | * Emits a {Stake} event 84 | */ 85 | function stake(uint256 _amount, uint8 _duration) external; 86 | 87 | /// @dev Set new `_tier` of `_holder` 88 | /// @param _holder foundation node address 89 | /// @param _tier foundation node address 90 | function setTier(address _holder, uint8 _tier) external; 91 | 92 | /** 93 | * @dev Set EXO token address 94 | * @param _EXO_ADDRESS EXO token address 95 | * 96 | * Emits a {EXOAddressUpdated} event 97 | */ 98 | function setEXOAddress(address _EXO_ADDRESS) external; 99 | 100 | /** 101 | * @dev Set GCRED token address 102 | * @param _GCRED_ADDRESS GCRED token address 103 | * 104 | * Emits a {GCREDAddressUpdated} event 105 | */ 106 | function setGCREDAddress(address _GCRED_ADDRESS) external; 107 | 108 | /** 109 | * @dev Set Foundation Node address 110 | * @param _FOUNDATION_NODE foundation node address 111 | * 112 | * Emits a {FoundationNodeUpdated} event 113 | */ 114 | function setFNAddress(address _FOUNDATION_NODE) external; 115 | 116 | /** 117 | * @dev Returns user's tier 118 | * @param _holder Staking holder address 119 | */ 120 | function getTier(address _holder) external view returns (uint8); 121 | 122 | /** 123 | * @dev Returns user's staking info array 124 | * @param _holder Staking holder address 125 | */ 126 | function getStakingInfos(address _holder) 127 | external 128 | view 129 | returns (StakingInfo[] memory); 130 | 131 | /** 132 | * @dev Returns staking count of the holder 133 | * @param _holder staking holder address 134 | */ 135 | function getStakingCount(address _holder) external view returns (uint256); 136 | 137 | /** 138 | * @dev Returns minimum token amount in tier 139 | */ 140 | function getTierMinAmount() external view returns (uint88[4] memory); 141 | } 142 | -------------------------------------------------------------------------------- /token/GCREDToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../interface/IGCREDToken.sol"; 5 | 6 | import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 10 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 11 | 12 | contract GCREDToken is 13 | Initializable, 14 | IGCREDToken, 15 | ERC20Upgradeable, 16 | ERC20BurnableUpgradeable, 17 | PausableUpgradeable, 18 | AccessControlUpgradeable 19 | { 20 | bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); 21 | bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE_ROLE"); 22 | bytes32 public constant STAKING_ROLE = keccak256("STAKING_ROLE"); 23 | 24 | // Metaverse development wallet address 25 | address public MD_ADDRESS; 26 | // DAO wallet address 27 | address public DAO_ADDRESS; 28 | // EXO contract address 29 | address public STAKING_REWARD; 30 | 31 | /// @custom:oz-upgrades-unsafe-allow constructor 32 | constructor() { 33 | _disableInitializers(); 34 | } 35 | 36 | function initialize(address _MD_ADDRESS, address _DAO_ADDRESS) 37 | public 38 | initializer 39 | { 40 | __ERC20_init("GCRED Token", "GCRED"); 41 | __ERC20Burnable_init(); 42 | __Pausable_init(); 43 | __AccessControl_init(); 44 | 45 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 46 | _grantRole(OWNER_ROLE, msg.sender); 47 | 48 | MD_ADDRESS = _MD_ADDRESS; 49 | DAO_ADDRESS = _DAO_ADDRESS; 50 | } 51 | 52 | /// @inheritdoc IGCREDToken 53 | function mintForReward(address to, uint256 amount) 54 | external 55 | onlyRole(STAKING_ROLE) 56 | { 57 | _mint(to, amount); 58 | } 59 | 60 | /// @inheritdoc IGCREDToken 61 | function bridgeMint(address to, uint256 amount) 62 | external 63 | onlyRole(BRIDGE_ROLE) 64 | { 65 | _mint(to, amount); 66 | } 67 | 68 | /// @inheritdoc IGCREDToken 69 | function bridgeBurn(address owner, uint256 amount) 70 | external 71 | onlyRole(BRIDGE_ROLE) 72 | { 73 | _burn(owner, amount); 74 | } 75 | 76 | /// @inheritdoc IGCREDToken 77 | function setStakingReward(address _STAKING_REWARD) 78 | external 79 | onlyRole(OWNER_ROLE) 80 | { 81 | _revokeRole(STAKING_ROLE, STAKING_REWARD); 82 | STAKING_REWARD = _STAKING_REWARD; 83 | _grantRole(STAKING_ROLE, STAKING_REWARD); 84 | 85 | emit StakingRewardUpdated(STAKING_REWARD); 86 | } 87 | 88 | /// @inheritdoc IGCREDToken 89 | function setMDAddress(address _MD_ADDRESS) external onlyRole(OWNER_ROLE) { 90 | MD_ADDRESS = _MD_ADDRESS; 91 | 92 | emit MDAddressUpdated(MD_ADDRESS); 93 | } 94 | 95 | /// @inheritdoc IGCREDToken 96 | function setDAOAddress(address _DAO_ADDRESS) external onlyRole(OWNER_ROLE) { 97 | DAO_ADDRESS = _DAO_ADDRESS; 98 | 99 | emit DAOAddressUpdated(DAO_ADDRESS); 100 | } 101 | 102 | /// @notice NPC game transaction breakdown 103 | /// @dev Breakdown transaction amount to MD, DAO, burn 104 | /// @param amount Token amount 105 | /// @return success 106 | function buyItem(uint256 amount) external returns (bool) { 107 | address _owner = _msgSender(); 108 | uint256 burnAmount = (amount * 70) / 100; 109 | uint256 MD_amount = (amount * 25) / 100; 110 | uint256 DAO_amount = (amount * 5) / 100; 111 | _transfer(_owner, MD_ADDRESS, MD_amount); 112 | _transfer(_owner, DAO_ADDRESS, DAO_amount); 113 | _burn(_owner, burnAmount); 114 | return true; 115 | } 116 | 117 | function mint(address to, uint256 amount) public onlyRole(OWNER_ROLE) { 118 | _mint(to, amount); 119 | } 120 | 121 | function pause() public onlyRole(OWNER_ROLE) { 122 | _pause(); 123 | } 124 | 125 | function unpause() public onlyRole(OWNER_ROLE) { 126 | _unpause(); 127 | } 128 | 129 | /// @notice Redefine transfer function for P2P game transactions breakdown from tokenomics 130 | /// @dev Breakdown transaction amount to MD, burn 131 | /// @inheritdoc IERC20Upgradeable 132 | function transfer(address to, uint256 amount) 133 | public 134 | virtual 135 | override 136 | returns (bool) 137 | { 138 | address owner = _msgSender(); 139 | if (to == MD_ADDRESS || to == DAO_ADDRESS) { 140 | _transfer(owner, to, amount); 141 | return true; 142 | } 143 | uint256 MDAmount = (amount * 2) / 100; 144 | uint256 burnAmount = (amount * 3) / 100; 145 | uint256 transferAmount = amount - MDAmount - burnAmount; 146 | 147 | _transfer(owner, to, transferAmount); 148 | _transfer(owner, MD_ADDRESS, MDAmount); 149 | _burn(owner, burnAmount); 150 | return true; 151 | } 152 | 153 | /// @notice Redefine transfer function for P2P game transactions breakdown from tokenomics 154 | /// @dev Breakdown transaction amount to MD, burn 155 | /// @inheritdoc IERC20Upgradeable 156 | function transferFrom( 157 | address from, 158 | address to, 159 | uint256 amount 160 | ) public virtual override returns (bool) { 161 | address spender = _msgSender(); 162 | _spendAllowance(from, spender, amount); 163 | if (to == MD_ADDRESS || to == DAO_ADDRESS) { 164 | _transfer(from, to, amount); 165 | return true; 166 | } 167 | uint256 MDAmount = (amount * 2) / 100; 168 | uint256 burnAmount = (amount * 3) / 100; 169 | uint256 transferAmount = amount - MDAmount - burnAmount; 170 | 171 | _transfer(from, to, transferAmount); 172 | _transfer(from, MD_ADDRESS, MDAmount); 173 | _burn(from, burnAmount); 174 | return true; 175 | } 176 | 177 | function _beforeTokenTransfer( 178 | address from, 179 | address to, 180 | uint256 amount 181 | ) internal override whenNotPaused { 182 | super._beforeTokenTransfer(from, to, amount); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /governance/Governance.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../interface/IGovernance.sol"; 5 | import "../interface/IStakingReward.sol"; 6 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 10 | 11 | contract Governance is 12 | Initializable, 13 | IGovernance, 14 | PausableUpgradeable, 15 | OwnableUpgradeable 16 | { 17 | // Counter for votes 18 | uint256 public voteCounter; 19 | // EXO token contract address 20 | address public EXO_ADDRESS; 21 | // Staking reward contract address 22 | address public STAKING_REWARD; 23 | 24 | // All registered votes 25 | // Mapping from vote index to the Vote struct 26 | mapping(uint256 => Vote) public registeredVotes; 27 | // Mapping from vote index to Proposal array 28 | mapping(uint256 => Proposal[]) public registeredProposals; 29 | // Whether voter can vote to the specific vote->proposal 30 | mapping(uint256 => mapping(address => bool)) public hasVoted; 31 | 32 | /// @custom:oz-upgrades-unsafe-allow constructor 33 | constructor() { 34 | _disableInitializers(); 35 | } 36 | 37 | function initialize(address _EXO_ADDRESS, address _STAKING_REWARD) 38 | public 39 | initializer 40 | { 41 | __Pausable_init(); 42 | __Ownable_init(); 43 | EXO_ADDRESS = _EXO_ADDRESS; 44 | STAKING_REWARD = _STAKING_REWARD; 45 | } 46 | 47 | /// @inheritdoc IGovernance 48 | function createVote( 49 | uint256 _startDate, 50 | uint256 _endDate, 51 | string memory _subject, 52 | string[] memory _proposals 53 | ) external override onlyOwner whenNotPaused { 54 | // Validate voting period 55 | require(_startDate > block.timestamp, "Governance: Invalid start date"); 56 | require(_startDate < _endDate, "Governance: Invalid end date"); 57 | // Register a new vote 58 | registeredVotes[voteCounter] = Vote( 59 | voteCounter, 60 | _startDate, 61 | _endDate, 62 | _subject 63 | ); 64 | Proposal[] storage proposals = registeredProposals[voteCounter]; 65 | for (uint256 i = 0; i < _proposals.length; i++) { 66 | proposals.push(Proposal(_proposals[i], 0)); 67 | } 68 | voteCounter++; 69 | 70 | emit NewVote(_subject, _startDate, _endDate, block.timestamp); 71 | } 72 | 73 | /// @inheritdoc IGovernance 74 | function castVote(uint256 _voteId, uint8 _proposalId) 75 | external 76 | override 77 | whenNotPaused 78 | returns (uint256) 79 | { 80 | address voter = _msgSender(); 81 | // Validate vote id 82 | require(_voteId < voteCounter, "Governance: Not valid Vote ID"); 83 | // Validate if EXO holder 84 | require( 85 | IERC20Upgradeable(EXO_ADDRESS).balanceOf(voter) > 0, 86 | "Governance: Not EXO holder" 87 | ); 88 | // Check if already voted or not 89 | require(!hasVoted[_voteId][voter], "Governance: User already voted"); 90 | // Register a new vote 91 | Vote memory vote = registeredVotes[_voteId]; 92 | Proposal[] storage proposals = registeredProposals[_voteId]; 93 | require( 94 | vote.endDate > block.timestamp, 95 | "Governance: Vote is already expired" 96 | ); 97 | require( 98 | vote.startDate <= block.timestamp, 99 | "Governance: Vote is not started yet" 100 | ); 101 | require( 102 | _proposalId < proposals.length, 103 | "Governance: Not valid proposal id" 104 | ); 105 | // Calculate vote weight using user's tier and EXO balance 106 | uint8 tier = IStakingReward(STAKING_REWARD).getTier(voter); 107 | uint256 balance = IERC20Upgradeable(EXO_ADDRESS).balanceOf(voter); 108 | uint256 voteWeight = ((uint256(_getVoteWeightPerEXO(tier)) / 100) * 109 | balance) / 1e18; 110 | proposals[_proposalId].voteCount += voteWeight; 111 | // Set true `hasVoted` flag 112 | hasVoted[_voteId][voter] = true; 113 | 114 | emit VoteCast(voter, _voteId, voteWeight, _proposalId); 115 | 116 | return voteWeight; 117 | } 118 | 119 | /// @inheritdoc IGovernance 120 | function setEXOAddress(address _EXO_ADDRESS) external onlyOwner { 121 | EXO_ADDRESS = _EXO_ADDRESS; 122 | 123 | emit EXOAddressUpdated(_EXO_ADDRESS); 124 | } 125 | 126 | /// @inheritdoc IGovernance 127 | function setStakingAddress(address _STAKING_REWARD) external onlyOwner { 128 | STAKING_REWARD = _STAKING_REWARD; 129 | 130 | emit StakingAddressUpdated(STAKING_REWARD); 131 | } 132 | 133 | /// @inheritdoc IGovernance 134 | function getAllVotes() external view override returns (Vote[] memory) { 135 | require(voteCounter > 0, "EXO: Registered votes Empty"); 136 | Vote[] memory allVotes = new Vote[](voteCounter); 137 | for (uint256 i = 0; i < voteCounter; i++) { 138 | Vote storage tmp_vote = registeredVotes[i]; 139 | allVotes[i] = tmp_vote; 140 | } 141 | return allVotes; 142 | } 143 | 144 | /// @inheritdoc IGovernance 145 | function getActiveVotes() external view override returns (Vote[] memory) { 146 | require(voteCounter > 0, "EXO: Vote Empty"); 147 | Vote[] memory activeVotes; 148 | uint256 j = 0; 149 | for (uint256 i = 0; i < voteCounter; i++) { 150 | Vote memory activeVote = registeredVotes[i]; 151 | if ( 152 | activeVote.startDate < block.timestamp && 153 | activeVote.endDate > block.timestamp 154 | ) { 155 | activeVotes[j++] = activeVote; 156 | } 157 | } 158 | return activeVotes; 159 | } 160 | 161 | /// @inheritdoc IGovernance 162 | function getFutureVotes() external view override returns (Vote[] memory) { 163 | require(voteCounter > 0, "EXO: Vote Empty"); 164 | Vote[] memory futureVotes; 165 | uint256 j = 0; 166 | for (uint256 i = 0; i < voteCounter; i++) { 167 | Vote memory tmp_vote = registeredVotes[i]; 168 | if (tmp_vote.startDate > block.timestamp) { 169 | futureVotes[j++] = tmp_vote; 170 | } 171 | } 172 | return futureVotes; 173 | } 174 | 175 | /// @inheritdoc IGovernance 176 | function getProposal(uint256 _voteId, uint256 _proposalId) 177 | external 178 | view 179 | override 180 | returns (Proposal memory) 181 | { 182 | Proposal memory targetProposal = registeredProposals[_voteId][ 183 | _proposalId 184 | ]; 185 | return targetProposal; 186 | } 187 | 188 | /// @dev Pause contract 189 | function pause() public onlyOwner { 190 | _pause(); 191 | } 192 | 193 | /// @dev Unpause contract 194 | function unpause() public onlyOwner { 195 | _unpause(); 196 | } 197 | 198 | /// @dev Vote weight per EXO 199 | function _getVoteWeightPerEXO(uint8 tier) internal pure returns (uint8) { 200 | uint8[4] memory weight = [100, 125, 175, 250]; 201 | return weight[tier]; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /staking/StakingReward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../interface/IStakingReward.sol"; 5 | import "../interface/IEXOToken.sol"; 6 | import "../interface/IGCREDToken.sol"; 7 | 8 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 10 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 11 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 12 | 13 | contract StakingReward is 14 | Initializable, 15 | IStakingReward, 16 | PausableUpgradeable, 17 | AccessControlUpgradeable 18 | { 19 | bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); 20 | bytes32 public constant EXO_ROLE = keccak256("EXO_ROLE"); 21 | 22 | uint256 constant MAX_REWRAD = 35e26; 23 | /// TODO should be updated when deploying 24 | /*------------------Test Only------------------*/ 25 | // uint256 constant CLAIM_DELAY = 1 minutes; 26 | /*---------------------------------------------*/ 27 | uint256 constant CLAIM_DELAY = 1 days; 28 | // Counter for staking 29 | uint256 public stakingCounter; 30 | // EXO token address 31 | address public EXO_ADDRESS; 32 | // GCRED token address 33 | address public GCRED_ADDRESS; 34 | // Foundation Node wallet which is releasing EXO to prevent inflation 35 | address public FOUNDATION_NODE; 36 | // Reward amount from FN wallet 37 | uint256 public FN_REWARD; 38 | // Last staking timestamp 39 | uint256 private latestStakingTime; 40 | // Last claimed time 41 | uint256 public latestClaimTime; 42 | // All staking infors 43 | StakingInfo[] public stakingInfos; 44 | // Tier of the user; Tier 0 ~ 3 45 | mapping(address => uint8) public tier; 46 | // Whether holder can upgrade tier status 47 | mapping(address => bool) public tierCandidate; 48 | // Mapping from holder to list of staking infos 49 | mapping(address => mapping(uint256 => uint256)) private _stakedTokens; 50 | // Mapping from staking index to index of the holder staking list 51 | mapping(uint256 => uint256) private _stakedTokensIndex; 52 | // Mapping from holder to count of staking 53 | mapping(address => uint256) private _stakingCounter; 54 | // Mapping from staking index to address 55 | mapping(uint256 => address) private _stakingHolder; 56 | 57 | /// @custom:oz-upgrades-unsafe-allow constructor 58 | constructor() { 59 | _disableInitializers(); 60 | } 61 | 62 | function initialize(address _EXO_ADDRESS, address _GCRED_ADDRESS) 63 | public 64 | initializer 65 | { 66 | __Pausable_init(); 67 | __AccessControl_init(); 68 | 69 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 70 | _grantRole(OWNER_ROLE, msg.sender); 71 | _grantRole(EXO_ROLE, _EXO_ADDRESS); 72 | 73 | EXO_ADDRESS = _EXO_ADDRESS; 74 | GCRED_ADDRESS = _GCRED_ADDRESS; 75 | } 76 | 77 | /// @inheritdoc IStakingReward 78 | function stake(uint256 _amount, uint8 _duration) 79 | external 80 | override 81 | whenNotPaused 82 | { 83 | address holder = _msgSender(); 84 | require( 85 | _amount <= IERC20Upgradeable(EXO_ADDRESS).balanceOf(holder), 86 | "StakingReward: Not enough EXO token to stake" 87 | ); 88 | require(_duration < 4, "StakingReward: Duration does not match"); 89 | 90 | if (holder == FOUNDATION_NODE) { 91 | // Calculate reward amount from Foudation Node wallet 92 | FN_REWARD = (_amount * 75) / 1000 / 365; 93 | } else { 94 | uint88[4] memory minAmount = _getTierMinAmount(); 95 | uint24[4] memory period = _getStakingPeriod(); 96 | latestStakingTime = block.timestamp; 97 | uint8 interestRate = tier[holder] * 4 + _duration; 98 | 99 | stakingInfos.push( 100 | StakingInfo( 101 | holder, 102 | _amount, 103 | latestStakingTime, 104 | latestStakingTime + uint256(period[_duration]), 105 | _duration, 106 | block.timestamp, 107 | interestRate 108 | ) 109 | ); 110 | // Check user can upgrade tier 111 | if ( 112 | tier[holder] < 3 && 113 | _amount >= uint256(minAmount[tier[holder] + 1]) && 114 | _duration > tier[holder] 115 | ) tierCandidate[holder] = true; 116 | _addStakingToHolderEnumeration(holder, stakingCounter); 117 | } 118 | 119 | IERC20Upgradeable(EXO_ADDRESS).transferFrom( 120 | holder, 121 | address(this), 122 | _amount 123 | ); 124 | emit Stake(holder, _amount, block.timestamp); 125 | } 126 | 127 | function claimBatch() external onlyRole(OWNER_ROLE) whenNotPaused { 128 | require(stakingInfos.length > 0, "StakingReward: Nobody staked"); 129 | require( 130 | latestClaimTime != 0 || 131 | block.timestamp - latestClaimTime >= CLAIM_DELAY, 132 | "StakingReward: Not started new multi claim" 133 | ); 134 | // Staking holder counter in each `interestRate` 135 | uint256[16] memory interestHolderCounter; 136 | 137 | for (uint256 i = 0; i < stakingInfos.length; ) { 138 | address stakingHolder = stakingInfos[i].holder; 139 | uint256 stakingAmount = stakingInfos[i].amount; 140 | uint256 interestRate = stakingInfos[i].interestRate; 141 | if (block.timestamp < stakingInfos[i].expireDate) { 142 | // Claim reward every day 143 | if ( 144 | block.timestamp - stakingInfos[i].latestClaimDate >= 145 | CLAIM_DELAY 146 | ) { 147 | // Count 148 | interestHolderCounter[interestRate] += 1; 149 | // Calculate reward EXO amount 150 | uint256 REWARD_APR = _getEXORewardAPR( 151 | stakingInfos[i].interestRate 152 | ); 153 | uint256 reward = _calcReward(stakingAmount, REWARD_APR); 154 | // Mint reward to staking holder 155 | IEXOToken(EXO_ADDRESS).mint(stakingHolder, reward); 156 | // Calculate GCRED daily reward 157 | uint256 GCRED_REWARD = (stakingInfos[i].amount * 158 | _getGCREDReturn(stakingInfos[i].interestRate)) / 1e6; 159 | // send GCRED to holder 160 | _sendGCRED(stakingHolder, GCRED_REWARD); 161 | // Update latest claimed date 162 | stakingInfos[i].latestClaimDate = block.timestamp; 163 | 164 | emit Claim(stakingHolder, block.timestamp); 165 | } 166 | i++; 167 | } else { 168 | /* The staking date is expired */ 169 | // Upgrade holder's tier 170 | if ( 171 | stakingInfos[i].duration >= tier[stakingHolder] && 172 | tierCandidate[stakingHolder] 173 | ) { 174 | if (tier[stakingHolder] < 3) { 175 | tier[stakingHolder] += 1; 176 | } 177 | tierCandidate[stakingHolder] = false; 178 | } 179 | // Remove holder's staking index array 180 | _removeStakingFromHolderEnumeration(stakingHolder, i); 181 | _removeStakingFromAllStakingEnumeration(i); 182 | // Return staked EXO to holder 183 | IERC20Upgradeable(EXO_ADDRESS).transfer( 184 | stakingHolder, 185 | stakingAmount 186 | ); 187 | emit UnStake(stakingHolder, stakingAmount, block.timestamp); 188 | } 189 | } 190 | _getRewardFromFN(interestHolderCounter); 191 | latestClaimTime = block.timestamp; 192 | } 193 | 194 | /// @inheritdoc IStakingReward 195 | function setEXOAddress(address _EXO_ADDRESS) 196 | external 197 | override 198 | onlyRole(OWNER_ROLE) 199 | { 200 | EXO_ADDRESS = _EXO_ADDRESS; 201 | 202 | emit EXOAddressUpdated(EXO_ADDRESS); 203 | } 204 | 205 | /// @inheritdoc IStakingReward 206 | function setGCREDAddress(address _GCRED_ADDRESS) 207 | external 208 | override 209 | onlyRole(OWNER_ROLE) 210 | { 211 | GCRED_ADDRESS = _GCRED_ADDRESS; 212 | 213 | emit GCREDAddressUpdated(GCRED_ADDRESS); 214 | } 215 | 216 | function setFNAddress(address _FOUNDATION_NODE) 217 | external 218 | override 219 | onlyRole(OWNER_ROLE) 220 | { 221 | FOUNDATION_NODE = _FOUNDATION_NODE; 222 | 223 | emit FoundationNodeUpdated(FOUNDATION_NODE); 224 | } 225 | 226 | function setTier(address _holder, uint8 _tier) 227 | external 228 | override 229 | onlyRole(EXO_ROLE) 230 | { 231 | tier[_holder] = _tier; 232 | } 233 | 234 | function getStakingInfos(address _holder) 235 | external 236 | view 237 | override 238 | returns (StakingInfo[] memory) 239 | { 240 | require(stakingCounter > 0, "EXO: Nobody staked"); 241 | uint256 stakingCount = _stakingCounter[_holder]; 242 | if (stakingCount == 0) { 243 | // Return an empty array 244 | return new StakingInfo[](0); 245 | } else { 246 | StakingInfo[] memory result = new StakingInfo[](stakingCount); 247 | for (uint256 index = 0; index < stakingCount; index++) { 248 | uint256 stakedIndex = stakingOfHolderByIndex(_holder, index); 249 | result[index] = stakingInfos[stakedIndex]; 250 | } 251 | return result; 252 | } 253 | } 254 | 255 | function getStakingCount(address _holder) 256 | external 257 | view 258 | override 259 | returns (uint256) 260 | { 261 | return _stakingCounter[_holder]; 262 | } 263 | 264 | /// @inheritdoc IStakingReward 265 | function getTier(address _user) external view returns (uint8) { 266 | return tier[_user]; 267 | } 268 | 269 | /// @dev Minimum EXO amount in tier 270 | function getTierMinAmount() 271 | external 272 | pure 273 | override 274 | returns (uint88[4] memory) 275 | { 276 | uint88[4] memory tierMinimumAmount = [ 277 | 0, 278 | 2_0000_0000_0000_0000_0000_0000, 279 | 4_0000_0000_0000_0000_0000_0000, 280 | 8_0000_0000_0000_0000_0000_0000 281 | ]; 282 | return tierMinimumAmount; 283 | } 284 | 285 | function pause() public onlyRole(OWNER_ROLE) { 286 | _pause(); 287 | } 288 | 289 | function unpause() public onlyRole(OWNER_ROLE) { 290 | _unpause(); 291 | } 292 | 293 | function stakingOfHolderByIndex(address holder, uint256 index) 294 | public 295 | view 296 | virtual 297 | returns (uint256) 298 | { 299 | require( 300 | index < _stakingCounter[holder], 301 | "StakingReward: invalid staking index" 302 | ); 303 | return _stakedTokens[holder][index]; 304 | } 305 | 306 | function _getRewardFromFN(uint256[16] memory _interestHolderCounter) 307 | internal 308 | { 309 | uint8[16] memory FN_REWARD_PERCENT = _getFNRewardPercent(); 310 | uint256[16] memory _rewardAmountFn; 311 | for (uint256 i = 0; i < 16; i++) { 312 | if (_interestHolderCounter[i] == 0) { 313 | _rewardAmountFn[i] = 0; 314 | } else { 315 | _rewardAmountFn[i] = 316 | (FN_REWARD * uint256(FN_REWARD_PERCENT[i])) / 317 | _interestHolderCounter[i] / 318 | 1000; 319 | } 320 | } 321 | for (uint256 i = 0; i < stakingInfos.length; i++) { 322 | uint256 _rewardAmount = _rewardAmountFn[ 323 | stakingInfos[i].interestRate 324 | ]; 325 | if (_rewardAmount != 0) { 326 | IEXOToken(EXO_ADDRESS).mint( 327 | stakingInfos[i].holder, 328 | _rewardAmount 329 | ); 330 | emit ClaimFN( 331 | stakingInfos[i].holder, 332 | _rewardAmount, 333 | block.timestamp 334 | ); 335 | } 336 | } 337 | } 338 | 339 | /// @dev Staking period 340 | function _getStakingPeriod() internal pure returns (uint24[4] memory) { 341 | uint24[4] memory stakingPeriod = [0, 30 days, 60 days, 90 days]; 342 | return stakingPeriod; 343 | } 344 | 345 | /// @dev Minimum EXO amount in tier 346 | function _getTierMinAmount() internal pure returns (uint88[4] memory) { 347 | uint88[4] memory tierMinimumAmount = [ 348 | 0, 349 | 2_0000_0000_0000_0000_0000_0000, 350 | 4_0000_0000_0000_0000_0000_0000, 351 | 8_0000_0000_0000_0000_0000_0000 352 | ]; 353 | return tierMinimumAmount; 354 | } 355 | 356 | /// @dev EXO Staking reward APR 357 | function _getEXORewardAPR(uint8 _interestRate) 358 | internal 359 | pure 360 | returns (uint8) 361 | { 362 | uint8[16] memory EXO_REWARD_APR = [ 363 | 50, 364 | 55, 365 | 60, 366 | 65, 367 | 60, 368 | 65, 369 | 70, 370 | 75, 371 | 60, 372 | 65, 373 | 70, 374 | 75, 375 | 60, 376 | 65, 377 | 70, 378 | 75 379 | ]; 380 | return EXO_REWARD_APR[_interestRate]; 381 | } 382 | 383 | /// @dev Foundation Node Reward Percent Array 384 | function _getFNRewardPercent() internal pure returns (uint8[16] memory) { 385 | uint8[16] memory FN_REWARD_PERCENT = [ 386 | 0, 387 | 0, 388 | 0, 389 | 0, 390 | 30, 391 | 60, 392 | 85, 393 | 115, 394 | 40, 395 | 70, 396 | 95, 397 | 125, 398 | 50, 399 | 80, 400 | 105, 401 | 145 402 | ]; 403 | return FN_REWARD_PERCENT; 404 | } 405 | 406 | /// @dev GCRED reward per day 407 | function _getGCREDReturn(uint8 _interest) internal pure returns (uint16) { 408 | uint16[16] memory GCRED_RETURN = [ 409 | 0, 410 | 0, 411 | 0, 412 | 242, 413 | 0, 414 | 0, 415 | 266, 416 | 354, 417 | 0, 418 | 0, 419 | 293, 420 | 390, 421 | 0, 422 | 0, 423 | 322, 424 | 426 425 | ]; 426 | return GCRED_RETURN[_interest]; 427 | } 428 | 429 | function _sendGCRED(address _address, uint256 _amount) internal { 430 | IGCREDToken(GCRED_ADDRESS).mintForReward(_address, _amount); 431 | 432 | emit ClaimGCRED(_address, _amount, block.timestamp); 433 | } 434 | 435 | function _calcReward(uint256 _amount, uint256 _percent) 436 | internal 437 | pure 438 | returns (uint256) 439 | { 440 | return (_amount * _percent) / 365000; 441 | } 442 | 443 | function _addStakingToHolderEnumeration( 444 | address holder, 445 | uint256 stakingIndex 446 | ) private { 447 | uint256 length = _stakingCounter[holder]; 448 | _stakedTokens[holder][length] = stakingIndex; 449 | _stakedTokensIndex[stakingIndex] = length; 450 | _stakingHolder[stakingIndex] = holder; 451 | _stakingCounter[holder]++; 452 | stakingCounter++; 453 | } 454 | 455 | function _removeStakingFromHolderEnumeration( 456 | address holder, 457 | uint256 removeStaking 458 | ) private { 459 | // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and 460 | // then delete the last slot (swap and pop). 461 | uint256 lastStakingIndex = _stakingCounter[holder] - 1; 462 | uint256 stakingIndexOfHolder = _stakedTokensIndex[removeStaking]; 463 | 464 | // When the token to delete is the last token, the swap operation is unnecessary 465 | if (stakingIndexOfHolder != lastStakingIndex) { 466 | uint256 lastStakingIndex_ = _stakedTokens[holder][lastStakingIndex]; 467 | 468 | _stakedTokens[holder][stakingIndexOfHolder] = lastStakingIndex_; // Move the last token to the slot of the to-delete token 469 | _stakedTokensIndex[lastStakingIndex_] = stakingIndexOfHolder; // Update the moved token's index 470 | } 471 | 472 | // This also deletes the contents at the last position of the array 473 | delete _stakedTokensIndex[removeStaking]; 474 | delete _stakedTokens[holder][lastStakingIndex]; 475 | _stakingCounter[holder]--; 476 | } 477 | 478 | function _removeStakingFromAllStakingEnumeration(uint256 index) private { 479 | // Update total staking array 480 | uint256 lastStakingIndex = stakingInfos.length - 1; 481 | stakingInfos[index] = stakingInfos[lastStakingIndex]; 482 | stakingInfos.pop(); 483 | stakingCounter--; 484 | 485 | address holder = _stakingHolder[lastStakingIndex]; 486 | uint256 stakingIndexOfHolder = _stakedTokensIndex[lastStakingIndex]; 487 | _stakedTokens[holder][stakingIndexOfHolder] = index; 488 | _stakedTokensIndex[index] = stakingIndexOfHolder; 489 | _stakingHolder[index] = holder; 490 | delete _stakedTokensIndex[lastStakingIndex]; 491 | delete _stakingHolder[lastStakingIndex]; 492 | } 493 | } 494 | --------------------------------------------------------------------------------