├── networks.js ├── contracts ├── test │ ├── TestERC1155.sol │ ├── TestERC721.sol │ ├── TestGysrUtils.sol │ ├── TestFee.sol │ ├── TestStakeUnstake.sol │ ├── TestERC20.sol │ ├── TestElastic.sol │ └── TestReentrant.sol ├── GeyserToken.sol ├── interfaces │ ├── IMetadata.sol │ ├── IModuleFactory.sol │ ├── IOwnerController.sol │ ├── IPoolInfo.sol │ ├── IPoolFactory.sol │ ├── IStakingModuleInfo.sol │ ├── IRewardModuleInfo.sol │ ├── IEvents.sol │ ├── IStakingModule.sol │ ├── IRewardModule.sol │ ├── IConfiguration.sol │ └── IPool.sol ├── AssignmentStakingModuleFactory.sol ├── ERC20StakingModuleFactory.sol ├── ERC721StakingModuleFactory.sol ├── ERC20BondStakingModuleFactory.sol ├── ERC20MultiRewardModuleFactory.sol ├── ERC20FixedRewardModuleFactory.sol ├── ERC20LinearRewardModuleFactory.sol ├── ERC20FriendlyRewardModuleFactory.sol ├── GysrUtils.sol ├── ERC20CompetitiveRewardModuleFactory.sol ├── info │ ├── TokenUtilsInfo.sol │ ├── PoolInfo.sol │ ├── AssignmentStakingModuleInfo.sol │ ├── ERC20StakingModuleInfo.sol │ ├── ERC721StakingModuleInfo.sol │ ├── ERC20FixedRewardModuleInfo.sol │ ├── ERC20LinearRewardModuleInfo.sol │ └── ERC20CompetitiveRewardModuleInfo.sol ├── OwnerController.sol ├── MathUtils.sol ├── Configuration.sol ├── PoolFactory.sol ├── AssignmentStakingModule.sol ├── TokenUtils.sol ├── ERC721StakingModule.sol └── ERC20StakingModule.sol ├── .solcover.js ├── .prettierrc ├── .env.template ├── .github └── workflows │ └── test.yml ├── scripts ├── ii_deploy_pool_info.js ├── i_deploy_token.js ├── i_deploy_config.js ├── iv_deploy_module_info_staking.js ├── ii_deploy_factory.js ├── iv_deploy_module_info_erc721staking.js ├── iv_deploy_module_info_bond.js ├── iv_deploy_module_info_fixed.js ├── iv_deploy_module_info_linear.js ├── iv_deploy_module_info_multi.js ├── iv_deploy_module_info_assignment.js ├── iv_deploy_module_info_friendly.js ├── iv_deploy_module_info_competitive.js ├── iii_deploy_module_factory_staking.js ├── iii_deploy_module_factory_erc721staking.js ├── iii_deploy_module_factory_multi.js ├── iii_deploy_module_factory_bond.js ├── iii_deploy_module_factory_fixed.js ├── iii_deploy_module_factory_linear.js ├── iii_deploy_module_factory_assignment.js ├── iii_deploy_module_factory_friendly.js ├── iii_deploy_module_factory_competitive.js └── abis.js ├── LICENSE ├── package.json ├── hardhat.config.js ├── test ├── unit │ ├── assignmentstakingmodulefactory.js │ ├── mathutils.js │ ├── erc20bondstakingmodulefactory.js │ ├── erc20stakingmodulefactory.js │ ├── erc721stakingmodulefactory.js │ ├── erc20fixedrewardmodulefactory.js │ ├── erc20linearrewardmodulefactory.js │ ├── erc20multirewardmodulefactory.js │ ├── erc20friendlyrewardmodulefactory.js │ ├── erc20competitiverewardmodulefactory.js │ ├── assignmentstakingmoduleinfo.js │ └── ownercontroller.js └── util │ └── helper.js ├── .gitignore └── README.md /networks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | protocol: 'http', 5 | host: 'localhost', 6 | port: 8545, 7 | gas: 5000000, 8 | gasPrice: 5e9, 9 | networkId: '*', 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /contracts/test/TestERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 6 | 7 | contract TestERC1155 is ERC1155 { 8 | constructor() ERC1155("") { 9 | _mint(msg.sender, 0, 10**18, ""); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | 'test/TestElastic.sol', 4 | 'test/TestERC20.sol', 5 | 'test/TestERC721.sol', 6 | 'test/TestERC1155.sol', 7 | 'test/TestFee.sol', 8 | 'test/TestGysrUtils.sol', 9 | 'test/Reentrant.sol', 10 | 'test/StakeUnstake.sol' 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.sol", 5 | "options": { 6 | "printWidth": 80, 7 | "tabWidth": 4, 8 | "useTabs": false, 9 | "singleQuote": false, 10 | "bracketSpacing": false, 11 | "explicitTypes": "always" 12 | } 13 | }, 14 | { 15 | "files": "*.js", 16 | "options": { 17 | "tabWidth": 2, 18 | "useTabs": false, 19 | "singleQuote": true 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /contracts/test/TestERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | 7 | contract TestERC721 is ERC721 { 8 | uint256 public minted; 9 | 10 | constructor() ERC721("TestERC721", "NFT") {} 11 | 12 | function mint(uint256 quantity) public { 13 | for (uint256 i = 0; i < quantity; i++) { 14 | minted = minted + 1; 15 | _mint(msg.sender, minted); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # .env 2 | 3 | # infura ethereum API key 4 | INFURA_KEY= 5 | 6 | # etherscan api key 7 | ETHERSCAN_KEY= 8 | 9 | # ledger derivation path account index 10 | DEPLOYER_INDEX= 11 | 12 | # address of GYSR token contract 13 | GYSR_ADDRESS= 14 | 15 | # address of GYSR configuration contract 16 | CONFIG_ADDRESS= 17 | 18 | # address of main pool factory 19 | FACTORY_ADDRESS= 20 | 21 | # mnemonic phrase for temporary deployer 22 | MNEMONIC_PHRASE= 23 | 24 | # polygonscan api key 25 | POLYGONSCAN_KEY= 26 | 27 | # optimistic etherscan key 28 | OPTIMISTIC_ETHERSCAN_KEY= 29 | -------------------------------------------------------------------------------- /contracts/GeyserToken.sol: -------------------------------------------------------------------------------- 1 | /* 2 | GeyserToken 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 12 | 13 | /** 14 | * @title GYSR token 15 | * 16 | * @notice simple ERC20 compliant contract to implement GYSR token 17 | */ 18 | contract GeyserToken is ERC20 { 19 | uint256 DECIMALS = 18; 20 | uint256 TOTAL_SUPPLY = 10 * 10**6 * 10**DECIMALS; 21 | 22 | constructor() ERC20("Geyser", "GYSR") { 23 | _mint(msg.sender, TOTAL_SUPPLY); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/interfaces/IMetadata.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IMetadata 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Metadata interface 13 | * 14 | * @notice this defines the metadata library interface for tokenized staking modules 15 | */ 16 | interface IMetadata { 17 | /** 18 | * @notice provide the metadata URI for a tokenized staking module position 19 | * @param module address of staking module 20 | * @param id position identifier 21 | * @param data additional encoded data 22 | */ 23 | function metadata( 24 | address module, 25 | uint256 id, 26 | bytes calldata data 27 | ) external view returns (string memory); 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # github action to setup node environment, install dependencies, and run the GYSR core test suite with Hardhat 2 | name: Test 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 14.x 19 | cache: "npm" 20 | 21 | - name: Install system depdencies 22 | run: sudo apt-get update && sudo apt-get install -y libusb-1.0-0-dev libudev-dev 23 | 24 | - name: Install npm dependencies 25 | run: npm ci 26 | 27 | - name: Compile solidity contracts 28 | run: npx hardhat compile 29 | 30 | - name: Run hardhat tests 31 | run: npx hardhat test 32 | -------------------------------------------------------------------------------- /contracts/interfaces/IModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Module factory interface 13 | * 14 | * @notice this defines the common module factory interface used by the 15 | * main factory to create the staking and reward modules for a new Pool. 16 | */ 17 | interface IModuleFactory { 18 | // events 19 | event ModuleCreated(address indexed user, address module); 20 | 21 | /** 22 | * @notice create a new Pool module 23 | * @param config address for configuration contract 24 | * @param data binary encoded construction parameters 25 | * @return address of newly created module 26 | */ 27 | function createModule(address config, bytes calldata data) 28 | external 29 | returns (address); 30 | } 31 | -------------------------------------------------------------------------------- /scripts/ii_deploy_pool_info.js: -------------------------------------------------------------------------------- 1 | // deploy pool info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const PoolInfo = await ethers.getContractFactory('PoolInfo'); 17 | const info = await PoolInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await info.deployed(); 19 | console.log('PoolInfo deployed to:', info.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/i_deploy_token.js: -------------------------------------------------------------------------------- 1 | // deploy GYSR token 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const GeyserToken = await ethers.getContractFactory('GeyserToken'); 17 | const token = await GeyserToken.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await token.deployed(); 19 | console.log('GeyserToken deployed to:', token.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/i_deploy_config.js: -------------------------------------------------------------------------------- 1 | // deploy GYSR protocol config 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const Configuration = await ethers.getContractFactory('Configuration'); 17 | const config = await Configuration.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await config.deployed(); 19 | console.log('Configuration deployed to:', config.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_staking.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 staking info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC20StakingModuleInfo = await ethers.getContractFactory('ERC20StakingModuleInfo'); 17 | const moduleinfo = await ERC20StakingModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC20StakingModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/ii_deploy_factory.js: -------------------------------------------------------------------------------- 1 | // deploy pool factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, GYSR_ADDRESS, CONFIG_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | let PoolFactory = await ethers.getContractFactory('PoolFactory'); 17 | const factory = await PoolFactory.connect(ledger).deploy( 18 | GYSR_ADDRESS, CONFIG_ADDRESS, 19 | { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority } 20 | ); 21 | await factory.deployed(); 22 | console.log('PoolFactory deployed to:', factory.address); 23 | } 24 | 25 | main().catch((error) => { 26 | console.error(error); 27 | process.exitCode = 1; 28 | }); 29 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_erc721staking.js: -------------------------------------------------------------------------------- 1 | // deploy erc721 staking info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC721StakingModuleInfo = await ethers.getContractFactory('ERC721StakingModuleInfo'); 17 | const moduleinfo = await ERC721StakingModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC721StakingModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_bond.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 bond staking info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC20BondStakingModuleInfo = await ethers.getContractFactory('ERC20BondStakingModuleInfo'); 17 | const moduleinfo = await ERC20BondStakingModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC20BondStakingModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_fixed.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 fixed reward info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC20FixedRewardModuleInfo = await ethers.getContractFactory('ERC20FixedRewardModuleInfo'); 17 | const moduleinfo = await ERC20FixedRewardModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC20FixedRewardModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_linear.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 linear info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC20LinearRewardModuleInfo = await ethers.getContractFactory('ERC20LinearRewardModuleInfo'); 17 | const moduleinfo = await ERC20LinearRewardModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC20LinearRewardModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_multi.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 multi reward info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC20MultiRewardModuleInfo = await ethers.getContractFactory('ERC20MultiRewardModuleInfo'); 17 | const moduleinfo = await ERC20MultiRewardModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC20MultiRewardModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_assignment.js: -------------------------------------------------------------------------------- 1 | // deploy assignment staking info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const AssignmentStakingModuleInfo = await ethers.getContractFactory('AssignmentStakingModuleInfo'); 17 | const moduleinfo = await AssignmentStakingModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('AssignmentStakingModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_friendly.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 friendly info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC20FriendlyRewardModuleInfo = await ethers.getContractFactory('ERC20FriendlyRewardModuleInfo'); 17 | const moduleinfo = await ERC20FriendlyRewardModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC20FriendlyRewardModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/iv_deploy_module_info_competitive.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 competitive info library 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | const ERC20CompetitiveRewardModuleInfo = await ethers.getContractFactory('ERC20CompetitiveRewardModuleInfo'); 17 | const moduleinfo = await ERC20CompetitiveRewardModuleInfo.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 18 | await moduleinfo.deployed(); 19 | console.log('ERC20CompetitiveRewardModuleInfo deployed to:', moduleinfo.address); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 gysr-io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/test/TestGysrUtils.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Test contract for GYSR utilities 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "../GysrUtils.sol"; 12 | 13 | /** 14 | * @title Test GYSR utilities 15 | * @dev simple wrapper contract to test GYSR library utilities 16 | */ 17 | contract TestGysrUtils { 18 | using GysrUtils for uint256; 19 | 20 | // dummy event 21 | event Test(uint256 x); 22 | 23 | // read only function to test GYSR bonus calculation 24 | function testGysrBonus( 25 | uint256 gysr, 26 | uint256 amount, 27 | uint256 total, 28 | uint256 ratio 29 | ) public pure returns (uint256) { 30 | return gysr.gysrBonus(amount, total, ratio); 31 | } 32 | 33 | // write function to test GYSR bonus as part of a transaction 34 | function testEventGysrBonus( 35 | uint256 gysr, 36 | uint256 amount, 37 | uint256 total, 38 | uint256 ratio 39 | ) external returns (uint256) { 40 | uint256 bonus = gysr.gysrBonus(amount, total, ratio); 41 | emit Test(bonus); 42 | return bonus; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/AssignmentStakingModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | AssignmentStakingModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./AssignmentStakingModule.sol"; 13 | 14 | /** 15 | * @title Assignment staking module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * AssignmentStakingModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract AssignmentStakingModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule(address, bytes calldata) 28 | external 29 | override 30 | returns (address) 31 | { 32 | // create module 33 | AssignmentStakingModule module = 34 | new AssignmentStakingModule(address(this)); 35 | module.transferOwnership(msg.sender); 36 | 37 | // output 38 | emit ModuleCreated(msg.sender, address(module)); 39 | return address(module); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/interfaces/IOwnerController.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IOwnerController 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Owner controller interface 13 | * 14 | * @notice this defines the interface for any contracts that use the 15 | * owner controller access pattern 16 | */ 17 | interface IOwnerController { 18 | /** 19 | * @dev Returns the address of the current owner. 20 | */ 21 | function owner() external view returns (address); 22 | 23 | /** 24 | * @dev Returns the address of the current controller. 25 | */ 26 | function controller() external view returns (address); 27 | 28 | /** 29 | * @dev Transfers ownership of the contract to a new account (`newOwner`). This can 30 | * include renouncing ownership by transferring to the zero address. 31 | * Can only be called by the current owner. 32 | */ 33 | function transferOwnership(address newOwner) external; 34 | 35 | /** 36 | * @dev Transfers control of the contract to a new account (`newController`). 37 | * Can only be called by the owner. 38 | */ 39 | function transferControl(address newController) external; 40 | } 41 | -------------------------------------------------------------------------------- /contracts/interfaces/IPoolInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IPoolInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Pool info interface 13 | * 14 | * @notice this defines the Pool info contract interface 15 | */ 16 | 17 | interface IPoolInfo { 18 | /** 19 | * @notice get information about the underlying staking and reward modules 20 | * @param pool address of Pool contract 21 | * @return staking module address 22 | * @return reward module address 23 | * @return staking module type 24 | * @return reward module type 25 | */ 26 | function modules( 27 | address pool 28 | ) external view returns (address, address, address, address); 29 | 30 | /** 31 | * @notice get pending rewards for arbitrary Pool and user pair 32 | * @param pool address of Pool contract 33 | * @param addr address of user for preview 34 | * @param stakingdata additional data passed to staking module info library 35 | * @param rewarddata additional data passed to reward module info library 36 | */ 37 | function rewards( 38 | address pool, 39 | address addr, 40 | bytes calldata stakingdata, 41 | bytes calldata rewarddata 42 | ) external view returns (uint256[] memory); 43 | } 44 | -------------------------------------------------------------------------------- /contracts/interfaces/IPoolFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IPoolFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Pool factory interface 13 | * 14 | * @notice this defines the Pool factory interface, primarily intended for 15 | * the Pool contract to interact with 16 | */ 17 | interface IPoolFactory { 18 | /** 19 | * @notice create a new Pool 20 | * @param staking address of factory that will be used to create staking module 21 | * @param reward address of factory that will be used to create reward module 22 | * @param stakingdata construction data for staking module factory 23 | * @param rewarddata construction data for reward module factory 24 | * @return address of newly created Pool 25 | */ 26 | function create( 27 | address staking, 28 | address reward, 29 | bytes calldata stakingdata, 30 | bytes calldata rewarddata 31 | ) external returns (address); 32 | 33 | /** 34 | * @return true if address is a pool created by the factory 35 | */ 36 | function map(address) external view returns (bool); 37 | 38 | /** 39 | * @return address of the nth pool created by the factory 40 | */ 41 | function list(uint256) external view returns (address); 42 | } 43 | -------------------------------------------------------------------------------- /contracts/ERC20StakingModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20StakingModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC20StakingModule.sol"; 13 | 14 | /** 15 | * @title ERC20 staking module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC20StakingModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC20StakingModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule(address, bytes calldata data) 28 | external 29 | override 30 | returns (address) 31 | { 32 | // validate 33 | require(data.length == 32, "smf1"); 34 | 35 | // parse staking token 36 | address token; 37 | assembly { 38 | token := calldataload(100) 39 | } 40 | 41 | // create module 42 | ERC20StakingModule module = 43 | new ERC20StakingModule(token, address(this)); 44 | module.transferOwnership(msg.sender); 45 | 46 | // output 47 | emit ModuleCreated(msg.sender, address(module)); 48 | return address(module); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/ERC721StakingModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC721StakingModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC721StakingModule.sol"; 13 | 14 | /** 15 | * @title ERC721 staking module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC721StakingModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC721StakingModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule(address, bytes calldata data) 28 | external 29 | override 30 | returns (address) 31 | { 32 | // validate 33 | require(data.length == 32, "smnf1"); 34 | 35 | // parse staking token 36 | address token; 37 | assembly { 38 | token := calldataload(100) 39 | } 40 | 41 | // create module 42 | ERC721StakingModule module = 43 | new ERC721StakingModule(token, address(this)); 44 | module.transferOwnership(msg.sender); 45 | 46 | // output 47 | emit ModuleCreated(msg.sender, address(module)); 48 | return address(module); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_staking.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 staking module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC20StakingModuleFactory = await ethers.getContractFactory('ERC20StakingModuleFactory'); 18 | const modulefactory = await ERC20StakingModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC20StakingModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 1, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /contracts/test/TestFee.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | /** 9 | * @title Test fee token 10 | * @dev mocked up transfer fee token 11 | */ 12 | contract TestFeeToken is ERC20 { 13 | uint256 _totalSupply = 10 * 10**6 * 10**18; 14 | address _feeAddress = 0x0000000000000000000000000000000000000FEE; 15 | uint256 _feeAmount = 5; // 5% 16 | 17 | constructor() ERC20("TestFeeToken", "FEE") { 18 | _mint(msg.sender, _totalSupply); 19 | } 20 | 21 | function transfer(address recipient, uint256 amount) 22 | public 23 | virtual 24 | override 25 | returns (bool) 26 | { 27 | _transfer(_msgSender(), recipient, (amount * (100 - _feeAmount)) / 100); 28 | _transfer(_msgSender(), _feeAddress, (amount * _feeAmount) / 100); 29 | return true; 30 | } 31 | 32 | function transferFrom( 33 | address sender, 34 | address recipient, 35 | uint256 amount 36 | ) public virtual override returns (bool) { 37 | super.transferFrom( 38 | sender, 39 | recipient, 40 | (amount * (100 - _feeAmount)) / 100 41 | ); 42 | super.transferFrom(sender, _feeAddress, (amount * _feeAmount) / 100); 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_erc721staking.js: -------------------------------------------------------------------------------- 1 | // deploy erc721 staking module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC721StakingModuleFactory = await ethers.getContractFactory('ERC721StakingModuleFactory'); 18 | const modulefactory = await ERC721StakingModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC721StakingModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 1, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_multi.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 multi reward module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC20MultiRewardModuleFactory = await ethers.getContractFactory('ERC20MultiRewardModuleFactory'); 18 | const modulefactory = await ERC20MultiRewardModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC20MultiModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 2, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_bond.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 bond staking module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC20BondStakingModuleFactory = await ethers.getContractFactory('ERC20BondStakingModuleFactory'); 18 | const modulefactory = await ERC20BondStakingModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC20BondStakingModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 1, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_fixed.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 fixed reward module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC20FixedRewardModuleFactory = await ethers.getContractFactory('ERC20FixedRewardModuleFactory'); 18 | const modulefactory = await ERC20FixedRewardModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC20FixedRewardModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 2, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_linear.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 linear reward module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC20LinearRewardModuleFactory = await ethers.getContractFactory('ERC20LinearRewardModuleFactory'); 18 | const modulefactory = await ERC20LinearRewardModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC20LinearRewardModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 2, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_assignment.js: -------------------------------------------------------------------------------- 1 | // deploy assignment staking module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const AssignmentStakingModuleFactory = await ethers.getContractFactory('AssignmentStakingModuleFactory'); 18 | const modulefactory = await AssignmentStakingModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('AssignmentStakingModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 1, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_friendly.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 friendly reward module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC20FriendlyRewardModuleFactory = await ethers.getContractFactory('ERC20FriendlyRewardModuleFactory'); 18 | const modulefactory = await ERC20FriendlyRewardModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC20FriendlyModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 2, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakingModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IStakingModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Staking module info interface 13 | * 14 | * @notice this contract defines the common interface that any staking module info 15 | * must implement to be compatible with the modular Pool architecture. 16 | */ 17 | interface IStakingModuleInfo { 18 | /** 19 | * @notice convenience function to get all token metadata in a single call 20 | * @param module address of staking module 21 | * @return addresses 22 | * @return names 23 | * @return symbols 24 | * @return decimals 25 | */ 26 | function tokens( 27 | address module 28 | ) 29 | external 30 | view 31 | returns ( 32 | address[] memory, 33 | string[] memory, 34 | string[] memory, 35 | uint8[] memory 36 | ); 37 | 38 | /** 39 | * @notice get all staking positions for user 40 | * @param module address of staking module 41 | * @param addr user address of interest 42 | * @param data additional encoded data 43 | * @return accounts_ 44 | * @return shares_ 45 | */ 46 | function positions( 47 | address module, 48 | address addr, 49 | bytes calldata data 50 | ) external view returns (bytes32[] memory, uint256[] memory); 51 | } 52 | -------------------------------------------------------------------------------- /scripts/iii_deploy_module_factory_competitive.js: -------------------------------------------------------------------------------- 1 | // deploy erc20 competitive reward module factory 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const { ethers, network } = require('hardhat'); 7 | const { LedgerSigner } = require('@anders-t/ethers-ledger'); 8 | 9 | const { DEPLOYER_INDEX, FACTORY_ADDRESS } = process.env; 10 | 11 | 12 | async function main() { 13 | const ledger = new LedgerSigner(ethers.provider, `m/44'/60'/${DEPLOYER_INDEX}'/0/0`); 14 | console.log('Deploying from address:', await ledger.getAddress()) 15 | 16 | // deploy 17 | const ERC20CompetitiveRewardModuleFactory = await ethers.getContractFactory('ERC20CompetitiveRewardModuleFactory'); 18 | const modulefactory = await ERC20CompetitiveRewardModuleFactory.connect(ledger).deploy({ maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority }); 19 | await modulefactory.deployed(); 20 | console.log('ERC20CompetitiveModuleFactory deployed to:', modulefactory.address); 21 | 22 | // whitelist 23 | const PoolFactory = await ethers.getContractFactory('PoolFactory'); 24 | const factory = await PoolFactory.attach(FACTORY_ADDRESS); 25 | const res = await factory.connect(ledger).setWhitelist(modulefactory.address, 2, { maxFeePerGas: network.config.gas, maxPriorityFeePerGas: network.config.priority, gasLimit: 50000 }); 26 | //console.log(res); 27 | } 28 | 29 | main().catch((error) => { 30 | console.error(error); 31 | process.exitCode = 1; 32 | }); 33 | -------------------------------------------------------------------------------- /contracts/interfaces/IRewardModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IRewardModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Reward module info interface 13 | * 14 | * @notice this contract defines the common interface that any reward module info 15 | * must implement to be compatible with the modular Pool architecture. 16 | */ 17 | 18 | interface IRewardModuleInfo { 19 | /** 20 | * @notice get all token metadata 21 | * @param module address of reward module 22 | * @return addresses 23 | * @return names 24 | * @return symbols 25 | * @return decimals 26 | */ 27 | function tokens( 28 | address module 29 | ) 30 | external 31 | view 32 | returns ( 33 | address[] memory, 34 | string[] memory, 35 | string[] memory, 36 | uint8[] memory 37 | ); 38 | 39 | /** 40 | * @notice generic function to get pending reward balances 41 | * @param module address of reward module 42 | * @param account bytes32 account of interest for preview 43 | * @param shares number of shares that would be used 44 | * @param data additional encoded data 45 | * @return estimated reward balances 46 | */ 47 | function rewards( 48 | address module, 49 | bytes32 account, 50 | uint256 shares, 51 | bytes calldata data 52 | ) external view returns (uint256[] memory); 53 | } 54 | -------------------------------------------------------------------------------- /contracts/ERC20BondStakingModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20BondStakingModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC20BondStakingModule.sol"; 13 | 14 | /** 15 | * @title ERC20 bond staking module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC20BondStakingModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC20BondStakingModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule( 28 | address config, 29 | bytes calldata data 30 | ) external override returns (address) { 31 | // validate 32 | require(data.length == 64, "bsmf1"); 33 | 34 | // parse staking token 35 | uint256 period; 36 | bool burndown; 37 | assembly { 38 | period := calldataload(100) 39 | burndown := calldataload(132) 40 | } 41 | 42 | // create module 43 | ERC20BondStakingModule module = new ERC20BondStakingModule( 44 | period, 45 | burndown, 46 | config, 47 | address(this) 48 | ); 49 | module.transferOwnership(msg.sender); 50 | 51 | // output 52 | emit ModuleCreated(msg.sender, address(module)); 53 | return address(module); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/ERC20MultiRewardModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20MultiRewardModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC20MultiRewardModule.sol"; 13 | 14 | /** 15 | * @title ERC20 multi reward module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC20MultiRewardModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC20MultiRewardModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule( 28 | address config, 29 | bytes calldata data 30 | ) external override returns (address) { 31 | // validate 32 | require(data.length == 64, "mrmf1"); 33 | 34 | // parse constructor arguments 35 | uint256 vestingStart; 36 | uint256 vestingPeriod; 37 | assembly { 38 | vestingStart := calldataload(100) 39 | vestingPeriod := calldataload(132) 40 | } 41 | 42 | // create module 43 | ERC20MultiRewardModule module = new ERC20MultiRewardModule( 44 | vestingStart, 45 | vestingPeriod, 46 | config, 47 | address(this) 48 | ); 49 | module.transferOwnership(msg.sender); 50 | 51 | // output 52 | emit ModuleCreated(msg.sender, address(module)); 53 | return address(module); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/test/TestStakeUnstake.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "../interfaces/IPool.sol"; 7 | 8 | /** 9 | * @title Test stake-unstake contract 10 | * @dev mocked up flashloan attack to manipulate GYSR bonus usage 11 | */ 12 | contract TestStakeUnstake { 13 | event UsageCheck(uint256 timestamp, uint256 usage, uint256 balance); 14 | 15 | address private _pool; 16 | 17 | function target(address pool_) external { 18 | _pool = pool_; 19 | IPool pool = IPool(_pool); 20 | IERC20 tkn = IERC20(pool.stakingTokens()[0]); 21 | tkn.approve(pool.stakingModule(), 10**36); 22 | } 23 | 24 | function deposit(address token, uint256 amount) external { 25 | IERC20 tkn = IERC20(token); 26 | tkn.transferFrom(msg.sender, address(this), amount); 27 | } 28 | 29 | function withdraw(address token, uint256 amount) external { 30 | IERC20 tkn = IERC20(token); 31 | tkn.transfer(msg.sender, amount); 32 | } 33 | 34 | function execute(uint256 amount) external { 35 | IPool pool = IPool(_pool); 36 | emit UsageCheck(block.timestamp, pool.usage(), pool.stakingTotals()[0]); 37 | // 38 | pool.stake(amount, "", ""); 39 | emit UsageCheck(block.timestamp, pool.usage(), pool.stakingTotals()[0]); 40 | // 41 | pool.unstake(amount, "", ""); 42 | // 43 | emit UsageCheck(block.timestamp, pool.usage(), pool.stakingTotals()[0]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/ERC20FixedRewardModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20FixedRewardModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC20FixedRewardModule.sol"; 13 | 14 | /** 15 | * @title ERC20 fixed reward module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC20FixedRewardModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC20FixedRewardModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule( 28 | address config, 29 | bytes calldata data 30 | ) external override returns (address) { 31 | // validate 32 | require(data.length == 96, "xrmf1"); 33 | 34 | // parse constructor arguments 35 | address token; 36 | uint256 period; 37 | uint256 rate; 38 | assembly { 39 | token := calldataload(100) 40 | period := calldataload(132) 41 | rate := calldataload(164) 42 | } 43 | 44 | // create module 45 | ERC20FixedRewardModule module = new ERC20FixedRewardModule( 46 | token, 47 | period, 48 | rate, 49 | config, 50 | address(this) 51 | ); 52 | module.transferOwnership(msg.sender); 53 | 54 | // output 55 | emit ModuleCreated(msg.sender, address(module)); 56 | return address(module); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /contracts/ERC20LinearRewardModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20LinearRewardModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC20LinearRewardModule.sol"; 13 | 14 | /** 15 | * @title ERC20 linear reward module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC20LinearRewardModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC20LinearRewardModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule( 28 | address config, 29 | bytes calldata data 30 | ) external override returns (address) { 31 | // validate 32 | require(data.length == 96, "lrmf1"); 33 | 34 | // parse constructor arguments 35 | address token; 36 | uint256 period; 37 | uint256 rate; 38 | assembly { 39 | token := calldataload(100) 40 | period := calldataload(132) 41 | rate := calldataload(164) 42 | } 43 | 44 | // create module 45 | ERC20LinearRewardModule module = new ERC20LinearRewardModule( 46 | token, 47 | period, 48 | rate, 49 | config, 50 | address(this) 51 | ); 52 | module.transferOwnership(msg.sender); 53 | 54 | // output 55 | emit ModuleCreated(msg.sender, address(module)); 56 | return address(module); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /contracts/ERC20FriendlyRewardModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20FriendlyRewardModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC20FriendlyRewardModule.sol"; 13 | 14 | /** 15 | * @title ERC20 friendly reward module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC20FriendlyRewardModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC20FriendlyRewardModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule( 28 | address config, 29 | bytes calldata data 30 | ) external override returns (address) { 31 | // validate 32 | require(data.length == 96, "frmf1"); 33 | 34 | // parse constructor arguments 35 | address token; 36 | uint256 penaltyStart; 37 | uint256 penaltyPeriod; 38 | assembly { 39 | token := calldataload(100) 40 | penaltyStart := calldataload(132) 41 | penaltyPeriod := calldataload(164) 42 | } 43 | 44 | // create module 45 | ERC20FriendlyRewardModule module = new ERC20FriendlyRewardModule( 46 | token, 47 | penaltyStart, 48 | penaltyPeriod, 49 | config, 50 | address(this) 51 | ); 52 | module.transferOwnership(msg.sender); 53 | 54 | // output 55 | emit ModuleCreated(msg.sender, address(module)); 56 | return address(module); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /contracts/GysrUtils.sol: -------------------------------------------------------------------------------- 1 | /* 2 | GysrUtils 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./MathUtils.sol"; 12 | 13 | /** 14 | * @title GYSR utilities 15 | * 16 | * @notice this library implements utility methods for the GYSR multiplier 17 | * and spending mechanics 18 | */ 19 | library GysrUtils { 20 | using MathUtils for int128; 21 | 22 | // constants 23 | uint256 public constant GYSR_PROPORTION = 1e16; // 1% 24 | 25 | /** 26 | * @notice compute GYSR bonus as a function of usage ratio, stake amount, 27 | * and GYSR spent 28 | * @param gysr number of GYSR token applied to bonus 29 | * @param amount number of tokens or shares to unstake 30 | * @param total number of tokens or shares in overall pool 31 | * @param ratio usage ratio from 0 to 1 32 | * @return multiplier value 33 | */ 34 | function gysrBonus( 35 | uint256 gysr, 36 | uint256 amount, 37 | uint256 total, 38 | uint256 ratio 39 | ) internal pure returns (uint256) { 40 | if (amount == 0) { 41 | return 0; 42 | } 43 | if (total == 0) { 44 | return 0; 45 | } 46 | if (gysr == 0) { 47 | return 1e18; 48 | } 49 | 50 | // scale GYSR amount with respect to proportion 51 | uint256 portion = (GYSR_PROPORTION * total) / 1e18; 52 | if (amount > portion) { 53 | gysr = (gysr * portion) / amount; 54 | } 55 | 56 | // 1 + gysr / (0.01 + ratio) 57 | uint256 x = 2 ** 64 + (2 ** 64 * gysr) / (1e16 + ratio); 58 | 59 | return 60 | 1e18 + 61 | (uint256(int256(int128(uint128(x)).logbase10())) * 1e18) / 62 | 2 ** 64; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/test/TestERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | /** 9 | * @title Test token 10 | * @dev basic ERC20 token for testing 11 | */ 12 | contract TestToken is ERC20 { 13 | uint256 _totalSupply = 50 * 10 ** 6 * 10 ** 18; 14 | 15 | constructor() ERC20("TestToken", "TKN") { 16 | _mint(msg.sender, _totalSupply); 17 | } 18 | } 19 | 20 | /** 21 | * @title Test liquidity token 22 | * @dev another basic ERC20 token for testing 23 | */ 24 | contract TestLiquidityToken is ERC20 { 25 | uint256 _totalSupply = 1 * 10 ** 6 * 10 ** 18; 26 | 27 | constructor() ERC20("TestLiquidityToken", "LP-TKN") { 28 | _mint(msg.sender, _totalSupply); 29 | } 30 | } 31 | 32 | /** 33 | * @title Test indivisible token 34 | * @dev test ERC20 token with no decimals 35 | */ 36 | contract TestIndivisibleToken is ERC20 { 37 | uint256 _totalSupply = 1000; 38 | 39 | constructor() ERC20("TestIndivisibleToken", "IND") { 40 | _mint(msg.sender, _totalSupply); 41 | } 42 | 43 | function decimals() public pure override returns (uint8) { 44 | return 0; 45 | } 46 | } 47 | 48 | /** 49 | * @title Test indivisible token 50 | * @dev test ERC20 token with parameterized 51 | */ 52 | contract TestTemplateToken is ERC20 { 53 | uint8 immutable _decimals; 54 | 55 | constructor( 56 | string memory name, 57 | string memory symbol, 58 | uint256 supply, 59 | uint8 decimals 60 | ) ERC20(name, symbol) { 61 | _mint(msg.sender, supply); 62 | _decimals = decimals; 63 | } 64 | 65 | function decimals() public view override returns (uint8) { 66 | return _decimals; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /contracts/ERC20CompetitiveRewardModuleFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20CompetitiveRewardModuleFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IModuleFactory.sol"; 12 | import "./ERC20CompetitiveRewardModule.sol"; 13 | 14 | /** 15 | * @title ERC20 competitive reward module factory 16 | * 17 | * @notice this factory contract handles deployment for the 18 | * ERC20CompetitiveRewardModule contract 19 | * 20 | * @dev it is called by the parent PoolFactory and is responsible 21 | * for parsing constructor arguments before creating a new contract 22 | */ 23 | contract ERC20CompetitiveRewardModuleFactory is IModuleFactory { 24 | /** 25 | * @inheritdoc IModuleFactory 26 | */ 27 | function createModule( 28 | address config, 29 | bytes calldata data 30 | ) external override returns (address) { 31 | // validate 32 | require(data.length == 128, "crmf1"); 33 | 34 | // parse constructor arguments 35 | address token; 36 | uint256 bonusMin; 37 | uint256 bonusMax; 38 | uint256 bonusPeriod; 39 | assembly { 40 | token := calldataload(100) 41 | bonusMin := calldataload(132) 42 | bonusMax := calldataload(164) 43 | bonusPeriod := calldataload(196) 44 | } 45 | 46 | // create module 47 | ERC20CompetitiveRewardModule module = new ERC20CompetitiveRewardModule( 48 | token, 49 | bonusMin, 50 | bonusMax, 51 | bonusPeriod, 52 | config, 53 | address(this) 54 | ); 55 | module.transferOwnership(msg.sender); 56 | 57 | // output 58 | emit ModuleCreated(msg.sender, address(module)); 59 | return address(module); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/test/TestElastic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | /** 8 | * @title Test elastic token 9 | * @dev mocked up elastic supply token 10 | */ 11 | contract TestElasticToken is ERC20 { 12 | uint256 _totalSupply = 50 * 10**6 * 10**18; 13 | 14 | uint256 private _coeff; 15 | 16 | constructor() ERC20("TestElasticToken", "ELASTIC") { 17 | _coeff = 1.0 * 10**18; 18 | _mint(msg.sender, 10 * 10**6 * 10**18); 19 | } 20 | 21 | // read current coefficient 22 | function getCoefficient() public view returns (uint256) { 23 | return _coeff; 24 | } 25 | 26 | // set new value for coefficient 27 | function setCoefficient(uint256 coeff) public { 28 | _coeff = coeff; 29 | } 30 | 31 | // wrap to adjust for coefficient 32 | function totalSupply() public view override returns (uint256) { 33 | return (super.totalSupply() * _coeff) / 1e18; 34 | } 35 | 36 | // wrap to adjust for coefficient 37 | function balanceOf(address account) public view override returns (uint256) { 38 | return (super.balanceOf(account) * _coeff) / 1e18; 39 | } 40 | 41 | // wrap to adjust for inverse of coefficient 42 | function transfer(address recipient, uint256 amount) 43 | public 44 | virtual 45 | override 46 | returns (bool) 47 | { 48 | return super.transfer(recipient, (amount * 1e18) / _coeff); 49 | } 50 | 51 | // wrap to adjust for inverse of coefficient 52 | function transferFrom( 53 | address sender, 54 | address recipient, 55 | uint256 amount 56 | ) public virtual override returns (bool) { 57 | return super.transferFrom(sender, recipient, (amount * 1e18) / _coeff); 58 | } 59 | 60 | // note: leave allowances to be handled in base units 61 | } 62 | -------------------------------------------------------------------------------- /contracts/interfaces/IEvents.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IEvents 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title GYSR event system 13 | * 14 | * @notice common interface to define GYSR event system 15 | */ 16 | interface IEvents { 17 | // staking 18 | event Staked( 19 | bytes32 indexed account, 20 | address indexed user, 21 | address indexed token, 22 | uint256 amount, 23 | uint256 shares 24 | ); 25 | event Unstaked( 26 | bytes32 indexed account, 27 | address indexed user, 28 | address indexed token, 29 | uint256 amount, 30 | uint256 shares 31 | ); 32 | event Claimed( 33 | bytes32 indexed account, 34 | address indexed user, 35 | address indexed token, 36 | uint256 amount, 37 | uint256 shares 38 | ); 39 | event Updated(bytes32 indexed account, address indexed user); 40 | 41 | // rewards 42 | event RewardsDistributed( 43 | address indexed user, 44 | address indexed token, 45 | uint256 amount, 46 | uint256 shares 47 | ); 48 | event RewardsFunded( 49 | address indexed token, 50 | uint256 amount, 51 | uint256 shares, 52 | uint256 timestamp 53 | ); 54 | event RewardsExpired( 55 | address indexed token, 56 | uint256 amount, 57 | uint256 shares, 58 | uint256 timestamp 59 | ); 60 | event RewardsWithdrawn( 61 | address indexed token, 62 | uint256 amount, 63 | uint256 shares, 64 | uint256 timestamp 65 | ); 66 | event RewardsUpdated(bytes32 indexed account); 67 | 68 | // gysr 69 | event GysrSpent(address indexed user, uint256 amount); 70 | event GysrVested(address indexed user, uint256 amount); 71 | event GysrWithdrawn(uint256 amount); 72 | event Fee(address indexed receiver, address indexed token, uint256 amount); 73 | } 74 | -------------------------------------------------------------------------------- /contracts/info/TokenUtilsInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | TokenUtilsInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 12 | 13 | /** 14 | * @title Token utilities info 15 | * 16 | * @notice this library implements utility methods for token handling, 17 | * dynamic balance accounting, and fee processing. 18 | * 19 | * this is a modified version to be used by info libraries. 20 | */ 21 | library TokenUtilsInfo { 22 | uint256 constant INITIAL_SHARES_PER_TOKEN = 1e6; 23 | uint256 constant FLOOR_SHARES_PER_TOKEN = 1e3; 24 | 25 | /** 26 | * @notice get token shares from amount 27 | * @param token erc20 token interface 28 | * @param module address of module 29 | * @param total current total shares 30 | * @param amount balance of tokens 31 | */ 32 | function getShares( 33 | IERC20 token, 34 | address module, 35 | uint256 total, 36 | uint256 amount 37 | ) internal view returns (uint256) { 38 | if (total == 0) return 0; 39 | uint256 balance = token.balanceOf(module); 40 | if (total < balance * FLOOR_SHARES_PER_TOKEN) 41 | return amount * FLOOR_SHARES_PER_TOKEN; 42 | return (total * amount) / balance; 43 | } 44 | 45 | /** 46 | * @notice get token amount from shares 47 | * @param token erc20 token interface 48 | * @param module address of module 49 | * @param total current total shares 50 | * @param shares balance of shares 51 | */ 52 | function getAmount( 53 | IERC20 token, 54 | address module, 55 | uint256 total, 56 | uint256 shares 57 | ) internal view returns (uint256) { 58 | if (total == 0) return 0; 59 | uint256 balance = token.balanceOf(module); 60 | if (total < balance * FLOOR_SHARES_PER_TOKEN) 61 | return shares / FLOOR_SHARES_PER_TOKEN; 62 | return (balance * shares) / total; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gysr/core", 3 | "version": "3.0.0", 4 | "description": "GYSR core protocol contracts, interfaces, libraries, and ABIs", 5 | "files": [ 6 | "contracts/*.sol", 7 | "contracts/info/*.sol", 8 | "contracts/interfaces/*.sol", 9 | "abis/*.json", 10 | "abis/info/*.json", 11 | "abis/interfaces/*.json", 12 | "!contracts/test" 13 | ], 14 | "dependencies": { 15 | "@openzeppelin/contracts": "^4.8.3" 16 | }, 17 | "devDependencies": { 18 | "@anders-t/ethers-ledger": "^1.0.4", 19 | "@nomiclabs/hardhat-ethers": "^2.1.1", 20 | "@nomiclabs/hardhat-etherscan": "^3.1.0", 21 | "@nomiclabs/hardhat-truffle5": "^2.0.6", 22 | "@nomiclabs/hardhat-waffle": "^2.0.3", 23 | "@nomiclabs/hardhat-web3": "^2.0.0", 24 | "@openzeppelin/test-helpers": "^0.5.16", 25 | "app-root-path": "^3.0.0", 26 | "bn-chai": "^1.0.1", 27 | "chai": "^4.2.0", 28 | "dotenv": "^10.0.0", 29 | "ethereum-waffle": "^3.4.4", 30 | "ethers": "^5.7.0", 31 | "hardhat": "^2.13.0", 32 | "hardhat-contract-sizer": "^2.6.1", 33 | "hardhat-gas-reporter": "^1.0.8", 34 | "micromatch": "^4.0.5", 35 | "mocha": "^8.1.1", 36 | "prettier": "^2.7.1", 37 | "prettier-plugin-solidity": "^1.1.2", 38 | "solidity-coverage": "^0.8.2" 39 | }, 40 | "scripts": { 41 | "test": "hardhat compile && hardhat test", 42 | "coverage": "hardhat clean && hardhat coverage", 43 | "stage": "hardhat compile && node scripts/abis.js && npm publish ./stage --dry-run", 44 | "package": "hardhat compile && node scripts/abis.js && npm publish ./stage --access public" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/gysr-io/core.git" 49 | }, 50 | "keywords": [ 51 | "gysr", 52 | "core", 53 | "solidity", 54 | "ethereum", 55 | "smart-contracts", 56 | "defi" 57 | ], 58 | "author": "gysr.io ", 59 | "license": "MIT", 60 | "bugs": { 61 | "url": "https://github.com/gysr-io/core/issues" 62 | }, 63 | "homepage": "gysr.io" 64 | } 65 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | // configuration for hardhat project 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | require('@nomiclabs/hardhat-truffle5'); 7 | require('@nomiclabs/hardhat-waffle'); // for deployment 8 | require("@nomiclabs/hardhat-etherscan"); 9 | require('hardhat-gas-reporter'); 10 | require('hardhat-contract-sizer'); 11 | require('solidity-coverage'); 12 | 13 | const { INFURA_KEY, ETHERSCAN_KEY, POLYGONSCAN_KEY, OPTIMISTIC_ETHERSCAN_KEY } = process.env; 14 | 15 | module.exports = { 16 | solidity: { 17 | version: '0.8.18', 18 | settings: { 19 | optimizer: { 20 | enabled: true, 21 | runs: 10000 22 | } 23 | } 24 | }, 25 | networks: { 26 | goerli: { 27 | url: `https://goerli.infura.io/v3/${INFURA_KEY}`, 28 | chainId: 5, 29 | gas: 1000000000, // 1 gwei 30 | priority: 100000000, // 0.1 gwei 31 | }, 32 | mainnet: { 33 | url: `https://mainnet.infura.io/v3/${INFURA_KEY}`, 34 | chainId: 1, 35 | gas: 20000000000, // 20 gwei 36 | priority: 1000000000, // 1 gwei 37 | }, 38 | polygon: { 39 | url: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`, 40 | chainId: 137, 41 | gas: 200000000000, // 200 gwei 42 | priority: 50000000000, // 50 gwei 43 | }, 44 | optimism: { 45 | url: `https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`, 46 | chainId: 10, 47 | gas: 1000000, // 0.001 gwei 48 | priority: 1000, // 0.000001 gwei 49 | } 50 | }, 51 | gasReporter: { 52 | enabled: true, 53 | outputFile: 'gas_report.txt', 54 | forceConsoleOutput: true, 55 | excludeContracts: [ 56 | 'TestToken', 57 | 'TestLiquidityToken', 58 | 'TestIndivisibleToken', 59 | 'TestReentrantToken', 60 | 'TestReentrantProxy', 61 | 'TestERC721', 62 | 'TestERC1155', 63 | 'TestFeeToken', 64 | 'TestElasticToken', 65 | 'TestStakeUnstake', 66 | 'TestTemplateToken', 67 | ] 68 | }, 69 | etherscan: { 70 | apiKey: { 71 | mainnet: ETHERSCAN_KEY, 72 | goerli: ETHERSCAN_KEY, 73 | polygon: POLYGONSCAN_KEY, 74 | optimisticEthereum: OPTIMISTIC_ETHERSCAN_KEY 75 | } 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /scripts/abis.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // prepare abis for packaging 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const micromatch = require('micromatch'); 7 | 8 | const stage = './stage' 9 | const artifacts = './artifacts/contracts'; 10 | console.log('Staging ABIs and contracts for packaging...'); 11 | 12 | const pkg = JSON.parse(fs.readFileSync('package.json')); 13 | const included = pkg.files.filter(f => !f.startsWith('!') && !f.startsWith('abi')); 14 | 15 | // setup package staging directory 16 | if (!fs.existsSync(stage)) { 17 | fs.mkdirSync(stage); 18 | fs.mkdirSync(path.join(stage, 'abis')); 19 | fs.mkdirSync(path.join(stage, 'abis', 'interfaces')); 20 | fs.mkdirSync(path.join(stage, 'abis', 'info')); 21 | fs.mkdirSync(path.join(stage, 'contracts')); 22 | fs.mkdirSync(path.join(stage, 'contracts', 'interfaces')); 23 | fs.mkdirSync(path.join(stage, 'contracts', 'info')); 24 | } 25 | fs.copyFileSync('package.json', path.join(stage, 'package.json')); 26 | fs.copyFileSync('README.md', path.join(stage, 'README.md')); 27 | fs.copyFileSync('LICENSE', path.join(stage, 'LICENSE')); 28 | 29 | // list files recursively 30 | function getFiles(dir, files) { 31 | files = files || []; 32 | fs.readdirSync(dir).forEach(file => { 33 | const abs = path.join(dir, file); 34 | if (fs.statSync(abs).isDirectory()) { 35 | files = getFiles(abs, files); 36 | } 37 | else if (!abs.includes('.dbg')) { 38 | files.push(abs); 39 | } 40 | }); 41 | return files; 42 | } 43 | const files = getFiles(artifacts); 44 | 45 | for (const f of files) { 46 | const artifact = JSON.parse(fs.readFileSync(f)); 47 | const source = path.relative('.', artifact.sourceName); 48 | 49 | if (micromatch.any(source, included)) { 50 | // abi 51 | const subpath = path.dirname(path.relative('./contracts', source)); 52 | const name = path.basename(f); 53 | console.log(source, subpath, name); 54 | fs.writeFileSync(path.join(stage, 'abis', subpath, name), JSON.stringify(artifact.abi, null, 2)); 55 | 56 | // contract 57 | const src = fs.readFileSync(source, 'utf8'); 58 | const replaced = src.replace(/0.8.18;/, '^0.8.18;') 59 | fs.writeFileSync(path.join(stage, source), replaced, 'utf8') 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/unit/assignmentstakingmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for AssignmentStakingModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert, constants } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { reportGas } = require('../util/helper'); 8 | 9 | const AssignmentStakingModule = artifacts.require('AssignmentStakingModule'); 10 | const AssignmentStakingModuleFactory = artifacts.require('AssignmentStakingModuleFactory'); 11 | 12 | describe('AssignmentStakingModuleFactory', function () { 13 | let org, owner, alice, config; 14 | before(async function () { 15 | [org, owner, alice, config] = await web3.eth.getAccounts(); 16 | }); 17 | 18 | beforeEach('setup', async function () { 19 | this.factory = await AssignmentStakingModuleFactory.new({ from: org }); 20 | }); 21 | 22 | describe('when a module is created with factory', function () { 23 | 24 | beforeEach(async function () { 25 | // create module with factory 26 | this.res = await this.factory.createModule(config, [], { from: owner }); 27 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 28 | this.module = await AssignmentStakingModule.at(this.addr); 29 | }); 30 | 31 | it('should emit ModuleCreated event', async function () { 32 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 33 | }); 34 | 35 | it('should set staking token properly', async function () { 36 | expect((await this.module.tokens())[0]).to.equal(constants.ZERO_ADDRESS); 37 | }); 38 | 39 | it('should set owner to message sender', async function () { 40 | expect(await this.module.owner()).to.equal(owner); 41 | }); 42 | 43 | it('should set have zero user balance', async function () { 44 | expect((await this.module.balances(alice))[0]).to.bignumber.equal(new BN(0)); 45 | }); 46 | 47 | it('should set have zero total balance', async function () { 48 | expect((await this.module.totals())[0]).to.bignumber.equal(new BN(0)); 49 | }); 50 | 51 | it('should set factory identifier properly', async function () { 52 | expect(await this.module.factory()).to.equal(this.factory.address); 53 | }); 54 | 55 | it('gas cost', async function () { 56 | reportGas('AssignmentStakingModuleFactory', 'createModule', '', this.res); 57 | }); 58 | 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | build/ 108 | .openzeppelin/ 109 | .openzeppelin/.session 110 | 111 | truffle.js 112 | truffle-config.js 113 | 114 | .vscode/ 115 | 116 | abis/ 117 | artifacts/ 118 | cache/ 119 | stage/ 120 | 121 | coverage/ 122 | coverage.json 123 | 124 | gas_report.txt 125 | gas_report_legacy.txt 126 | -------------------------------------------------------------------------------- /test/unit/mathutils.js: -------------------------------------------------------------------------------- 1 | // test module for MathUtils 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { toFixedPointBigNumber, fromFixedPointBigNumber } = require('../util/helper'); 8 | 9 | const MathUtils = artifacts.require('MathUtils'); 10 | 11 | 12 | describe('testing helpers (yes, meta tests...)', function () { 13 | 14 | it('should encode integer to fixed point big number', function () { 15 | const bn = toFixedPointBigNumber(42, 10, 18); 16 | expect(bn).to.be.bignumber.equal(new BN(42).mul((new BN(10)).pow(new BN(18)))); 17 | }); 18 | 19 | it('should encode decimal to fixed point big number', function () { 20 | const bn = toFixedPointBigNumber(1.23456, 10, 18); 21 | expect(bn).to.be.bignumber.equal(new BN('1234560000000000000')); 22 | }); 23 | 24 | it('should encode irregular unit decimal to fixed point big number', function () { 25 | const bn = toFixedPointBigNumber(5.4321, 10, 9); 26 | expect(bn).to.be.bignumber.equal(new BN('5432100000')); 27 | }); 28 | 29 | it('should decode fixed point big number to integer', function () { 30 | const bn = new BN(42).mul((new BN(10)).pow(new BN(18))); 31 | const x = fromFixedPointBigNumber(bn, 10, 18); 32 | expect(x).to.be.equal(42); 33 | }); 34 | 35 | it('should decode fixed point big number to decimal', function () { 36 | const bn = new BN('1234560000000000000'); 37 | const x = fromFixedPointBigNumber(bn, 10, 18); 38 | expect(x).to.be.equal(1.23456); 39 | }); 40 | 41 | it('should decode irregular unit fixed point big number to decimal', function () { 42 | const bn = new BN('5432100000'); 43 | const x = fromFixedPointBigNumber(bn, 10, 9); 44 | expect(x).to.be.equal(5.4321); 45 | }); 46 | }); 47 | 48 | describe('math utils library', function () { 49 | 50 | it('should compute log2 properly', async function () { 51 | const math = await MathUtils.new(); 52 | const coeff = (new BN(2)).pow(new BN(64)); 53 | var x = new BN(500); 54 | x = x.mul(coeff); 55 | const ybn = await math.testlogbase2(x); 56 | const y = fromFixedPointBigNumber(ybn, 2, 64); 57 | expect(y).to.be.approximately(Math.log2(500), 0.000001) 58 | }); 59 | 60 | it('should compute log10 properly', async function () { 61 | const math = await MathUtils.new(); 62 | const coeff = (new BN(2)).pow(new BN(64)); 63 | for (var i = 0; i < 8; i++) { 64 | var x = new BN(10 ** i); 65 | x = x.mul(coeff); 66 | var y = await math.testlogbase10(x); 67 | y = y.div(coeff); 68 | expect(y.toNumber()).to.be.equal(i); 69 | } 70 | }); 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GYSR core 2 | 3 | This repository contains the Solidity contracts for the GYSR core procotol, including modular pools, factory system, and token. 4 | 5 | For more information on the project, whitepapers, audits, and other resources, 6 | see [gysr.io](https://www.gysr.io/) 7 | 8 | 9 | ## Install 10 | 11 | To use the core contracts, interfaces, libraries, or ABIs in your own project 12 | 13 | ``` 14 | npm install @gysr/core 15 | ``` 16 | 17 | See the [documentation](https://docs.gysr.io/developers) to learn more about interacting with the GYSR protocol. 18 | 19 | *Note: the package is published with solidity `^0.8.18` compatibility, but core contracts have only been tested and audited with solidity `0.8.18` exact.* 20 | 21 | 22 | ## Development 23 | 24 | Both **Node.js** and **npm** are required for package management and testing. See instructions 25 | for installation [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). 26 | 27 | This project uses [Hardhat](https://hardhat.org/docs) for development, testing, and deployment. 28 | 29 | To install these packages along with other dependencies: 30 | ``` 31 | npm install 32 | ``` 33 | 34 | 35 | ## Test 36 | 37 | To run all unit tests 38 | ``` 39 | npm test 40 | ``` 41 | 42 | To run some subset of tests 43 | ``` 44 | npx hardhat compile && npx hardhat test --grep ERC20CompetitiveRewardModule 45 | ``` 46 | 47 | 48 | ## Deploy 49 | 50 | Copy `.env.template` to `.env` and define the `INFURA_KEY`, `DEPLOYER_INDEX`, 51 | and `ETHERSCAN_KEY` variables. 52 | 53 | 54 | To deploy GYSR token to Goerli 55 | ``` 56 | npx hardhat run --network goerli scripts/i_deploy_token.js 57 | ``` 58 | 59 | Once GYSR token is deployed, define the `GYSR_ADDRESS` variable in your `.env` file. 60 | 61 | 62 | To deploy the configuration contract to Goerli 63 | ``` 64 | npx hardhat run --network goerli scripts/i_deploy_config.js 65 | ``` 66 | 67 | Once the configuration contract is deployed, define the `CONFIG_ADDRESS` variable in your `.env` file. 68 | 69 | 70 | To deploy the factory contract to Goerli 71 | ``` 72 | npx hardhat run --network goerli scripts/ii_deploy_factory.js 73 | ``` 74 | 75 | Once the factory is deployed, define the `FACTORY_ADDRESS` variable in your `.env` file. 76 | 77 | 78 | To deploy the ERC20 staking module factory to Goerli 79 | ``` 80 | npx hardhat run --network goerli scripts/iii_deploy_module_factory_staking.js 81 | ``` 82 | 83 | 84 | To deploy the ERC20 competitive reward module factory to Goerli 85 | ``` 86 | npx hardhat run --network goerli scripts/iii_deploy_module_factory_competitive.js 87 | ``` 88 | 89 | Follow the remaining migration steps to deploy all contracts and libraries. 90 | 91 | 92 | To verify a contract on Goerli 93 | ``` 94 | npx hardhat verify --network goerli --contract contracts/PoolFactory.sol:PoolFactory 0xpoolfactory 0xgysrtoken 0xconfig 95 | ``` 96 | -------------------------------------------------------------------------------- /test/unit/erc20bondstakingmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC20BondStakingModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const ERC20BondStakingModule = artifacts.require('ERC20BondStakingModule'); 8 | const ERC20BondStakingModuleFactory = artifacts.require('ERC20BondStakingModuleFactory'); 9 | 10 | 11 | describe('ERC20BondStakingModuleFactory', function () { 12 | let org, owner, alice, config; 13 | before(async function () { 14 | [org, owner, alice, config] = await web3.eth.getAccounts(); 15 | }); 16 | 17 | beforeEach('setup', async function () { 18 | this.factory = await ERC20BondStakingModuleFactory.new({ from: org }); 19 | }); 20 | 21 | describe('when constructor parameters are not encoded properly', function () { 22 | 23 | it('should fail', async function () { 24 | const data = "0x0de0b6b3a7640000"; // not a full 64 bytes 25 | await expectRevert( 26 | this.factory.createModule(config, data, { from: owner }), 27 | 'bsmf1' // ERC20BondStakingModuleFactory: invalid data 28 | ); 29 | }); 30 | }); 31 | 32 | describe('when a module is created with factory', function () { 33 | 34 | beforeEach(async function () { 35 | // encode bond period and burndown flag as bytes 36 | const data = web3.eth.abi.encodeParameters( 37 | ['uint256', 'bool'], 38 | ['86400', true] 39 | ); 40 | 41 | // create module with factory 42 | this.res = await this.factory.createModule(config, data, { from: owner }); 43 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 44 | this.module = await ERC20BondStakingModule.at(this.addr); 45 | }); 46 | 47 | it('should set period', async function () { 48 | expect(await this.module.period()).to.bignumber.equal(new BN(86400)); 49 | }); 50 | 51 | it('should set burndown', async function () { 52 | expect(await this.module.burndown()).to.be.true; 53 | }); 54 | 55 | it('should set factory identifier', async function () { 56 | expect(await this.module.factory()).to.equal(this.factory.address); 57 | }); 58 | 59 | it('should set the initial nonce', async function () { 60 | expect(await this.module.nonce()).to.be.bignumber.equal(new BN(1)); 61 | }) 62 | 63 | it('should have empty list for total balances', async function () { 64 | expect((await this.module.totals()).length).to.equal(0); 65 | }); 66 | 67 | it('should set owner to message sender', async function () { 68 | expect(await this.module.owner()).to.equal(owner); 69 | }); 70 | 71 | it('should emit ModuleCreated event', async function () { 72 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 73 | }); 74 | 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/unit/erc20stakingmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC20StakingModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { reportGas } = require('../util/helper'); 8 | 9 | const ERC20StakingModule = artifacts.require('ERC20StakingModule'); 10 | const ERC20StakingModuleFactory = artifacts.require('ERC20StakingModuleFactory'); 11 | const TestToken = artifacts.require('TestToken'); 12 | 13 | 14 | describe('ERC20StakingModuleFactory', function () { 15 | let org, owner, alice, config; 16 | before(async function () { 17 | [org, owner, alice, config] = await web3.eth.getAccounts(); 18 | }); 19 | 20 | beforeEach('setup', async function () { 21 | this.factory = await ERC20StakingModuleFactory.new({ from: org }); 22 | this.token = await TestToken.new({ from: org }); 23 | }); 24 | 25 | describe('when constructor parameters are not encoded properly constructed', function () { 26 | 27 | it('should fail', async function () { 28 | const data = "0x0de0b6b3a7640000"; // not a full 32 bytes 29 | await expectRevert( 30 | this.factory.createModule(config, data, { from: owner }), 31 | 'smf1' // ERC20StakingModuleFactory: invalid data 32 | ); 33 | }); 34 | }); 35 | 36 | describe('when a module is created with factory', function () { 37 | 38 | beforeEach(async function () { 39 | // encode staking token as bytes 40 | const data = web3.eth.abi.encodeParameter('address', this.token.address); 41 | 42 | // create module with factory 43 | this.res = await this.factory.createModule(config, data, { from: owner }); 44 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 45 | this.module = await ERC20StakingModule.at(this.addr); 46 | }); 47 | 48 | it('should emit ModuleCreated event', async function () { 49 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 50 | }); 51 | 52 | it('should set staking token properly', async function () { 53 | expect((await this.module.tokens())[0]).to.equal(this.token.address); 54 | }); 55 | 56 | it('should set owner to message sender', async function () { 57 | expect(await this.module.owner()).to.equal(owner); 58 | }); 59 | 60 | it('should set have zero user balance', async function () { 61 | expect((await this.module.balances(alice))[0]).to.bignumber.equal(new BN(0)); 62 | }); 63 | 64 | it('should set have zero total balance', async function () { 65 | expect((await this.module.totals())[0]).to.bignumber.equal(new BN(0)); 66 | }); 67 | 68 | it('should set factory identifier properly', async function () { 69 | expect(await this.module.factory()).to.equal(this.factory.address); 70 | }); 71 | 72 | it('gas cost', async function () { 73 | reportGas('ERC20StakingModuleFactory', 'createModule', '', this.res); 74 | }); 75 | 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /test/unit/erc721stakingmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC721StakingModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { reportGas } = require('../util/helper'); 8 | 9 | const ERC721StakingModule = artifacts.require('ERC721StakingModule'); 10 | const ERC721StakingModuleFactory = artifacts.require('ERC721StakingModuleFactory'); 11 | const TestERC721 = artifacts.require('TestERC721'); 12 | 13 | describe('ERC721StakingModuleFactory', function () { 14 | let org, owner, alice, config; 15 | before(async function () { 16 | [org, owner, alice, config] = await web3.eth.getAccounts(); 17 | }); 18 | 19 | beforeEach('setup', async function () { 20 | this.factory = await ERC721StakingModuleFactory.new({ from: org }); 21 | this.token = await TestERC721.new({ from: org }); 22 | }); 23 | 24 | describe('when constructor parameters are not encoded properly constructed', function () { 25 | 26 | it('should fail', async function () { 27 | const data = "0x0de0b6b3a7640000"; // not a full 32 bytes 28 | await expectRevert( 29 | this.factory.createModule(config, data, { from: owner }), 30 | 'smnf1' // ERC721StakingModuleFactory: invalid data 31 | ); 32 | }); 33 | }); 34 | 35 | describe('when a module is created with factory', function () { 36 | 37 | beforeEach(async function () { 38 | // encode staking token as bytes 39 | const data = web3.eth.abi.encodeParameter('address', this.token.address); 40 | 41 | // create module with factory 42 | this.res = await this.factory.createModule(config, data, { from: owner }); 43 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 44 | this.module = await ERC721StakingModule.at(this.addr); 45 | }); 46 | 47 | it('should emit ModuleCreated event', async function () { 48 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 49 | }); 50 | 51 | it('should set staking token properly', async function () { 52 | expect((await this.module.tokens())[0]).to.equal(this.token.address); 53 | }); 54 | 55 | it('should set owner to message sender', async function () { 56 | expect(await this.module.owner()).to.equal(owner); 57 | }); 58 | 59 | it('should set have zero user balance', async function () { 60 | expect((await this.module.balances(alice))[0]).to.bignumber.equal(new BN(0)); 61 | }); 62 | 63 | it('should set have zero total balance', async function () { 64 | expect((await this.module.totals())[0]).to.bignumber.equal(new BN(0)); 65 | }); 66 | 67 | it('should set factory identifier properly', async function () { 68 | expect(await this.module.factory()).to.equal(this.factory.address); 69 | }); 70 | 71 | it('gas cost', async function () { 72 | reportGas('ERC721StakingModuleFactory', 'createModule', '', this.res); 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /contracts/info/PoolInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | PoolInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "../interfaces/IPoolInfo.sol"; 12 | import "../interfaces/IPool.sol"; 13 | import "../interfaces/IStakingModule.sol"; 14 | import "../interfaces/IRewardModule.sol"; 15 | import "../interfaces/IStakingModuleInfo.sol"; 16 | import "../interfaces/IRewardModuleInfo.sol"; 17 | import "../OwnerController.sol"; 18 | 19 | /** 20 | * @title Pool info library 21 | * 22 | * @notice this implements the Pool info library, which provides read-only 23 | * convenience functions to query additional information and metadata 24 | * about the core Pool contract. 25 | */ 26 | 27 | contract PoolInfo is IPoolInfo, OwnerController { 28 | mapping(address => address) public registry; 29 | 30 | /** 31 | * @inheritdoc IPoolInfo 32 | */ 33 | function modules( 34 | address pool 35 | ) public view override returns (address, address, address, address) { 36 | IPool p = IPool(pool); 37 | IStakingModule s = IStakingModule(p.stakingModule()); 38 | IRewardModule r = IRewardModule(p.rewardModule()); 39 | return (address(s), address(r), s.factory(), r.factory()); 40 | } 41 | 42 | /** 43 | * @notice register factory to info module 44 | * @param factory address of factory 45 | * @param info address of info module contract 46 | */ 47 | function register(address factory, address info) external onlyController { 48 | registry[factory] = info; 49 | } 50 | 51 | /** 52 | * @inheritdoc IPoolInfo 53 | */ 54 | function rewards( 55 | address pool, 56 | address addr, 57 | bytes calldata stakingdata, 58 | bytes calldata rewarddata 59 | ) public view override returns (uint256[] memory rewards_) { 60 | address stakingModule; 61 | address rewardModule; 62 | IStakingModuleInfo stakingModuleInfo; 63 | IRewardModuleInfo rewardModuleInfo; 64 | { 65 | address stakingModuleType; 66 | address rewardModuleType; 67 | ( 68 | stakingModule, 69 | rewardModule, 70 | stakingModuleType, 71 | rewardModuleType 72 | ) = modules(pool); 73 | 74 | stakingModuleInfo = IStakingModuleInfo(registry[stakingModuleType]); 75 | rewardModuleInfo = IRewardModuleInfo(registry[rewardModuleType]); 76 | } 77 | 78 | rewards_ = new uint256[](IPool(pool).rewardTokens().length); 79 | 80 | (bytes32[] memory accounts, uint256[] memory shares) = stakingModuleInfo 81 | .positions(stakingModule, addr, stakingdata); 82 | 83 | for (uint256 i; i < accounts.length; ++i) { 84 | uint256[] memory r = rewardModuleInfo.rewards( 85 | rewardModule, 86 | accounts[i], 87 | shares[i], 88 | rewarddata 89 | ); 90 | for (uint256 j; j < r.length; ++j) rewards_[j] += r[j]; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/unit/erc20fixedrewardmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC20FixedRewardModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { e18, days } = require('../util/helper'); 8 | 9 | const ERC20FixedRewardModule = artifacts.require('ERC20FixedRewardModule'); 10 | const ERC20FixedRewardModuleFactory = artifacts.require('ERC20FixedRewardModuleFactory'); 11 | const TestToken = artifacts.require('TestToken'); 12 | 13 | 14 | describe('ERC20FixedRewardModuleFactory', function () { 15 | let org, owner, alice, config; 16 | before(async function () { 17 | [org, owner, alice, config] = await web3.eth.getAccounts(); 18 | }); 19 | 20 | beforeEach('setup', async function () { 21 | this.factory = await ERC20FixedRewardModuleFactory.new({ from: org }); 22 | this.token = await TestToken.new({ from: org }); 23 | }); 24 | 25 | describe('when constructor parameters are not encoded properly', function () { 26 | 27 | it('should fail', async function () { 28 | // missing an argument 29 | const data = web3.eth.abi.encodeParameters( 30 | ['address', 'uint256'], 31 | [this.token.address, e18(0).toString()] 32 | ); 33 | 34 | await expectRevert( 35 | this.factory.createModule(config, data, { from: owner }), 36 | 'xrmf1' // ERC20FixedRewardModuleFactory: invalid constructor data 37 | ); 38 | }); 39 | }); 40 | 41 | describe('when a module is created with factory', function () { 42 | 43 | beforeEach(async function () { 44 | // encode configuration parameters as bytes 45 | const data = web3.eth.abi.encodeParameters( 46 | ['address', 'uint256', 'uint256'], 47 | [this.token.address, days(30).toString(), e18(42).toString()] 48 | ); 49 | 50 | // create module with factory 51 | this.res = await this.factory.createModule(config, data, { from: owner }); 52 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 53 | this.module = await ERC20FixedRewardModule.at(this.addr); 54 | }); 55 | 56 | it('should set reward token properly', async function () { 57 | expect((await this.module.tokens())[0]).to.equal(this.token.address); 58 | }); 59 | 60 | it('should set vesting period properly', async function () { 61 | expect(await this.module.period()).to.be.bignumber.equal(days(30)); 62 | }); 63 | 64 | it('should set rate properly', async function () { 65 | expect(await this.module.rate()).to.be.bignumber.equal(e18(42)); 66 | }); 67 | 68 | it('should set owner to message sender', async function () { 69 | expect(await this.module.owner()).to.equal(owner); 70 | }); 71 | 72 | it('should set have zero reward balances', async function () { 73 | expect((await this.module.balances())[0]).to.bignumber.equal(new BN(0)); 74 | }); 75 | 76 | it('should set factory identifier properly', async function () { 77 | expect(await this.module.factory()).to.equal(this.factory.address); 78 | }); 79 | 80 | it('should emit ModuleCreated event', async function () { 81 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 82 | }); 83 | 84 | }); 85 | 86 | }); 87 | -------------------------------------------------------------------------------- /test/unit/erc20linearrewardmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC20LinearRewardModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { e18, days } = require('../util/helper'); 8 | 9 | const ERC20LinearRewardModule = artifacts.require('ERC20LinearRewardModule'); 10 | const ERC20LinearRewardModuleFactory = artifacts.require('ERC20LinearRewardModuleFactory'); 11 | const TestToken = artifacts.require('TestToken'); 12 | 13 | 14 | describe('ERC20LinearRewardModuleFactory', function () { 15 | let org, owner, alice, config; 16 | before(async function () { 17 | [org, owner, alice, config] = await web3.eth.getAccounts(); 18 | }); 19 | 20 | beforeEach('setup', async function () { 21 | this.factory = await ERC20LinearRewardModuleFactory.new({ from: org }); 22 | this.token = await TestToken.new({ from: org }); 23 | }); 24 | 25 | describe('when constructor parameters are not encoded properly', function () { 26 | 27 | it('should fail', async function () { 28 | // missing an argument 29 | const data = web3.eth.abi.encodeParameters( 30 | ['address', 'uint256'], 31 | [this.token.address, e18(0).toString()] 32 | ); 33 | 34 | await expectRevert( 35 | this.factory.createModule(config, data, { from: owner }), 36 | 'lrmf1' // ERC20LinearRewardModuleFactory: invalid constructor data 37 | ); 38 | }); 39 | }); 40 | 41 | describe('when a module is created with factory', function () { 42 | 43 | beforeEach(async function () { 44 | // encode configuration parameters as bytes 45 | const data = web3.eth.abi.encodeParameters( 46 | ['address', 'uint256', 'uint256'], 47 | [this.token.address, days(30).toString(), e18(1).toString()] 48 | ); 49 | 50 | // create module with factory 51 | this.res = await this.factory.createModule(config, data, { from: owner }); 52 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 53 | this.module = await ERC20LinearRewardModule.at(this.addr); 54 | }); 55 | 56 | it('should emit ModuleCreated event', async function () { 57 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 58 | }); 59 | 60 | it('should set reward token properly', async function () { 61 | expect((await this.module.tokens())[0]).to.equal(this.token.address); 62 | }); 63 | 64 | it('should set time period properly', async function () { 65 | expect(await this.module.period()).to.be.bignumber.equal(days(30)); 66 | }); 67 | 68 | it('should set earning rate properly', async function () { 69 | expect(await this.module.rate()).to.be.bignumber.equal(e18(1)); 70 | }); 71 | 72 | it('should set owner to message sender', async function () { 73 | expect(await this.module.owner()).to.equal(owner); 74 | }); 75 | 76 | it('should set have zero reward balances', async function () { 77 | expect((await this.module.balances())[0]).to.bignumber.equal(new BN(0)); 78 | }); 79 | 80 | it('should set factory identifier properly', async function () { 81 | expect(await this.module.factory()).to.equal(this.factory.address); 82 | }); 83 | 84 | }); 85 | 86 | }); 87 | -------------------------------------------------------------------------------- /test/unit/erc20multirewardmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC20MultiRewardModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { e18, days } = require('../util/helper'); 8 | 9 | const ERC20MultiRewardModule = artifacts.require('ERC20MultiRewardModule'); 10 | const ERC20MultiRewardModuleFactory = artifacts.require('ERC20MultiRewardModuleFactory'); 11 | 12 | 13 | describe('ERC20MultiRewardModuleFactory', function () { 14 | let org, owner, alice, config; 15 | before(async function () { 16 | [org, owner, alice, config] = await web3.eth.getAccounts(); 17 | }); 18 | 19 | beforeEach('setup', async function () { 20 | this.factory = await ERC20MultiRewardModuleFactory.new({ from: org }); 21 | }); 22 | 23 | describe('when constructor parameters are not encoded properly', function () { 24 | it('should fail', async function () { 25 | // missing an argument 26 | const data = web3.eth.abi.encodeParameters(['uint256'], [e18(0.25)]); 27 | await expectRevert( 28 | this.factory.createModule(config, data, { from: owner }), 29 | 'mrmf1' // ERC20MultiRewardModuleFactory: invalid constructor data 30 | ); 31 | }); 32 | }); 33 | 34 | describe('when vesting start is greater than one', function () { 35 | it('should fail', async function () { 36 | // missing an argument 37 | const data = web3.eth.abi.encodeParameters(['uint256', 'uint256'], [e18(1.01), days(30)]); 38 | await expectRevert( 39 | this.factory.createModule(config, data, { from: owner }), 40 | 'mrm1' 41 | ); 42 | }); 43 | }); 44 | 45 | 46 | describe('when a module is created with factory', function () { 47 | 48 | beforeEach(async function () { 49 | // encode configuration parameters as bytes 50 | const data = web3.eth.abi.encodeParameters(['uint256', 'uint256'], [e18(0.25), days(30)]); 51 | 52 | // create module with factory 53 | this.res = await this.factory.createModule(config, data, { from: owner }); 54 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 55 | this.module = await ERC20MultiRewardModule.at(this.addr); 56 | }); 57 | 58 | it('should set vesting start properly', async function () { 59 | expect(await this.module.vestingStart()).to.be.bignumber.equal(e18(0.25)); 60 | }); 61 | 62 | it('should set vesting period properly', async function () { 63 | expect(await this.module.vestingPeriod()).to.be.bignumber.equal(days(30)); 64 | }); 65 | 66 | it('should have no reward tokens', async function () { 67 | expect((await this.module.tokens()).length).to.equal(0); 68 | }); 69 | 70 | it('should have no reward balances', async function () { 71 | expect((await this.module.balances()).length).to.equal(0); 72 | }); 73 | 74 | it('should set owner to message sender', async function () { 75 | expect(await this.module.owner()).to.equal(owner); 76 | }); 77 | 78 | it('should set factory identifier properly', async function () { 79 | expect(await this.module.factory()).to.equal(this.factory.address); 80 | }); 81 | 82 | it('should emit ModuleCreated event', async function () { 83 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 84 | }); 85 | 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /contracts/info/AssignmentStakingModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | AssignmentStakingModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "../interfaces/IStakingModule.sol"; 12 | import "../AssignmentStakingModule.sol"; 13 | 14 | /** 15 | * @title Assignment staking module info library 16 | * 17 | * @notice this library provides read-only convenience functions to query 18 | * additional information about the ERC20StakingModule contract. 19 | */ 20 | library AssignmentStakingModuleInfo { 21 | // -- IStakingModuleInfo -------------------------------------------------- 22 | 23 | /** 24 | * @notice convenient function to get all token metadata in a single call 25 | * @param module address of reward module 26 | * @return addresses_ 27 | * @return names_ 28 | * @return symbols_ 29 | * @return decimals_ 30 | */ 31 | function tokens( 32 | address module 33 | ) 34 | external 35 | view 36 | returns ( 37 | address[] memory addresses_, 38 | string[] memory names_, 39 | string[] memory symbols_, 40 | uint8[] memory decimals_ 41 | ) 42 | { 43 | addresses_ = new address[](1); 44 | names_ = new string[](1); 45 | symbols_ = new string[](1); 46 | decimals_ = new uint8[](1); 47 | } 48 | 49 | /** 50 | * @notice get all staking positions for user 51 | * @param module address of staking module 52 | * @param addr user address of interest 53 | * @param data additional encoded data 54 | * @return accounts_ 55 | * @return shares_ 56 | */ 57 | function positions( 58 | address module, 59 | address addr, 60 | bytes calldata data 61 | ) 62 | external 63 | view 64 | returns (bytes32[] memory accounts_, uint256[] memory shares_) 65 | { 66 | uint256 s = shares(module, addr, 0); 67 | if (s > 0) { 68 | accounts_ = new bytes32[](1); 69 | shares_ = new uint256[](1); 70 | accounts_[0] = bytes32(uint256(uint160(addr))); 71 | shares_[0] = s; 72 | } 73 | } 74 | 75 | // -- AssignmentStakingModuleInfo ----------------------------------------- 76 | 77 | /** 78 | * @notice quote the share value for an amount of tokens 79 | * @param module address of staking module 80 | * @param addr account address of interest 81 | * @param amount number of tokens. if zero, return entire share balance 82 | * @return number of shares 83 | */ 84 | function shares( 85 | address module, 86 | address addr, 87 | uint256 amount 88 | ) public view returns (uint256) { 89 | AssignmentStakingModule m = AssignmentStakingModule(module); 90 | 91 | // return all user shares 92 | if (amount == 0) { 93 | return m.rates(addr); 94 | } 95 | 96 | require(amount <= m.rates(addr), "smai1"); 97 | return amount * m.SHARES_COEFF(); 98 | } 99 | 100 | /** 101 | * @notice get shares per unit 102 | * @param module address of staking module 103 | * @return current shares per token 104 | */ 105 | function sharesPerToken(address module) public view returns (uint256) { 106 | AssignmentStakingModule m = AssignmentStakingModule(module); 107 | return m.SHARES_COEFF() * 1e18; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /contracts/test/TestReentrant.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/token/ERC777/ERC777.sol"; 6 | import "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol"; 7 | import "@openzeppelin/contracts/utils/introspection/ERC1820Implementer.sol"; 8 | import "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol"; 9 | import "../interfaces/IPool.sol"; 10 | 11 | /** 12 | * @title Test reentrant token 13 | * @dev basic ERC777 token for reentrancy testing 14 | */ 15 | contract TestReentrantToken is ERC777 { 16 | uint256 _totalSupply = 10 * 10**6 * 10**18; 17 | 18 | constructor() ERC777("ReentrantToken", "RE", new address[](0)) { 19 | _mint(msg.sender, _totalSupply, "", ""); 20 | } 21 | } 22 | 23 | /** 24 | * @title Test reentrancy proxy 25 | * @dev mocked up reentrancy attack 26 | */ 27 | contract TestReentrantProxy is IERC777Sender, ERC1820Implementer { 28 | address private _pool; 29 | uint256 private _last; 30 | uint256 private _amount; 31 | uint256 private _mode; 32 | 33 | constructor() { 34 | _pool = address(0); 35 | _last = 0; 36 | _amount = 0; 37 | _mode = 0; 38 | } 39 | 40 | function register( 41 | bytes32 interfaceHash, 42 | address addr, 43 | address registry 44 | ) external { 45 | _registerInterfaceForAddress(interfaceHash, addr); 46 | IERC1820Registry reg = IERC1820Registry(registry); 47 | reg.setInterfaceImplementer( 48 | address(this), 49 | interfaceHash, 50 | address(this) 51 | ); 52 | } 53 | 54 | function target( 55 | address pool, 56 | uint256 amount, 57 | uint256 mode 58 | ) external { 59 | _pool = pool; 60 | _amount = amount; 61 | _mode = mode; 62 | } 63 | 64 | function deposit(address token, uint256 amount) external { 65 | IERC20 tkn = IERC20(token); 66 | IPool pool = IPool(_pool); 67 | uint256 temp = _mode; 68 | _mode = 0; 69 | tkn.transferFrom(msg.sender, address(this), amount); 70 | _mode = temp; 71 | tkn.approve(pool.stakingModule(), 100000 * 10**18); 72 | } 73 | 74 | function withdraw(address token, uint256 amount) external { 75 | IERC20 tkn = IERC20(token); 76 | uint256 temp = _mode; 77 | _mode = 0; 78 | tkn.transfer(msg.sender, amount); 79 | _mode = temp; 80 | } 81 | 82 | function stake(uint256 amount) external { 83 | IPool pool = IPool(_pool); 84 | pool.stake(amount, "", ""); 85 | } 86 | 87 | function unstake(uint256 amount) external { 88 | IPool pool = IPool(_pool); 89 | pool.unstake(amount, "", ""); 90 | } 91 | 92 | function tokensToSend( 93 | address, 94 | address, 95 | address, 96 | uint256, 97 | bytes calldata, 98 | bytes calldata 99 | ) external override { 100 | if (block.timestamp == _last) { 101 | return; 102 | } 103 | _last = block.timestamp; 104 | _exploit(); 105 | } 106 | 107 | function _exploit() private { 108 | if (_pool == address(0)) { 109 | return; 110 | } 111 | IPool pool = IPool(_pool); 112 | if (_mode == 1) { 113 | pool.stake(_amount, "", ""); 114 | } else if (_mode == 2) { 115 | pool.unstake(_amount, "", ""); 116 | } else if (_mode == 3) { 117 | pool.update("", ""); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakingModule.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IStakingModule 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 12 | 13 | import "./IEvents.sol"; 14 | import "./IOwnerController.sol"; 15 | 16 | /** 17 | * @title Staking module interface 18 | * 19 | * @notice this contract defines the common interface that any staking module 20 | * must implement to be compatible with the modular Pool architecture. 21 | */ 22 | interface IStakingModule is IOwnerController, IEvents { 23 | /** 24 | * @return array of staking tokens 25 | */ 26 | function tokens() external view returns (address[] memory); 27 | 28 | /** 29 | * @notice get balance of user 30 | * @param user address of user 31 | * @return balances of each staking token 32 | */ 33 | function balances(address user) external view returns (uint256[] memory); 34 | 35 | /** 36 | * @return address of module factory 37 | */ 38 | function factory() external view returns (address); 39 | 40 | /** 41 | * @notice get total staked amount 42 | * @return totals for each staking token 43 | */ 44 | function totals() external view returns (uint256[] memory); 45 | 46 | /** 47 | * @notice stake an amount of tokens for user 48 | * @param sender address of sender 49 | * @param amount number of tokens to stake 50 | * @param data additional data 51 | * @return bytes32 id of staking account 52 | * @return number of shares minted for stake 53 | */ 54 | function stake( 55 | address sender, 56 | uint256 amount, 57 | bytes calldata data 58 | ) external returns (bytes32, uint256); 59 | 60 | /** 61 | * @notice unstake an amount of tokens for user 62 | * @param sender address of sender 63 | * @param amount number of tokens to unstake 64 | * @param data additional data 65 | * @return bytes32 id of staking account 66 | * @return address of reward receiver 67 | * @return number of shares burned for unstake 68 | */ 69 | function unstake( 70 | address sender, 71 | uint256 amount, 72 | bytes calldata data 73 | ) external returns (bytes32, address, uint256); 74 | 75 | /** 76 | * @notice quote the share value for an amount of tokens without unstaking 77 | * @param sender address of sender 78 | * @param amount number of tokens to claim with 79 | * @param data additional data 80 | * @return bytes32 id of staking account 81 | * @return address of reward receiver 82 | * @return number of shares that the claim amount is worth 83 | */ 84 | function claim( 85 | address sender, 86 | uint256 amount, 87 | bytes calldata data 88 | ) external returns (bytes32, address, uint256); 89 | 90 | /** 91 | * @notice method called by anyone to update accounting 92 | * @dev will only be called ad hoc and should not contain essential logic 93 | * @param sender address of user for update 94 | * @param data additional data 95 | * @return bytes32 id of staking account 96 | */ 97 | function update( 98 | address sender, 99 | bytes calldata data 100 | ) external returns (bytes32); 101 | 102 | /** 103 | * @notice method called by owner to clean up and perform additional accounting 104 | * @dev will only be called ad hoc and should not contain any essential logic 105 | * @param data additional data 106 | */ 107 | function clean(bytes calldata data) external; 108 | } 109 | -------------------------------------------------------------------------------- /test/unit/erc20friendlyrewardmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC20FriendlyRewardModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { bonus, days, reportGas, FEE } = require('../util/helper'); 8 | 9 | const ERC20FriendlyRewardModule = artifacts.require('ERC20FriendlyRewardModule'); 10 | const ERC20FriendlyRewardModuleFactory = artifacts.require('ERC20FriendlyRewardModuleFactory'); 11 | const TestToken = artifacts.require('TestToken'); 12 | 13 | 14 | describe('ERC20FriendlyRewardModuleFactory', function () { 15 | let org, owner, alice, config; 16 | before(async function () { 17 | [org, owner, alice, config] = await web3.eth.getAccounts(); 18 | }); 19 | 20 | beforeEach('setup', async function () { 21 | this.factory = await ERC20FriendlyRewardModuleFactory.new({ from: org }); 22 | this.token = await TestToken.new({ from: org }); 23 | }); 24 | 25 | describe('when constructor parameters are not encoded properly constructed', function () { 26 | 27 | it('should fail', async function () { 28 | // missing an argument 29 | const data = web3.eth.abi.encodeParameters( 30 | ['address', 'uint256'], 31 | [this.token.address, bonus(0).toString()] 32 | ); 33 | 34 | await expectRevert( 35 | this.factory.createModule(config, data, { from: owner }), 36 | 'frmf1' // ERC20FriendlyRewardModuleFactory: invalid constructor data 37 | ); 38 | }); 39 | }); 40 | 41 | describe('when a module is created with factory', function () { 42 | 43 | beforeEach(async function () { 44 | // encode configuration parameters as bytes 45 | const data = web3.eth.abi.encodeParameters( 46 | ['address', 'uint256', 'uint256'], 47 | [this.token.address, bonus(0).toString(), days(90).toString()] 48 | ); 49 | 50 | // create module with factory 51 | this.res = await this.factory.createModule(config, data, { from: owner }); 52 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 53 | this.module = await ERC20FriendlyRewardModule.at(this.addr); 54 | }); 55 | 56 | it('should emit ModuleCreated event', async function () { 57 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 58 | }); 59 | 60 | it('should set reward token properly', async function () { 61 | expect((await this.module.tokens())[0]).to.equal(this.token.address); 62 | }); 63 | 64 | it('should set minimum time bonus properly', async function () { 65 | expect(await this.module.vestingStart()).to.be.bignumber.equal(new BN(0)); 66 | }); 67 | 68 | it('should set time bonus period properly', async function () { 69 | expect(await this.module.vestingPeriod()).to.be.bignumber.equal(days(90)); 70 | }); 71 | 72 | it('should set owner to message sender', async function () { 73 | expect(await this.module.owner()).to.equal(owner); 74 | }); 75 | 76 | it('should set have zero reward balances', async function () { 77 | expect((await this.module.balances())[0]).to.bignumber.equal(new BN(0)); 78 | }); 79 | 80 | it('should have zero usage ratio', async function () { 81 | expect(await this.module.usage()).to.be.bignumber.equal(new BN(0)); 82 | }); 83 | 84 | it('should set factory identifier properly', async function () { 85 | expect(await this.module.factory()).to.equal(this.factory.address); 86 | }); 87 | 88 | it('report gas', async function () { 89 | reportGas('ERC20FriendlyRewardModuleFactory', 'createModule', '', this.res) 90 | }); 91 | 92 | }); 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /contracts/OwnerController.sol: -------------------------------------------------------------------------------- 1 | /* 2 | OwnerController 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IOwnerController.sol"; 12 | 13 | /** 14 | * @title Owner controller 15 | * 16 | * @notice this base contract implements an owner-controller access model. 17 | * 18 | * @dev the contract is an adapted version of the OpenZeppelin Ownable contract. 19 | * It allows the owner to designate an additional account as the controller to 20 | * perform restricted operations. 21 | * 22 | * Other changes include supporting role verification with a require method 23 | * in addition to the modifier option, and removing some unneeded functionality. 24 | * 25 | * Original contract here: 26 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol 27 | */ 28 | contract OwnerController is IOwnerController { 29 | address private _owner; 30 | address private _controller; 31 | 32 | event OwnershipTransferred( 33 | address indexed previousOwner, 34 | address indexed newOwner 35 | ); 36 | 37 | event ControlTransferred( 38 | address indexed previousController, 39 | address indexed newController 40 | ); 41 | 42 | constructor() { 43 | _owner = msg.sender; 44 | _controller = msg.sender; 45 | emit OwnershipTransferred(address(0), _owner); 46 | emit ControlTransferred(address(0), _owner); 47 | } 48 | 49 | /** 50 | * @dev Returns the address of the current owner. 51 | */ 52 | function owner() public view override returns (address) { 53 | return _owner; 54 | } 55 | 56 | /** 57 | * @dev Returns the address of the current controller. 58 | */ 59 | function controller() public view override returns (address) { 60 | return _controller; 61 | } 62 | 63 | /** 64 | * @dev Modifier that throws if called by any account other than the owner. 65 | */ 66 | modifier onlyOwner() { 67 | require(_owner == msg.sender, "oc1"); 68 | _; 69 | } 70 | 71 | /** 72 | * @dev Modifier that throws if called by any account other than the controller. 73 | */ 74 | modifier onlyController() { 75 | require(_controller == msg.sender, "oc2"); 76 | _; 77 | } 78 | 79 | /** 80 | * @dev Throws if called by any account other than the owner. 81 | */ 82 | function requireOwner() internal view { 83 | require(_owner == msg.sender, "oc1"); 84 | } 85 | 86 | /** 87 | * @dev Throws if called by any account other than the controller. 88 | */ 89 | function requireController() internal view { 90 | require(_controller == msg.sender, "oc2"); 91 | } 92 | 93 | /** 94 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 95 | * Can only be called by the current owner. 96 | */ 97 | function transferOwnership(address newOwner) public virtual override { 98 | requireOwner(); 99 | require(newOwner != address(0), "oc3"); 100 | emit OwnershipTransferred(_owner, newOwner); 101 | _owner = newOwner; 102 | } 103 | 104 | /** 105 | * @dev Transfers control of the contract to a new account (`newController`). 106 | * Can only be called by the owner. 107 | */ 108 | function transferControl(address newController) public virtual override { 109 | requireOwner(); 110 | require(newController != address(0), "oc4"); 111 | emit ControlTransferred(_controller, newController); 112 | _controller = newController; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /contracts/MathUtils.sol: -------------------------------------------------------------------------------- 1 | /* 2 | MathUtils 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: BSD-4-Clause 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Math utilities 13 | * 14 | * @notice this library implements various logarithmic math utilies which support 15 | * other contracts and specifically the GYSR multiplier calculation 16 | * 17 | * @dev h/t https://github.com/abdk-consulting/abdk-libraries-solidity 18 | */ 19 | library MathUtils { 20 | /** 21 | * @notice calculate binary logarithm of x 22 | * 23 | * @param x signed 64.64-bit fixed point number, require x > 0 24 | * @return signed 64.64-bit fixed point number 25 | */ 26 | function logbase2(int128 x) internal pure returns (int128) { 27 | unchecked { 28 | require(x > 0); 29 | 30 | int256 msb = 0; 31 | int256 xc = x; 32 | if (xc >= 0x10000000000000000) { 33 | xc >>= 64; 34 | msb += 64; 35 | } 36 | if (xc >= 0x100000000) { 37 | xc >>= 32; 38 | msb += 32; 39 | } 40 | if (xc >= 0x10000) { 41 | xc >>= 16; 42 | msb += 16; 43 | } 44 | if (xc >= 0x100) { 45 | xc >>= 8; 46 | msb += 8; 47 | } 48 | if (xc >= 0x10) { 49 | xc >>= 4; 50 | msb += 4; 51 | } 52 | if (xc >= 0x4) { 53 | xc >>= 2; 54 | msb += 2; 55 | } 56 | if (xc >= 0x2) msb += 1; // No need to shift xc anymore 57 | 58 | int256 result = (msb - 64) << 64; 59 | uint256 ux = uint256(int256(x)) << uint256(127 - msb); 60 | for (int256 bit = 0x8000000000000000; bit > 0; bit >>= 1) { 61 | ux *= ux; 62 | uint256 b = ux >> 255; 63 | ux >>= 127 + b; 64 | result += bit * int256(b); 65 | } 66 | 67 | return int128(result); 68 | } 69 | } 70 | 71 | /** 72 | * @notice calculate natural logarithm of x 73 | * @dev magic constant comes from ln(2) * 2^128 -> hex 74 | * @param x signed 64.64-bit fixed point number, require x > 0 75 | * @return signed 64.64-bit fixed point number 76 | */ 77 | function ln(int128 x) internal pure returns (int128) { 78 | unchecked { 79 | require(x > 0); 80 | 81 | return 82 | int128( 83 | int256( 84 | (uint256(int256(logbase2(x))) * 85 | 0xB17217F7D1CF79ABC9E3B39803F2F6AF) >> 128 86 | ) 87 | ); 88 | } 89 | } 90 | 91 | /** 92 | * @notice calculate logarithm base 10 of x 93 | * @dev magic constant comes from log10(2) * 2^128 -> hex 94 | * @param x signed 64.64-bit fixed point number, require x > 0 95 | * @return signed 64.64-bit fixed point number 96 | */ 97 | function logbase10(int128 x) internal pure returns (int128) { 98 | require(x > 0); 99 | 100 | return 101 | int128( 102 | int256( 103 | (uint256(int256(logbase2(x))) * 104 | 0x4d104d427de7fce20a6e420e02236748) >> 128 105 | ) 106 | ); 107 | } 108 | 109 | // wrapper functions to allow testing 110 | function testlogbase2(int128 x) public pure returns (int128) { 111 | return logbase2(x); 112 | } 113 | 114 | function testlogbase10(int128 x) public pure returns (int128) { 115 | return logbase10(x); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /contracts/interfaces/IRewardModule.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IRewardModule 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 12 | 13 | import "./IEvents.sol"; 14 | import "./IOwnerController.sol"; 15 | 16 | /** 17 | * @title Reward module interface 18 | * 19 | * @notice this contract defines the common interface that any reward module 20 | * must implement to be compatible with the modular Pool architecture. 21 | */ 22 | interface IRewardModule is IOwnerController, IEvents { 23 | /** 24 | * @return array of reward tokens 25 | */ 26 | function tokens() external view returns (address[] memory); 27 | 28 | /** 29 | * @return array of reward token balances 30 | */ 31 | function balances() external view returns (uint256[] memory); 32 | 33 | /** 34 | * @return GYSR usage ratio for reward module 35 | */ 36 | function usage() external view returns (uint256); 37 | 38 | /** 39 | * @return address of module factory 40 | */ 41 | function factory() external view returns (address); 42 | 43 | /** 44 | * @notice perform any necessary accounting for new stake 45 | * @param account bytes32 id of staking account 46 | * @param sender address of sender 47 | * @param shares number of new shares minted 48 | * @param data addtional data 49 | * @return amount of gysr spent 50 | * @return amount of gysr vested 51 | */ 52 | function stake( 53 | bytes32 account, 54 | address sender, 55 | uint256 shares, 56 | bytes calldata data 57 | ) external returns (uint256, uint256); 58 | 59 | /** 60 | * @notice reward user and perform any necessary accounting for unstake 61 | * @param account bytes32 id of staking account 62 | * @param sender address of sender 63 | * @param receiver address of reward receiver 64 | * @param shares number of shares burned 65 | * @param data additional data 66 | * @return amount of gysr spent 67 | * @return amount of gysr vested 68 | */ 69 | function unstake( 70 | bytes32 account, 71 | address sender, 72 | address receiver, 73 | uint256 shares, 74 | bytes calldata data 75 | ) external returns (uint256, uint256); 76 | 77 | /** 78 | * @notice reward user and perform and necessary accounting for existing stake 79 | * @param account bytes32 id of staking account 80 | * @param sender address of sender 81 | * @param receiver address of reward receiver 82 | * @param shares number of shares being claimed against 83 | * @param data additional data 84 | * @return amount of gysr spent 85 | * @return amount of gysr vested 86 | */ 87 | function claim( 88 | bytes32 account, 89 | address sender, 90 | address receiver, 91 | uint256 shares, 92 | bytes calldata data 93 | ) external returns (uint256, uint256); 94 | 95 | /** 96 | * @notice method called by anyone to update accounting 97 | * @dev will only be called ad hoc and should not contain essential logic 98 | * @param account bytes32 id of staking account for update 99 | * @param sender address of sender 100 | * @param data additional data 101 | */ 102 | function update( 103 | bytes32 account, 104 | address sender, 105 | bytes calldata data 106 | ) external; 107 | 108 | /** 109 | * @notice method called by owner to clean up and perform additional accounting 110 | * @dev will only be called ad hoc and should not contain any essential logic 111 | * @param data additional data 112 | */ 113 | function clean(bytes calldata data) external; 114 | } 115 | -------------------------------------------------------------------------------- /contracts/Configuration.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Configuration 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IConfiguration.sol"; 12 | import "./OwnerController.sol"; 13 | 14 | /** 15 | * @title Configuration 16 | * 17 | * @notice configuration contract to define global variables for GYSR protocol 18 | */ 19 | contract Configuration is IConfiguration, OwnerController { 20 | // data 21 | mapping(bytes32 => uint256) private _data; 22 | mapping(address => mapping(bytes32 => uint256)) _overrides; 23 | 24 | /** 25 | * @inheritdoc IConfiguration 26 | */ 27 | function setUint256( 28 | bytes32 key, 29 | uint256 value 30 | ) external override onlyController { 31 | _data[key] = value; 32 | emit ParameterUpdated(key, value); 33 | } 34 | 35 | /** 36 | * @inheritdoc IConfiguration 37 | */ 38 | function setAddress( 39 | bytes32 key, 40 | address value 41 | ) external override onlyController { 42 | _data[key] = uint256(uint160(value)); 43 | emit ParameterUpdated(key, value); 44 | } 45 | 46 | /** 47 | * @inheritdoc IConfiguration 48 | */ 49 | function setAddressUint96( 50 | bytes32 key, 51 | address value0, 52 | uint96 value1 53 | ) external override onlyController { 54 | uint256 val = uint256(uint160(value0)); 55 | val |= uint256(value1) << 160; 56 | _data[key] = val; 57 | emit ParameterUpdated(key, value0, value1); 58 | } 59 | 60 | /** 61 | * @inheritdoc IConfiguration 62 | */ 63 | function getUint256(bytes32 key) external view override returns (uint256) { 64 | if (_overrides[msg.sender][key] > 0) return _overrides[msg.sender][key]; 65 | return _data[key]; 66 | } 67 | 68 | /** 69 | * @inheritdoc IConfiguration 70 | */ 71 | function getAddress(bytes32 key) external view override returns (address) { 72 | if (_overrides[msg.sender][key] > 0) 73 | return address(uint160(_overrides[msg.sender][key])); 74 | return address(uint160(_data[key])); 75 | } 76 | 77 | /** 78 | * @inheritdoc IConfiguration 79 | */ 80 | function getAddressUint96( 81 | bytes32 key 82 | ) external view override returns (address, uint96) { 83 | uint256 val = _overrides[msg.sender][key] > 0 84 | ? _overrides[msg.sender][key] 85 | : _data[key]; 86 | return (address(uint160(val)), uint96(val >> 160)); 87 | } 88 | 89 | /** 90 | * @inheritdoc IConfiguration 91 | */ 92 | function overrideUint256( 93 | address caller, 94 | bytes32 key, 95 | uint256 value 96 | ) external override onlyController { 97 | _overrides[caller][key] = value; 98 | emit ParameterOverridden(caller, key, value); 99 | } 100 | 101 | /** 102 | * @inheritdoc IConfiguration 103 | */ 104 | function overrideAddress( 105 | address caller, 106 | bytes32 key, 107 | address value 108 | ) external override onlyController { 109 | uint256 val = uint256(uint160(value)); 110 | _overrides[caller][key] = val; 111 | emit ParameterOverridden(caller, key, value); 112 | } 113 | 114 | /** 115 | * @inheritdoc IConfiguration 116 | */ 117 | function overrideAddressUint96( 118 | address caller, 119 | bytes32 key, 120 | address value0, 121 | uint96 value1 122 | ) external override onlyController { 123 | uint256 val = uint256(uint160(value0)); 124 | val |= uint256(value1) << 160; 125 | _overrides[caller][key] = val; 126 | emit ParameterOverridden(caller, key, value0, value1); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/unit/erc20competitiverewardmodulefactory.js: -------------------------------------------------------------------------------- 1 | // test module for ERC20CompetitiveRewardModuleFactory 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const { bonus, days, reportGas, FEE } = require('../util/helper'); 8 | 9 | const ERC20CompetitiveRewardModule = artifacts.require('ERC20CompetitiveRewardModule'); 10 | const ERC20CompetitiveRewardModuleFactory = artifacts.require('ERC20CompetitiveRewardModuleFactory'); 11 | const TestToken = artifacts.require('TestToken'); 12 | 13 | 14 | describe('ERC20CompetitiveRewardModuleFactory', function () { 15 | let org, owner, alice, config; 16 | before(async function () { 17 | [org, owner, alice, config] = await web3.eth.getAccounts(); 18 | }); 19 | 20 | beforeEach('setup', async function () { 21 | this.factory = await ERC20CompetitiveRewardModuleFactory.new({ from: org }); 22 | this.token = await TestToken.new({ from: org }); 23 | }); 24 | 25 | describe('when constructor parameters are not encoded properly constructed', function () { 26 | 27 | it('should fail', async function () { 28 | // missing an argument 29 | const data = web3.eth.abi.encodeParameters( 30 | ['address', 'uint256', 'uint256'], 31 | [this.token.address, bonus(0).toString(), bonus(2.0).toString()] 32 | ); 33 | 34 | await expectRevert( 35 | this.factory.createModule(config, data, { from: owner }), 36 | 'crmf1' // ERC20CompetitiveRewardModuleFactory: invalid data 37 | ); 38 | }); 39 | }); 40 | 41 | describe('when a module is created with factory', function () { 42 | 43 | beforeEach(async function () { 44 | // encode configuration parameters as bytes 45 | const data = web3.eth.abi.encodeParameters( 46 | ['address', 'uint256', 'uint256', 'uint256'], 47 | [this.token.address, bonus(0).toString(), bonus(2.0).toString(), days(90).toString()] 48 | ); 49 | 50 | // create module with factory 51 | this.res = await this.factory.createModule(config, data, { from: owner }); 52 | this.addr = this.res.logs.filter(l => l.event === 'ModuleCreated')[0].args.module; 53 | this.module = await ERC20CompetitiveRewardModule.at(this.addr); 54 | }); 55 | 56 | it('should emit ModuleCreated event', async function () { 57 | expectEvent(this.res, 'ModuleCreated', { 'user': owner, 'module': this.addr }); 58 | }); 59 | 60 | it('should set reward token properly', async function () { 61 | expect((await this.module.tokens())[0]).to.equal(this.token.address); 62 | }); 63 | 64 | it('should set minimum time bonus properly', async function () { 65 | expect(await this.module.bonusMin()).to.be.bignumber.equal(new BN(0)); 66 | }); 67 | 68 | it('should set maximum time bonus properly', async function () { 69 | expect(await this.module.bonusMax()).to.be.bignumber.equal(bonus(2.0)); 70 | }); 71 | 72 | it('should set time bonus period properly', async function () { 73 | expect(await this.module.bonusPeriod()).to.be.bignumber.equal(days(90)); 74 | }); 75 | 76 | it('should set owner to message sender', async function () { 77 | expect(await this.module.owner()).to.equal(owner); 78 | }); 79 | 80 | it('should set have zero reward balances', async function () { 81 | expect((await this.module.balances())[0]).to.bignumber.equal(new BN(0)); 82 | }); 83 | 84 | it('should have zero usage ratio', async function () { 85 | expect(await this.module.usage()).to.be.bignumber.equal(new BN(0)); 86 | }); 87 | 88 | it('should set factory identifier properly', async function () { 89 | expect(await this.module.factory()).to.equal(this.factory.address); 90 | }); 91 | 92 | it('report gas', async function () { 93 | reportGas('ERC20CompetitiveRewardModuleFactory', 'createModule', '', this.res) 94 | }); 95 | 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /contracts/interfaces/IConfiguration.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IConfiguration 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Configuration interface 13 | * 14 | * @notice this defines the protocol configuration interface 15 | */ 16 | interface IConfiguration { 17 | // events 18 | event ParameterUpdated(bytes32 indexed key, address value); 19 | event ParameterUpdated(bytes32 indexed key, uint256 value); 20 | event ParameterUpdated(bytes32 indexed key, address value0, uint96 value1); 21 | event ParameterOverridden( 22 | address indexed caller, 23 | bytes32 indexed key, 24 | address value 25 | ); 26 | event ParameterOverridden( 27 | address indexed caller, 28 | bytes32 indexed key, 29 | uint256 value 30 | ); 31 | event ParameterOverridden( 32 | address indexed caller, 33 | bytes32 indexed key, 34 | address value0, 35 | uint96 value1 36 | ); 37 | 38 | /** 39 | * @notice set or update uint256 parameter 40 | * @param key keccak256 hash of parameter key 41 | * @param value uint256 parameter value 42 | */ 43 | function setUint256(bytes32 key, uint256 value) external; 44 | 45 | /** 46 | * @notice set or update address parameter 47 | * @param key keccak256 hash of parameter key 48 | * @param value address parameter value 49 | */ 50 | function setAddress(bytes32 key, address value) external; 51 | 52 | /** 53 | * @notice set or update packed address + uint96 pair 54 | * @param key keccak256 hash of parameter key 55 | * @param value0 address parameter value 56 | * @param value1 uint96 parameter value 57 | */ 58 | function setAddressUint96( 59 | bytes32 key, 60 | address value0, 61 | uint96 value1 62 | ) external; 63 | 64 | /** 65 | * @notice get uint256 parameter 66 | * @param key keccak256 hash of parameter key 67 | * @return uint256 parameter value 68 | */ 69 | function getUint256(bytes32 key) external view returns (uint256); 70 | 71 | /** 72 | * @notice get address parameter 73 | * @param key keccak256 hash of parameter key 74 | * @return uint256 parameter value 75 | */ 76 | function getAddress(bytes32 key) external view returns (address); 77 | 78 | /** 79 | * @notice get packed address + uint96 pair 80 | * @param key keccak256 hash of parameter key 81 | * @return address parameter value 82 | * @return uint96 parameter value 83 | */ 84 | function getAddressUint96( 85 | bytes32 key 86 | ) external view returns (address, uint96); 87 | 88 | /** 89 | * @notice override uint256 parameter for specific caller 90 | * @param caller address of caller 91 | * @param key keccak256 hash of parameter key 92 | * @param value uint256 parameter value 93 | */ 94 | function overrideUint256( 95 | address caller, 96 | bytes32 key, 97 | uint256 value 98 | ) external; 99 | 100 | /** 101 | * @notice override address parameter for specific caller 102 | * @param caller address of caller 103 | * @param key keccak256 hash of parameter key 104 | * @param value address parameter value 105 | */ 106 | function overrideAddress( 107 | address caller, 108 | bytes32 key, 109 | address value 110 | ) external; 111 | 112 | /** 113 | * @notice override address parameter for specific caller 114 | * @param caller address of caller 115 | * @param key keccak256 hash of parameter key 116 | * @param value0 address parameter value 117 | * @param value1 uint96 parameter value 118 | */ 119 | function overrideAddressUint96( 120 | address caller, 121 | bytes32 key, 122 | address value0, 123 | uint96 value1 124 | ) external; 125 | } 126 | -------------------------------------------------------------------------------- /contracts/PoolFactory.sol: -------------------------------------------------------------------------------- 1 | /* 2 | PoolFactory 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IPoolFactory.sol"; 12 | import "./interfaces/IModuleFactory.sol"; 13 | import "./interfaces/IStakingModule.sol"; 14 | import "./interfaces/IRewardModule.sol"; 15 | import "./OwnerController.sol"; 16 | import "./Pool.sol"; 17 | 18 | /** 19 | * @title Pool factory 20 | * 21 | * @notice this implements the Pool factory contract which allows any user to 22 | * easily configure and deploy their own Pool 23 | * 24 | * @dev it relies on a system of sub-factories which are responsible for the 25 | * creation of underlying staking and reward modules. This primary factory 26 | * calls each module factory and assembles the overall Pool contract. 27 | * 28 | * this contract also manages the module factory whitelist. 29 | */ 30 | contract PoolFactory is IPoolFactory, OwnerController { 31 | // events 32 | event PoolCreated(address indexed user, address pool); 33 | event WhitelistUpdated( 34 | address indexed factory, 35 | uint256 previous, 36 | uint256 updated 37 | ); 38 | 39 | // types 40 | enum ModuleFactoryType { 41 | Unknown, 42 | Staking, 43 | Reward 44 | } 45 | 46 | // fields 47 | mapping(address => bool) public override map; 48 | address[] public override list; 49 | address private immutable _gysr; 50 | address private immutable _config; 51 | mapping(address => ModuleFactoryType) public whitelist; 52 | 53 | /** 54 | * @param gysr_ address of GYSR token 55 | * @param config_ address of configuration contract 56 | */ 57 | constructor(address gysr_, address config_) { 58 | _gysr = gysr_; 59 | _config = config_; 60 | } 61 | 62 | /** 63 | * @inheritdoc IPoolFactory 64 | */ 65 | function create( 66 | address staking, 67 | address reward, 68 | bytes calldata stakingdata, 69 | bytes calldata rewarddata 70 | ) external override returns (address) { 71 | // validate 72 | require(whitelist[staking] == ModuleFactoryType.Staking, "f1"); 73 | require(whitelist[reward] == ModuleFactoryType.Reward, "f2"); 74 | 75 | // create modules 76 | address stakingModule = IModuleFactory(staking).createModule( 77 | _config, 78 | stakingdata 79 | ); 80 | address rewardModule = IModuleFactory(reward).createModule( 81 | _config, 82 | rewarddata 83 | ); 84 | 85 | // create pool 86 | Pool pool = new Pool(stakingModule, rewardModule, _gysr, _config); 87 | 88 | // set access 89 | IStakingModule(stakingModule).transferOwnership(address(pool)); 90 | IRewardModule(rewardModule).transferOwnership(address(pool)); 91 | pool.transferControl(msg.sender); 92 | pool.transferControlStakingModule(msg.sender); 93 | pool.transferControlRewardModule(msg.sender); 94 | pool.transferOwnership(msg.sender); 95 | 96 | // bookkeeping 97 | map[address(pool)] = true; 98 | list.push(address(pool)); 99 | 100 | // output 101 | emit PoolCreated(msg.sender, address(pool)); 102 | return address(pool); 103 | } 104 | 105 | /** 106 | * @notice set the whitelist status of a module factory 107 | * @param factory_ address of module factory 108 | * @param type_ updated whitelist status for module 109 | */ 110 | function setWhitelist(address factory_, uint256 type_) external { 111 | requireController(); 112 | require(type_ <= uint256(ModuleFactoryType.Reward), "f4"); 113 | require(factory_ != address(0), "f5"); 114 | emit WhitelistUpdated(factory_, uint256(whitelist[factory_]), type_); 115 | whitelist[factory_] = ModuleFactoryType(type_); 116 | } 117 | 118 | /** 119 | * @return total number of Pools created by the factory 120 | */ 121 | function count() public view returns (uint256) { 122 | return list.length; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/util/helper.js: -------------------------------------------------------------------------------- 1 | const { BN, time } = require('@openzeppelin/test-helpers'); 2 | const { fromWei, toWei, padLeft, hexToBytes, numberToHex } = require('web3-utils'); 3 | const { appendFileSync, existsSync, unlinkSync } = require('fs'); 4 | 5 | 6 | // same const used for GYSR, test token, and bonus value returns 7 | const DECIMALS = 18; 8 | // max 20% GYSR spending fee 9 | const FEE = 0.2; 10 | 11 | const UNITMAP = { 12 | 3: 'kwei', 13 | 6: 'mwei', 14 | 9: 'gwei', 15 | 12: 'micro', 16 | 15: 'milli', 17 | 18: 'ether', 18 | 21: 'kether', 19 | }; 20 | 21 | const GAS_REPORT = './gas_report_legacy.txt'; 22 | var gas_report_initialized = false; 23 | 24 | function tokens(x) { 25 | return toFixedPointBigNumber(x, 10, DECIMALS); 26 | } 27 | 28 | function fromTokens(x) { 29 | return fromFixedPointBigNumber(x, 10, DECIMALS); 30 | } 31 | 32 | function bonus(x) { 33 | return toFixedPointBigNumber(x, 10, DECIMALS); 34 | } 35 | 36 | function fromBonus(x) { 37 | return fromFixedPointBigNumber(x, 10, DECIMALS); 38 | } 39 | 40 | function e18(x) { 41 | return toFixedPointBigNumber(x, 10, DECIMALS); 42 | } 43 | 44 | function fromE18(x) { 45 | return fromFixedPointBigNumber(x, 10, DECIMALS); 46 | } 47 | 48 | function e6(x) { 49 | return toFixedPointBigNumber(x, 10, 6); 50 | } 51 | 52 | function fromE6(x) { 53 | return fromFixedPointBigNumber(x, 10, 6); 54 | } 55 | 56 | function days(x) { 57 | return new BN(60 * 60 * 24 * x); 58 | } 59 | 60 | function shares(x) { 61 | return new BN(10 ** 6).mul(toFixedPointBigNumber(x, 10, DECIMALS)); 62 | } 63 | 64 | function rate(x) { 65 | return toFixedPointBigNumber(x, 10, DECIMALS); 66 | }; 67 | 68 | function bytes32(x) { 69 | if (typeof x == 'number') x = numberToHex(new BN(x)) 70 | return padLeft(x, 64).toLowerCase(); 71 | } 72 | 73 | async function now() { 74 | return time.latest(); 75 | } 76 | 77 | function toFixedPointBigNumber(x, base, decimal) { 78 | // use web3 utils if possible 79 | if (base == 10 && decimal in UNITMAP) { 80 | return new BN(toWei(x.toString(), UNITMAP[decimal])); 81 | } 82 | 83 | var x_ = x; 84 | var bn = new BN(0); 85 | for (var i = 0; i < decimal; i++) { 86 | // shift next decimal chunk to integer 87 | var v = x_; 88 | for (j = 0; j < i; j++) { 89 | v *= base; 90 | } 91 | v = Math.floor(v); 92 | 93 | // add to big number 94 | bn = bn.add((new BN(v)).mul((new BN(base)).pow(new BN(decimal - i)))); 95 | 96 | // shift back to decimal and remove 97 | for (j = 0; j < i; j++) { 98 | v /= base; 99 | } 100 | x_ -= v; 101 | } 102 | return bn; 103 | } 104 | 105 | function fromFixedPointBigNumber(x, base, decimal) { 106 | // use web3 utils if possible 107 | if (base == 10 && decimal in UNITMAP) { 108 | return parseFloat(fromWei(x, UNITMAP[decimal])); 109 | } 110 | 111 | var x_ = new BN(x); 112 | var value = 0.0; 113 | for (var i = 0; i < decimal; i++) { 114 | // get next chunk from big number 115 | var c = (new BN(base)).pow(new BN(decimal - i)) 116 | var v = x_.div(c); 117 | x_ = x_.sub(v.mul(c)); 118 | 119 | // shift bn chunk to decimal and add 120 | v_ = v.toNumber(); 121 | for (j = 0; j < i; j++) { 122 | v_ /= base; 123 | } 124 | value += v_; 125 | } 126 | return value; 127 | } 128 | 129 | async function setupTime(t0, delta) { 130 | // decrement target time by one second to setup for next tx 131 | return time.increaseTo(t0.add(delta).sub(new BN(1))); 132 | } 133 | 134 | function compareAddresses(a, b) { 135 | // for sorting list of addresses 136 | return a.localeCompare(b, 'en', { sensitivity: 'base' }); 137 | } 138 | 139 | function reportGas(contract, method, description, tx) { 140 | if (!gas_report_initialized) { 141 | // reset 142 | if (existsSync(GAS_REPORT)) { 143 | unlinkSync(GAS_REPORT); 144 | } 145 | appendFileSync(GAS_REPORT, 'contract, method, description, gas\n'); 146 | gas_report_initialized = true; 147 | } 148 | // write entry 149 | const amount = tx.receipt.gasUsed; 150 | appendFileSync(GAS_REPORT, `${contract}, ${method}, ${description}, ${amount}\n`); 151 | } 152 | 153 | module.exports = { 154 | tokens, 155 | fromTokens, 156 | bonus, 157 | fromBonus, 158 | e18, 159 | fromE18, 160 | e6, 161 | fromE6, 162 | days, 163 | shares, 164 | bytes32, 165 | now, 166 | toFixedPointBigNumber, 167 | fromFixedPointBigNumber, 168 | rate, 169 | reportGas, 170 | setupTime, 171 | compareAddresses, 172 | DECIMALS, 173 | FEE, 174 | }; 175 | -------------------------------------------------------------------------------- /contracts/interfaces/IPool.sol: -------------------------------------------------------------------------------- 1 | /* 2 | IPool 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | /** 12 | * @title Pool interface 13 | * 14 | * @notice this defines the core Pool contract interface 15 | */ 16 | interface IPool { 17 | /** 18 | * @return staking tokens for Pool 19 | */ 20 | function stakingTokens() external view returns (address[] memory); 21 | 22 | /** 23 | * @return reward tokens for Pool 24 | */ 25 | function rewardTokens() external view returns (address[] memory); 26 | 27 | /** 28 | * @return staking balances for user 29 | */ 30 | function stakingBalances( 31 | address user 32 | ) external view returns (uint256[] memory); 33 | 34 | /** 35 | * @return total staking balances for Pool 36 | */ 37 | function stakingTotals() external view returns (uint256[] memory); 38 | 39 | /** 40 | * @return reward balances for Pool 41 | */ 42 | function rewardBalances() external view returns (uint256[] memory); 43 | 44 | /** 45 | * @return GYSR usage ratio for Pool 46 | */ 47 | function usage() external view returns (uint256); 48 | 49 | /** 50 | * @return address of staking module 51 | */ 52 | function stakingModule() external view returns (address); 53 | 54 | /** 55 | * @return address of reward module 56 | */ 57 | function rewardModule() external view returns (address); 58 | 59 | /** 60 | * @notice stake asset and begin earning rewards 61 | * @param amount number of tokens to stake 62 | * @param stakingdata data passed to staking module 63 | * @param rewarddata data passed to reward module 64 | */ 65 | function stake( 66 | uint256 amount, 67 | bytes calldata stakingdata, 68 | bytes calldata rewarddata 69 | ) external; 70 | 71 | /** 72 | * @notice unstake asset and claim rewards 73 | * @param amount number of tokens to unstake 74 | * @param stakingdata data passed to staking module 75 | * @param rewarddata data passed to reward module 76 | */ 77 | function unstake( 78 | uint256 amount, 79 | bytes calldata stakingdata, 80 | bytes calldata rewarddata 81 | ) external; 82 | 83 | /** 84 | * @notice claim rewards without unstaking 85 | * @param amount number of tokens to claim against 86 | * @param stakingdata data passed to staking module 87 | * @param rewarddata data passed to reward module 88 | */ 89 | function claim( 90 | uint256 amount, 91 | bytes calldata stakingdata, 92 | bytes calldata rewarddata 93 | ) external; 94 | 95 | /** 96 | * @notice method called ad hoc to update user accounting 97 | * @param stakingdata data passed to staking module 98 | * @param rewarddata data passed to reward module 99 | */ 100 | function update( 101 | bytes calldata stakingdata, 102 | bytes calldata rewarddata 103 | ) external; 104 | 105 | /** 106 | * @notice method called ad hoc to clean up and perform additional accounting 107 | * @param stakingdata data passed to staking module 108 | * @param rewarddata data passed to reward module 109 | */ 110 | function clean( 111 | bytes calldata stakingdata, 112 | bytes calldata rewarddata 113 | ) external; 114 | 115 | /** 116 | * @return gysr balance available for withdrawal 117 | */ 118 | function gysrBalance() external view returns (uint256); 119 | 120 | /** 121 | * @notice withdraw GYSR tokens applied during unstaking 122 | * @param amount number of GYSR to withdraw 123 | */ 124 | function withdraw(uint256 amount) external; 125 | 126 | /** 127 | * @notice transfer control of the staking module to another account 128 | * @param newController address of new controller 129 | */ 130 | function transferControlStakingModule(address newController) external; 131 | 132 | /** 133 | * @notice transfer control of the reward module to another account 134 | * @param newController address of new controller 135 | */ 136 | function transferControlRewardModule(address newController) external; 137 | 138 | /** 139 | * @notice execute multiple operations in a single call 140 | * @param data array of encoded function data 141 | */ 142 | function multicall( 143 | bytes[] calldata data 144 | ) external returns (bytes[] memory results); 145 | } 146 | -------------------------------------------------------------------------------- /test/unit/assignmentstakingmoduleinfo.js: -------------------------------------------------------------------------------- 1 | // unit tests for AssignmentStakingModuleInfo library 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, time } = require('@openzeppelin/test-helpers'); 5 | const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); 6 | const { expect } = require('chai'); 7 | 8 | const { days, toFixedPointBigNumber, DECIMALS } = require('../util/helper'); 9 | 10 | const AssignmentStakingModule = artifacts.require('AssignmentStakingModule'); 11 | const AssignmentStakingModuleInfo = artifacts.require('AssignmentStakingModuleInfo'); 12 | 13 | describe('AssignmentStakingModuleInfo', function () { 14 | let org, owner, alice, bob, other, factory; 15 | before(async function () { 16 | [org, owner, alice, bob, other, factory] = await web3.eth.getAccounts(); 17 | }); 18 | 19 | beforeEach('setup', async function () { 20 | this.info = await AssignmentStakingModuleInfo.new({ from: org }); 21 | }); 22 | 23 | describe('when pool is first initialized', function () { 24 | beforeEach(async function () { 25 | this.module = await AssignmentStakingModule.new(factory, { from: owner }); 26 | }); 27 | 28 | describe('when getting token info', function () { 29 | beforeEach(async function () { 30 | this.res = await this.info.tokens(this.module.address); 31 | }); 32 | 33 | it('should return staking token address as first argument', async function () { 34 | expect(this.res.addresses_[0]).to.equal(ZERO_ADDRESS); 35 | }); 36 | 37 | it('should return empty name as second argument', async function () { 38 | expect(this.res.names_[0]).to.equal(''); 39 | }); 40 | 41 | it('should return empty symbol as third argument', async function () { 42 | expect(this.res.symbols_[0]).to.equal(''); 43 | }); 44 | 45 | it('should return 0 decimals as fourth argument', async function () { 46 | expect(this.res.decimals_[0]).to.be.bignumber.equal(new BN(0)); 47 | }); 48 | }); 49 | 50 | describe('when user gets all shares', function () { 51 | beforeEach(async function () { 52 | this.res = await this.info.shares(this.module.address, alice, 0); 53 | }); 54 | 55 | it('should return zero', async function () { 56 | expect(this.res).to.be.bignumber.equal(new BN(0)); 57 | }); 58 | }); 59 | 60 | describe('when getting shares per token', function () { 61 | beforeEach(async function () { 62 | this.res = await this.info.sharesPerToken(this.module.address); 63 | }); 64 | 65 | it('should return 1e6', async function () { 66 | expect(this.res).to.be.bignumber.equal( 67 | toFixedPointBigNumber(1, 10, 24) 68 | ); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('when multiple users have staked', function () { 74 | beforeEach('setup', async function () { 75 | // owner creates staking module 76 | this.module = await AssignmentStakingModule.new(factory, { from: owner }); 77 | 78 | // alice gets 100 shares / day 79 | const data0 = web3.eth.abi.encodeParameters(['address'], [alice]); 80 | this.res0 = await this.module.stake(owner, 100, data0, { from: owner }); 81 | 82 | // bob gets 200 shares / day 83 | const data1 = web3.eth.abi.encodeParameters(['address'], [bob]); 84 | this.res1 = await this.module.stake(owner, 200, data1, { from: owner }); 85 | 86 | // advance time 87 | await time.increase(days(30)); 88 | }); 89 | 90 | describe('when user gets all shares', function () { 91 | beforeEach(async function () { 92 | this.res = await this.info.shares(this.module.address, alice, 0); 93 | }); 94 | 95 | it('should return full share balance', async function () { 96 | expect(this.res).to.be.bignumber.equal(new BN(100)); 97 | }); 98 | }); 99 | 100 | describe('when user gets share value on some tokens', function () { 101 | beforeEach(async function () { 102 | this.res = await this.info.shares( 103 | this.module.address, 104 | alice, 105 | new BN(50) 106 | ); 107 | }); 108 | 109 | it('should return expected number of shares', async function () { 110 | expect(this.res).to.be.bignumber.equal( 111 | toFixedPointBigNumber(50, 10, 6) 112 | ); 113 | }); 114 | }); 115 | 116 | describe('when getting shares per token', function () { 117 | beforeEach(async function () { 118 | this.res = await this.info.sharesPerToken(this.module.address); 119 | }); 120 | 121 | it('should return 1e6', async function () { 122 | expect(this.res).to.be.bignumber.equal( 123 | toFixedPointBigNumber(1, 10, 24) 124 | ); 125 | }); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /contracts/info/ERC20StakingModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20StakingModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 12 | 13 | import "../interfaces/IStakingModuleInfo.sol"; 14 | import "../interfaces/IStakingModule.sol"; 15 | import "../ERC20StakingModule.sol"; 16 | import "./TokenUtilsInfo.sol"; 17 | 18 | /** 19 | * @title ERC20 staking module info library 20 | * 21 | * @notice this library provides read-only convenience functions to query 22 | * additional information about the ERC20StakingModule contract. 23 | */ 24 | library ERC20StakingModuleInfo { 25 | using TokenUtilsInfo for IERC20; 26 | 27 | // -- IStakingModuleInfo -------------------------------------------------- 28 | 29 | /** 30 | * @notice convenience function to get all token metadata in a single call 31 | * @param module address of reward module 32 | * @return addresses_ 33 | * @return names_ 34 | * @return symbols_ 35 | * @return decimals_ 36 | */ 37 | function tokens( 38 | address module 39 | ) 40 | external 41 | view 42 | returns ( 43 | address[] memory addresses_, 44 | string[] memory names_, 45 | string[] memory symbols_, 46 | uint8[] memory decimals_ 47 | ) 48 | { 49 | addresses_ = new address[](1); 50 | names_ = new string[](1); 51 | symbols_ = new string[](1); 52 | decimals_ = new uint8[](1); 53 | (addresses_[0], names_[0], symbols_[0], decimals_[0]) = token(module); 54 | } 55 | 56 | /** 57 | * @notice get all staking positions for user 58 | * @param module address of staking module 59 | * @param addr user address of interest 60 | * @param data additional encoded data 61 | * @return accounts_ 62 | * @return shares_ 63 | */ 64 | function positions( 65 | address module, 66 | address addr, 67 | bytes calldata data 68 | ) 69 | external 70 | view 71 | returns (bytes32[] memory accounts_, uint256[] memory shares_) 72 | { 73 | uint256 s = shares(module, addr, 0); 74 | if (s > 0) { 75 | accounts_ = new bytes32[](1); 76 | shares_ = new uint256[](1); 77 | accounts_[0] = bytes32(uint256(uint160(addr))); 78 | shares_[0] = s; 79 | } 80 | } 81 | 82 | // -- ERC20StakingModuleInfo ---------------------------------------------- 83 | 84 | /** 85 | * @notice convenience function to get token metadata in a single call 86 | * @param module address of staking module 87 | * @return address 88 | * @return name 89 | * @return symbol 90 | * @return decimals 91 | */ 92 | function token( 93 | address module 94 | ) public view returns (address, string memory, string memory, uint8) { 95 | IStakingModule m = IStakingModule(module); 96 | IERC20Metadata tkn = IERC20Metadata(m.tokens()[0]); 97 | return (address(tkn), tkn.name(), tkn.symbol(), tkn.decimals()); 98 | } 99 | 100 | /** 101 | * @notice quote the share value for an amount of tokens 102 | * @param module address of staking module 103 | * @param addr account address of interest 104 | * @param amount number of tokens. if zero, return entire share balance 105 | * @return number of shares 106 | */ 107 | function shares( 108 | address module, 109 | address addr, 110 | uint256 amount 111 | ) public view returns (uint256) { 112 | ERC20StakingModule m = ERC20StakingModule(module); 113 | 114 | // return all user shares 115 | if (amount == 0) { 116 | return m.shares(addr); 117 | } 118 | 119 | uint256 totalShares = m.totalShares(); 120 | require(totalShares > 0, "smi1"); 121 | 122 | // convert token amount to shares 123 | IERC20 tkn = IERC20(m.tokens()[0]); 124 | uint256 s = tkn.getShares(module, totalShares, amount); 125 | 126 | require(s > 0, "smi2"); 127 | require(m.shares(addr) >= s, "smi3"); 128 | 129 | return s; 130 | } 131 | 132 | /** 133 | * @notice get shares per token 134 | * @param module address of staking module 135 | * @return current shares per token 136 | */ 137 | function sharesPerToken(address module) public view returns (uint256) { 138 | ERC20StakingModule m = ERC20StakingModule(module); 139 | 140 | uint256 totalShares = m.totalShares(); 141 | if (totalShares == 0) { 142 | return 1e24; 143 | } 144 | 145 | IERC20 tkn = IERC20(m.tokens()[0]); 146 | return tkn.getShares(module, totalShares, 1e18); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /contracts/AssignmentStakingModule.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20StakingModule 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "./interfaces/IStakingModule.sol"; 12 | import "./OwnerController.sol"; 13 | 14 | /** 15 | * @title Assignment staking module 16 | * 17 | * @notice this staking module allows an administrator to set a fixed rate of 18 | * earnings for a specific user. 19 | */ 20 | contract AssignmentStakingModule is IStakingModule, OwnerController { 21 | // constant 22 | uint256 public constant SHARES_COEFF = 1e6; 23 | 24 | // members 25 | address private immutable _factory; 26 | 27 | uint256 public totalRate; 28 | mapping(address => uint256) public rates; 29 | 30 | /** 31 | * @param factory_ address of module factory 32 | */ 33 | constructor(address factory_) { 34 | _factory = factory_; 35 | } 36 | 37 | /** 38 | * @inheritdoc IStakingModule 39 | */ 40 | function tokens() 41 | external 42 | pure 43 | override 44 | returns (address[] memory tokens_) 45 | { 46 | tokens_ = new address[](1); 47 | tokens_[0] = address(0x0); 48 | } 49 | 50 | /** 51 | * @inheritdoc IStakingModule 52 | */ 53 | function balances( 54 | address user 55 | ) external view override returns (uint256[] memory balances_) { 56 | balances_ = new uint256[](1); 57 | balances_[0] = rates[user]; 58 | } 59 | 60 | /** 61 | * @inheritdoc IStakingModule 62 | */ 63 | function factory() external view override returns (address) { 64 | return _factory; 65 | } 66 | 67 | /** 68 | * @inheritdoc IStakingModule 69 | */ 70 | function totals() 71 | external 72 | view 73 | override 74 | returns (uint256[] memory totals_) 75 | { 76 | totals_ = new uint256[](1); 77 | totals_[0] = totalRate; 78 | } 79 | 80 | /** 81 | * @inheritdoc IStakingModule 82 | */ 83 | function stake( 84 | address sender, 85 | uint256 amount, 86 | bytes calldata data 87 | ) external override onlyOwner returns (bytes32, uint256) { 88 | // validate 89 | require(amount > 0, "asm1"); 90 | require(sender == controller(), "asm2"); 91 | require(data.length == 32, "asm3"); 92 | 93 | address assignee; 94 | assembly { 95 | assignee := calldataload(132) 96 | } 97 | 98 | // increase rate 99 | rates[assignee] += amount; 100 | totalRate += amount; 101 | 102 | // emit 103 | bytes32 account = bytes32(uint256(uint160(assignee))); 104 | uint256 shares = amount * SHARES_COEFF; 105 | emit Staked(account, sender, address(0x0), amount, shares); 106 | 107 | return (account, shares); 108 | } 109 | 110 | /** 111 | * @inheritdoc IStakingModule 112 | */ 113 | function unstake( 114 | address sender, 115 | uint256 amount, 116 | bytes calldata data 117 | ) external override onlyOwner returns (bytes32, address, uint256) { 118 | // validate 119 | require(amount > 0, "asm4"); 120 | require(sender == controller(), "asm5"); 121 | require(data.length == 32, "asm6"); 122 | 123 | address assignee; 124 | assembly { 125 | assignee := calldataload(132) 126 | } 127 | uint256 r = rates[assignee]; 128 | require(amount <= r, "asm7"); 129 | 130 | // decrease rate 131 | rates[assignee] = r - amount; 132 | totalRate -= amount; 133 | 134 | // emit 135 | bytes32 account = bytes32(uint256(uint160(assignee))); 136 | uint256 shares = amount * SHARES_COEFF; 137 | emit Unstaked(account, sender, address(0x0), amount, shares); 138 | 139 | return (account, assignee, shares); 140 | } 141 | 142 | /** 143 | * @inheritdoc IStakingModule 144 | */ 145 | function claim( 146 | address sender, 147 | uint256 amount, 148 | bytes calldata 149 | ) external override onlyOwner returns (bytes32, address, uint256) { 150 | require(amount > 0, "asm8"); 151 | require(amount <= rates[sender], "asm9"); 152 | bytes32 account = bytes32(uint256(uint160(sender))); 153 | uint256 shares = amount * SHARES_COEFF; 154 | emit Claimed(account, sender, address(0x0), amount, shares); 155 | return (account, sender, shares); 156 | } 157 | 158 | /** 159 | * @inheritdoc IStakingModule 160 | */ 161 | function update( 162 | address sender, 163 | bytes calldata 164 | ) external pure override returns (bytes32) { 165 | return (bytes32(uint256(uint160(sender)))); 166 | } 167 | 168 | /** 169 | * @inheritdoc IStakingModule 170 | */ 171 | function clean(bytes calldata) external override {} 172 | } 173 | -------------------------------------------------------------------------------- /contracts/TokenUtils.sol: -------------------------------------------------------------------------------- 1 | /* 2 | TokenUtils 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 12 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 13 | 14 | /** 15 | * @title Token utilities 16 | * 17 | * @notice this library implements utility methods for token handling, 18 | * dynamic balance accounting, and fee processing 19 | */ 20 | library TokenUtils { 21 | using SafeERC20 for IERC20; 22 | 23 | event Fee(address indexed receiver, address indexed token, uint256 amount); // redefinition 24 | 25 | uint256 constant INITIAL_SHARES_PER_TOKEN = 1e6; 26 | uint256 constant FLOOR_SHARES_PER_TOKEN = 1e3; 27 | 28 | /** 29 | * @notice get token shares from amount 30 | * @param token erc20 token interface 31 | * @param total current total shares 32 | * @param amount balance of tokens 33 | */ 34 | function getShares( 35 | IERC20 token, 36 | uint256 total, 37 | uint256 amount 38 | ) internal view returns (uint256) { 39 | if (total == 0) return 0; 40 | uint256 balance = token.balanceOf(address(this)); 41 | if (total < balance * FLOOR_SHARES_PER_TOKEN) 42 | return amount * FLOOR_SHARES_PER_TOKEN; 43 | return (total * amount) / balance; 44 | } 45 | 46 | /** 47 | * @notice get token amount from shares 48 | * @param token erc20 token interface 49 | * @param total current total shares 50 | * @param shares balance of shares 51 | */ 52 | function getAmount( 53 | IERC20 token, 54 | uint256 total, 55 | uint256 shares 56 | ) internal view returns (uint256) { 57 | if (total == 0) return 0; 58 | uint256 balance = token.balanceOf(address(this)); 59 | if (total < balance * FLOOR_SHARES_PER_TOKEN) 60 | return shares / FLOOR_SHARES_PER_TOKEN; 61 | return (balance * shares) / total; 62 | } 63 | 64 | /** 65 | * @notice transfer tokens from sender into contract and convert to shares 66 | * for internal accounting 67 | * @param token erc20 token interface 68 | * @param total current total shares 69 | * @param sender token sender 70 | * @param amount number of tokens to be sent 71 | */ 72 | function receiveAmount( 73 | IERC20 token, 74 | uint256 total, 75 | address sender, 76 | uint256 amount 77 | ) internal returns (uint256) { 78 | // note: we assume amount > 0 has already been validated 79 | 80 | // transfer 81 | uint256 balance = token.balanceOf(address(this)); 82 | token.safeTransferFrom(sender, address(this), amount); 83 | uint256 actual = token.balanceOf(address(this)) - balance; 84 | require(amount >= actual); 85 | 86 | // mint shares at current rate 87 | uint256 minted; 88 | if (total == 0) { 89 | minted = actual * INITIAL_SHARES_PER_TOKEN; 90 | } else if (total < balance * FLOOR_SHARES_PER_TOKEN) { 91 | minted = actual * FLOOR_SHARES_PER_TOKEN; 92 | } else { 93 | minted = (total * actual) / balance; 94 | } 95 | require(minted > 0); 96 | return minted; 97 | } 98 | 99 | /** 100 | * @notice transfer tokens from sender into contract, process protocol fee, 101 | * and convert to shares for internal accounting 102 | * @param token erc20 token interface 103 | * @param total current total shares 104 | * @param sender token sender 105 | * @param amount number of tokens to be sent 106 | * @param feeReceiver address to receive fee 107 | * @param feeRate portion of amount to take as fee in 18 decimals 108 | */ 109 | function receiveWithFee( 110 | IERC20 token, 111 | uint256 total, 112 | address sender, 113 | uint256 amount, 114 | address feeReceiver, 115 | uint256 feeRate 116 | ) internal returns (uint256) { 117 | // note: we assume amount > 0 has already been validated 118 | 119 | // check initial token balance 120 | uint256 balance = token.balanceOf(address(this)); 121 | 122 | // process fee 123 | uint256 fee; 124 | if (feeReceiver != address(0) && feeRate > 0 && feeRate < 1e18) { 125 | fee = (amount * feeRate) / 1e18; 126 | token.safeTransferFrom(sender, feeReceiver, fee); 127 | emit Fee(feeReceiver, address(token), fee); 128 | } 129 | 130 | // do transfer 131 | token.safeTransferFrom(sender, address(this), amount - fee); 132 | uint256 actual = token.balanceOf(address(this)) - balance; 133 | require(amount >= actual); 134 | 135 | // mint shares at current rate 136 | uint256 minted; 137 | if (total == 0) { 138 | minted = actual * INITIAL_SHARES_PER_TOKEN; 139 | } else if (total < balance * FLOOR_SHARES_PER_TOKEN) { 140 | minted = actual * FLOOR_SHARES_PER_TOKEN; 141 | } else { 142 | minted = (total * actual) / balance; 143 | } 144 | require(minted > 0); 145 | return minted; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test/unit/ownercontroller.js: -------------------------------------------------------------------------------- 1 | // unit tests for OwnerController contract 2 | 3 | const { artifacts, web3 } = require('hardhat'); 4 | const { BN, time, expectEvent, expectRevert, constants } = require('@openzeppelin/test-helpers'); 5 | const { expect } = require('chai'); 6 | 7 | const OwnerController = artifacts.require('OwnerController'); 8 | 9 | 10 | describe('OwnerController', function () { 11 | let owner, controller, bob, alice; 12 | before(async function () { 13 | [owner, controller, bob, alice] = await web3.eth.getAccounts(); 14 | }); 15 | 16 | beforeEach('setup', async function () { 17 | this.contract = await OwnerController.new({ from: owner }); 18 | }); 19 | 20 | describe('construction', function () { 21 | 22 | describe('when created', function () { 23 | 24 | it('should initialize owner to sender', async function () { 25 | expect(await this.contract.owner()).to.equal(owner); 26 | }); 27 | 28 | it('should initialize controller to sender', async function () { 29 | expect(await this.contract.controller()).to.equal(owner); 30 | }); 31 | 32 | }); 33 | }); 34 | 35 | describe('ownership', function () { 36 | 37 | beforeEach(async function () { 38 | await this.contract.transferControl(controller, { from: owner }); 39 | }); 40 | 41 | describe('when owner transfers ownership', function () { 42 | beforeEach(async function () { 43 | this.res = await this.contract.transferOwnership(alice, { from: owner }); 44 | }); 45 | 46 | it('should update owner to new address', async function () { 47 | expect(await this.contract.owner()).to.equal(alice); 48 | }); 49 | 50 | it('should not change controller', async function () { 51 | expect(await this.contract.controller()).to.equal(controller); 52 | }); 53 | 54 | it('should emit OwnershipTransferred event', async function () { 55 | expectEvent( 56 | this.res, 57 | 'OwnershipTransferred', 58 | { previousOwner: owner, newOwner: alice } 59 | ); 60 | }); 61 | 62 | }); 63 | 64 | describe('when controller tries to transfer ownership', function () { 65 | it('should fail', async function () { 66 | await expectRevert( 67 | this.contract.transferOwnership(alice, { from: controller }), 68 | 'oc1' // OwnerController: caller is not the owner 69 | ); 70 | }); 71 | }); 72 | 73 | describe('when other account tries to transfer ownership', function () { 74 | it('should fail', async function () { 75 | await expectRevert( 76 | this.contract.transferOwnership(bob, { from: bob }), 77 | 'oc1' // OwnerController: caller is not the owner 78 | ); 79 | }); 80 | }); 81 | 82 | describe('when owner tries to transfer ownership to zero address', function () { 83 | it('should fail', async function () { 84 | await expectRevert( 85 | this.contract.transferOwnership(constants.ZERO_ADDRESS, { from: owner }), 86 | 'oc3' // OwnerController: new owner is zero address 87 | ); 88 | }); 89 | }); 90 | 91 | }); 92 | 93 | describe('control', function () { 94 | 95 | beforeEach(async function () { 96 | await this.contract.transferControl(controller, { from: owner }); 97 | }); 98 | 99 | describe('when owner transfers control', function () { 100 | beforeEach(async function () { 101 | this.res = await this.contract.transferControl(alice, { from: owner }); 102 | }); 103 | 104 | it('should update controller to new address', async function () { 105 | expect(await this.contract.controller()).to.equal(alice); 106 | }); 107 | 108 | it('should not change owner', async function () { 109 | expect(await this.contract.owner()).to.equal(owner); 110 | }); 111 | 112 | it('should emit ControlTransferred event', async function () { 113 | expectEvent( 114 | this.res, 115 | 'ControlTransferred', 116 | { previousController: controller, newController: alice } 117 | ); 118 | }); 119 | 120 | }); 121 | 122 | describe('when controller tries to transfer control', function () { 123 | it('should fail', async function () { 124 | await expectRevert( 125 | this.contract.transferControl(alice, { from: controller }), 126 | 'oc1' // OwnerController: caller is not the owner 127 | ); 128 | }); 129 | }); 130 | 131 | describe('when other account tries to transfer ownership', function () { 132 | it('should fail', async function () { 133 | await expectRevert( 134 | this.contract.transferControl(bob, { from: bob }), 135 | 'oc1' // OwnerController: caller is not the owner 136 | ); 137 | }); 138 | }); 139 | 140 | describe('when owner tries to transfer ownership to zero address', function () { 141 | it('should fail', async function () { 142 | await expectRevert( 143 | this.contract.transferControl(constants.ZERO_ADDRESS, { from: owner }), 144 | 'oc4' // OwnerController: new controller is zero address 145 | ); 146 | }); 147 | }); 148 | 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /contracts/info/ERC721StakingModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC721StakingModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 12 | 13 | import "../interfaces/IStakingModule.sol"; 14 | import "../ERC721StakingModule.sol"; 15 | 16 | /** 17 | * @title ERC721 staking module info library 18 | * 19 | * @notice this library provides read-only convenience functions to query 20 | * additional information about the ERC721StakingModule contract. 21 | */ 22 | library ERC721StakingModuleInfo { 23 | // -- IStakingModuleInfo -------------------------------------------------- 24 | 25 | /** 26 | * @notice convenience function to get all token metadata in a single call 27 | * @param module address of reward module 28 | * @return addresses_ 29 | * @return names_ 30 | * @return symbols_ 31 | * @return decimals_ 32 | */ 33 | function tokens( 34 | address module 35 | ) 36 | external 37 | view 38 | returns ( 39 | address[] memory addresses_, 40 | string[] memory names_, 41 | string[] memory symbols_, 42 | uint8[] memory decimals_ 43 | ) 44 | { 45 | addresses_ = new address[](1); 46 | names_ = new string[](1); 47 | symbols_ = new string[](1); 48 | decimals_ = new uint8[](1); 49 | (addresses_[0], names_[0], symbols_[0], decimals_[0]) = token(module); 50 | } 51 | 52 | /** 53 | * @notice get all staking positions for user 54 | * @param module address of staking module 55 | * @param addr user address of interest 56 | * @param data additional encoded data 57 | * @return accounts_ 58 | * @return shares_ 59 | */ 60 | function positions( 61 | address module, 62 | address addr, 63 | bytes calldata data 64 | ) 65 | external 66 | view 67 | returns (bytes32[] memory accounts_, uint256[] memory shares_) 68 | { 69 | uint256 s = shares(module, addr, 0); 70 | if (s > 0) { 71 | accounts_ = new bytes32[](1); 72 | shares_ = new uint256[](1); 73 | accounts_[0] = bytes32(uint256(uint160(addr))); 74 | shares_[0] = s; 75 | } 76 | } 77 | 78 | // -- ERC721StakingModuleInfo --------------------------------------------- 79 | 80 | /** 81 | * @notice convenience function to get token metadata in a single call 82 | * @param module address of staking module 83 | * @return address 84 | * @return name 85 | * @return symbol 86 | * @return decimals 87 | */ 88 | function token( 89 | address module 90 | ) public view returns (address, string memory, string memory, uint8) { 91 | IStakingModule m = IStakingModule(module); 92 | IERC721Metadata tkn = IERC721Metadata(m.tokens()[0]); 93 | if (!tkn.supportsInterface(0x5b5e139f)) { 94 | return (address(tkn), "", "", 0); 95 | } 96 | return (address(tkn), tkn.name(), tkn.symbol(), 0); 97 | } 98 | 99 | /** 100 | * @notice quote the share value for an amount of tokens 101 | * @param module address of staking module 102 | * @param addr account address of interest 103 | * @param amount number of tokens. if zero, return entire share balance 104 | * @return number of shares 105 | */ 106 | function shares( 107 | address module, 108 | address addr, 109 | uint256 amount 110 | ) public view returns (uint256) { 111 | ERC721StakingModule m = ERC721StakingModule(module); 112 | 113 | // return all user shares 114 | if (amount == 0) { 115 | return m.counts(addr) * m.SHARES_PER_TOKEN(); 116 | } 117 | 118 | require(amount <= m.counts(addr), "smni1"); 119 | return amount * m.SHARES_PER_TOKEN(); 120 | } 121 | 122 | /** 123 | * @notice get shares per token 124 | * @param module address of staking module 125 | * @return current shares per token 126 | */ 127 | function sharesPerToken(address module) public view returns (uint256) { 128 | ERC721StakingModule m = ERC721StakingModule(module); 129 | return m.SHARES_PER_TOKEN() * 1e18; 130 | } 131 | 132 | /** 133 | * @notice get staked token ids for user 134 | * @param module address of staking module 135 | * @param addr account address of interest 136 | * @param amount number of tokens to enumerate 137 | * @param start token index to start at 138 | * @return ids array of token ids 139 | */ 140 | function tokenIds( 141 | address module, 142 | address addr, 143 | uint256 amount, 144 | uint256 start 145 | ) public view returns (uint256[] memory ids) { 146 | ERC721StakingModule m = ERC721StakingModule(module); 147 | uint256 sz = m.counts(addr); 148 | require(start + amount <= sz, "smni2"); 149 | 150 | if (amount == 0) { 151 | amount = sz - start; 152 | } 153 | 154 | ids = new uint256[](amount); 155 | 156 | for (uint256 i = 0; i < amount; i++) { 157 | ids[i] = m.tokenByOwner(addr, i + start); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /contracts/info/ERC20FixedRewardModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20FixedRewardModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 12 | 13 | import "../interfaces/IRewardModule.sol"; 14 | import "../ERC20FixedRewardModule.sol"; 15 | import "./TokenUtilsInfo.sol"; 16 | 17 | /** 18 | * @title ERC20 fixed reward module info library 19 | * 20 | * @notice this library provides read-only convenience functions to query 21 | * additional information about the ERC20FixedRewardModule contract. 22 | */ 23 | library ERC20FixedRewardModuleInfo { 24 | using TokenUtilsInfo for IERC20; 25 | 26 | /** 27 | * @notice get all token metadata 28 | * @param module address of reward module 29 | * @return addresses_ 30 | * @return names_ 31 | * @return symbols_ 32 | * @return decimals_ 33 | */ 34 | function tokens( 35 | address module 36 | ) 37 | external 38 | view 39 | returns ( 40 | address[] memory addresses_, 41 | string[] memory names_, 42 | string[] memory symbols_, 43 | uint8[] memory decimals_ 44 | ) 45 | { 46 | addresses_ = new address[](1); 47 | names_ = new string[](1); 48 | symbols_ = new string[](1); 49 | decimals_ = new uint8[](1); 50 | (addresses_[0], names_[0], symbols_[0], decimals_[0]) = token(module); 51 | } 52 | 53 | /** 54 | * @notice convenience function to get token metadata in a single call 55 | * @param module address of reward module 56 | * @return address 57 | * @return name 58 | * @return symbol 59 | * @return decimals 60 | */ 61 | function token( 62 | address module 63 | ) public view returns (address, string memory, string memory, uint8) { 64 | IRewardModule m = IRewardModule(module); 65 | IERC20Metadata tkn = IERC20Metadata(m.tokens()[0]); 66 | return (address(tkn), tkn.name(), tkn.symbol(), tkn.decimals()); 67 | } 68 | 69 | /** 70 | * @notice generic function to get pending reward balances 71 | * @param module address of reward module 72 | * @param account bytes32 account of interest for preview 73 | * @param shares number of shares that would be used 74 | * @return rewards_ estimated reward balances 75 | */ 76 | function rewards( 77 | address module, 78 | bytes32 account, 79 | uint256 shares, 80 | bytes calldata 81 | ) public view returns (uint256[] memory rewards_) { 82 | rewards_ = new uint256[](1); 83 | (rewards_[0], ) = preview(module, account); 84 | } 85 | 86 | /** 87 | * @notice preview estimated rewards 88 | * @param module address of reward module 89 | * @param account bytes32 account of interest for preview 90 | * @return estimated reward 91 | * @return estimated time vesting coefficient 92 | */ 93 | function preview( 94 | address module, 95 | bytes32 account 96 | ) public view returns (uint256, uint256) { 97 | ERC20FixedRewardModule m = ERC20FixedRewardModule(module); 98 | (uint256 debt, , uint256 earned, uint128 timestamp, uint128 updated) = m 99 | .positions(account); 100 | 101 | uint256 r = earned; 102 | { 103 | uint256 end = timestamp + m.period(); 104 | if (block.timestamp > end) { 105 | r += debt; 106 | } else { 107 | r += (debt * (block.timestamp - updated)) / (end - updated); 108 | } 109 | } 110 | 111 | if (r == 0) return (0, 0); 112 | 113 | // convert to tokens 114 | r = IERC20(m.tokens()[0]).getAmount(module, m.rewards(), r); 115 | 116 | // get vesting coeff 117 | uint256 v = 1e18; 118 | if (block.timestamp < timestamp + m.period()) 119 | v = ((block.timestamp - timestamp) * 1e18) / m.period(); 120 | 121 | return (r, v); 122 | } 123 | 124 | /** 125 | * @notice get effective budget 126 | * @param module address of reward module 127 | * @return estimated budget in debt shares 128 | */ 129 | function budget(address module) public view returns (uint256) { 130 | ERC20FixedRewardModule m = ERC20FixedRewardModule(module); 131 | return m.rewards() - m.debt(); 132 | } 133 | 134 | /** 135 | * @notice check potential increase in staking shares for sufficient budget 136 | * @param module address of reward module 137 | * @param shares number of shares to be staked 138 | * @return okay if stake amount is within budget for time period 139 | * @return estimated debt shares allocated 140 | * @return remaining total debt shares 141 | */ 142 | function validate( 143 | address module, 144 | uint256 shares 145 | ) public view returns (bool, uint256, uint256) { 146 | ERC20FixedRewardModule m = ERC20FixedRewardModule(module); 147 | 148 | uint256 reward = (shares * m.rate()) / 1e18; 149 | uint256 budget_ = budget(module); 150 | if (reward > budget_) return (false, reward, 0); 151 | 152 | return (true, reward, budget_ - reward); 153 | } 154 | 155 | /** 156 | * @notice get withdrawable excess budget 157 | * @param module address of reward module 158 | * @return withdrawable budget in tokens 159 | */ 160 | function withdrawable(address module) public view returns (uint256) { 161 | ERC20FixedRewardModule m = ERC20FixedRewardModule(module); 162 | IERC20 tkn = IERC20(m.tokens()[0]); 163 | return tkn.getAmount(module, m.rewards(), budget(module)); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /contracts/info/ERC20LinearRewardModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20LinearRewardModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 12 | 13 | import "../interfaces/IRewardModule.sol"; 14 | import "../ERC20LinearRewardModule.sol"; 15 | import "./TokenUtilsInfo.sol"; 16 | 17 | /** 18 | * @title ERC20 linear reward module info library 19 | * 20 | * @notice this library provides read-only convenience functions to query 21 | * additional information about the ERC20LinearRewardModule contract. 22 | */ 23 | library ERC20LinearRewardModuleInfo { 24 | using TokenUtilsInfo for IERC20; 25 | 26 | /** 27 | * @notice get all token metadata 28 | * @param module address of reward module 29 | * @return addresses_ 30 | * @return names_ 31 | * @return symbols_ 32 | * @return decimals_ 33 | */ 34 | function tokens( 35 | address module 36 | ) 37 | external 38 | view 39 | returns ( 40 | address[] memory addresses_, 41 | string[] memory names_, 42 | string[] memory symbols_, 43 | uint8[] memory decimals_ 44 | ) 45 | { 46 | addresses_ = new address[](1); 47 | names_ = new string[](1); 48 | symbols_ = new string[](1); 49 | decimals_ = new uint8[](1); 50 | (addresses_[0], names_[0], symbols_[0], decimals_[0]) = token(module); 51 | } 52 | 53 | /** 54 | * @notice convenience function to get token metadata in a single call 55 | * @param module address of reward module 56 | * @return address 57 | * @return name 58 | * @return symbol 59 | * @return decimals 60 | */ 61 | function token( 62 | address module 63 | ) public view returns (address, string memory, string memory, uint8) { 64 | IRewardModule m = IRewardModule(module); 65 | IERC20Metadata tkn = IERC20Metadata(m.tokens()[0]); 66 | return (address(tkn), tkn.name(), tkn.symbol(), tkn.decimals()); 67 | } 68 | 69 | /** 70 | * @notice generic function to get pending reward balances 71 | * @param module address of reward module 72 | * @param account bytes32 account of interest for preview 73 | * @param shares number of shares that would be used 74 | * @return rewards_ estimated reward balances 75 | */ 76 | function rewards( 77 | address module, 78 | bytes32 account, 79 | uint256 shares, 80 | bytes calldata 81 | ) public view returns (uint256[] memory rewards_) { 82 | rewards_ = new uint256[](1); 83 | rewards_[0] = preview(module, account); 84 | } 85 | 86 | /** 87 | * @notice preview estimated rewards 88 | * @param module address of reward module 89 | * @param account bytes32 account of interest for preview 90 | * @return estimated reward 91 | */ 92 | function preview( 93 | address module, 94 | bytes32 account 95 | ) public view returns (uint256) { 96 | ERC20LinearRewardModule m = ERC20LinearRewardModule(module); 97 | 98 | // get effective elapsed time 99 | uint256 budget = m.rewardShares() - m.earned(); 100 | uint256 e = block.timestamp - m.lastUpdated(); 101 | 102 | if (budget < (m.stakingShares() * e * m.rate()) / 1e18) { 103 | // over budget, clip elapsed 104 | e = budget / ((m.stakingShares() * m.rate()) / 1e18); 105 | } 106 | uint256 elapsed = m.elapsed() + e; 107 | 108 | // compute earned 109 | (uint256 shares, uint256 timestamp, uint256 earned) = m.positions( 110 | account 111 | ); 112 | uint256 r = earned + (shares * m.rate() * (elapsed - timestamp)) / 1e18; 113 | 114 | if (r == 0) return 0; 115 | 116 | IERC20 tkn = IERC20(m.tokens()[0]); 117 | return tkn.getAmount(module, m.rewardShares(), r); 118 | } 119 | 120 | /** 121 | * @notice compute effective runway with current budget and reward rate 122 | * @param module address of reward module 123 | * @return estimated runway in seconds 124 | */ 125 | function runway(address module) public view returns (uint256) { 126 | ERC20LinearRewardModule m = ERC20LinearRewardModule(module); 127 | if (m.stakingShares() == 0) return 0; 128 | 129 | uint256 budget = m.rewardShares() - m.earned(); 130 | uint256 t = budget / ((m.stakingShares() * m.rate()) / 1e18); 131 | uint256 dt = block.timestamp - m.lastUpdated(); 132 | 133 | return (t > dt) ? t - dt : 0; 134 | } 135 | 136 | /** 137 | * @notice check potential increase in shares for sufficient budget and runway 138 | * @param module address of reward module 139 | * @param shares number of shares to be staked 140 | * @return okay if stake amount is within budget for time period 141 | * @return estimated runway in seconds 142 | */ 143 | function validate( 144 | address module, 145 | uint256 shares 146 | ) public view returns (bool, uint256) { 147 | ERC20LinearRewardModule m = ERC20LinearRewardModule(module); 148 | if (m.stakingShares() + shares == 0) return (false, 0); 149 | 150 | uint256 budget = m.rewardShares() - m.earned(); 151 | uint256 dt = block.timestamp - m.lastUpdated(); 152 | uint256 earned = (m.stakingShares() * dt * m.rate()) / 1e18; 153 | 154 | // already depleted 155 | if (budget < earned) return (false, 0); 156 | 157 | // get runway with added shares updated budget 158 | budget -= earned; 159 | uint256 t = budget / (((m.stakingShares() + shares) * m.rate()) / 1e18); 160 | 161 | return (t > m.period(), t); 162 | } 163 | 164 | /** 165 | * @notice get withdrawable excess budget 166 | * @param module address of linear reward module 167 | * @return withdrawable budget in tokens 168 | */ 169 | function withdrawable(address module) public view returns (uint256) { 170 | ERC20LinearRewardModule m = ERC20LinearRewardModule(module); 171 | 172 | // earned since last update 173 | uint256 budget = m.rewardShares() - m.earned(); 174 | uint256 dt = block.timestamp - m.lastUpdated(); 175 | uint256 earned = (m.stakingShares() * dt * m.rate()) / 1e18; 176 | if (budget < earned) return 0; // already depleted 177 | budget -= earned; 178 | 179 | // committed rolling period 180 | uint256 committed = (m.stakingShares() * m.period() * m.rate()) / 1e18; 181 | if (budget < committed) return 0; 182 | budget -= committed; 183 | 184 | IERC20 tkn = IERC20(m.tokens()[0]); 185 | return tkn.getAmount(module, m.rewardShares(), budget); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /contracts/ERC721StakingModule.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC721StakingModule 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 12 | 13 | import "./interfaces/IStakingModule.sol"; 14 | import "./OwnerController.sol"; 15 | 16 | /** 17 | * @title ERC721 staking module 18 | * 19 | * @notice this staking module allows users to deposit one or more ERC721 20 | * tokens in exchange for shares credited to their address. When the user 21 | * unstakes, these shares will be burned and a reward will be distributed. 22 | */ 23 | contract ERC721StakingModule is IStakingModule, OwnerController { 24 | // constant 25 | uint256 public constant SHARES_PER_TOKEN = 1e6; 26 | 27 | // members 28 | IERC721 private immutable _token; 29 | address private immutable _factory; 30 | 31 | mapping(address => uint256) public counts; 32 | mapping(uint256 => address) public owners; 33 | mapping(address => mapping(uint256 => uint256)) public tokenByOwner; 34 | mapping(uint256 => uint256) public tokenIndex; 35 | 36 | /** 37 | * @param token_ the token that will be rewarded 38 | * @param factory_ address of module factory 39 | */ 40 | constructor(address token_, address factory_) { 41 | require(IERC165(token_).supportsInterface(0x80ac58cd), "smn1"); 42 | _token = IERC721(token_); 43 | _factory = factory_; 44 | } 45 | 46 | /** 47 | * @inheritdoc IStakingModule 48 | */ 49 | function tokens() 50 | external 51 | view 52 | override 53 | returns (address[] memory tokens_) 54 | { 55 | tokens_ = new address[](1); 56 | tokens_[0] = address(_token); 57 | } 58 | 59 | /** 60 | * @inheritdoc IStakingModule 61 | */ 62 | function balances( 63 | address user 64 | ) external view override returns (uint256[] memory balances_) { 65 | balances_ = new uint256[](1); 66 | balances_[0] = counts[user]; 67 | } 68 | 69 | /** 70 | * @inheritdoc IStakingModule 71 | */ 72 | function factory() external view override returns (address) { 73 | return _factory; 74 | } 75 | 76 | /** 77 | * @inheritdoc IStakingModule 78 | */ 79 | function totals() 80 | external 81 | view 82 | override 83 | returns (uint256[] memory totals_) 84 | { 85 | totals_ = new uint256[](1); 86 | totals_[0] = _token.balanceOf(address(this)); 87 | } 88 | 89 | /** 90 | * @inheritdoc IStakingModule 91 | */ 92 | function stake( 93 | address sender, 94 | uint256 amount, 95 | bytes calldata data 96 | ) external override onlyOwner returns (bytes32, uint256) { 97 | // validate 98 | require(amount > 0, "smn2"); 99 | require(amount <= _token.balanceOf(sender), "smn3"); 100 | require(data.length == 32 * amount, "smn4"); 101 | 102 | uint256 count = counts[sender]; 103 | 104 | // stake 105 | for (uint256 i; i < amount; ) { 106 | // get token id 107 | uint256 id; 108 | uint256 pos = 132 + 32 * i; 109 | assembly { 110 | id := calldataload(pos) 111 | } 112 | 113 | // ownership mappings 114 | owners[id] = sender; 115 | uint256 len = count + i; 116 | tokenByOwner[sender][len] = id; 117 | tokenIndex[id] = len; 118 | 119 | // transfer to module 120 | _token.transferFrom(sender, address(this), id); 121 | 122 | unchecked { 123 | ++i; 124 | } 125 | } 126 | 127 | // update position 128 | counts[sender] = count + amount; 129 | 130 | // emit 131 | bytes32 account = bytes32(uint256(uint160(sender))); 132 | uint256 shares = amount * SHARES_PER_TOKEN; 133 | emit Staked(account, sender, address(_token), amount, shares); 134 | 135 | return (account, shares); 136 | } 137 | 138 | /** 139 | * @inheritdoc IStakingModule 140 | */ 141 | function unstake( 142 | address sender, 143 | uint256 amount, 144 | bytes calldata data 145 | ) external override onlyOwner returns (bytes32, address, uint256) { 146 | // validate 147 | require(amount > 0, "smn5"); 148 | uint256 count = counts[sender]; 149 | require(amount <= count, "smn6"); 150 | require(data.length == 32 * amount, "smn7"); 151 | 152 | // unstake 153 | for (uint256 i; i < amount; ) { 154 | // get token id 155 | uint256 id; 156 | { 157 | uint256 pos = 132 + 32 * i; 158 | assembly { 159 | id := calldataload(pos) 160 | } 161 | } 162 | 163 | // ownership 164 | require(owners[id] == sender, "smn8"); 165 | delete owners[id]; 166 | 167 | // clean up ownership mappings 168 | uint256 lastIndex = count - 1 - i; 169 | if (amount != count) { 170 | // reindex on partial unstake 171 | uint256 index = tokenIndex[id]; 172 | if (index != lastIndex) { 173 | uint256 lastId = tokenByOwner[sender][lastIndex]; 174 | tokenByOwner[sender][index] = lastId; 175 | tokenIndex[lastId] = index; 176 | } 177 | } 178 | delete tokenByOwner[sender][lastIndex]; 179 | delete tokenIndex[id]; 180 | 181 | // transfer to user 182 | _token.safeTransferFrom(address(this), sender, id); 183 | 184 | unchecked { 185 | ++i; 186 | } 187 | } 188 | 189 | // update position 190 | counts[sender] = count - amount; 191 | 192 | // emit 193 | bytes32 account = bytes32(uint256(uint160(sender))); 194 | uint256 shares = amount * SHARES_PER_TOKEN; 195 | emit Unstaked(account, sender, address(_token), amount, shares); 196 | 197 | return (account, sender, shares); 198 | } 199 | 200 | /** 201 | * @inheritdoc IStakingModule 202 | */ 203 | function claim( 204 | address sender, 205 | uint256 amount, 206 | bytes calldata 207 | ) external override onlyOwner returns (bytes32, address, uint256) { 208 | // validate 209 | require(amount > 0, "smn9"); 210 | require(amount <= counts[sender], "smn10"); 211 | 212 | bytes32 account = bytes32(uint256(uint160(sender))); 213 | uint256 shares = amount * SHARES_PER_TOKEN; 214 | emit Claimed(account, sender, address(_token), amount, shares); 215 | return (account, sender, shares); 216 | } 217 | 218 | /** 219 | * @inheritdoc IStakingModule 220 | */ 221 | function update( 222 | address sender, 223 | bytes calldata 224 | ) external pure override returns (bytes32) { 225 | return (bytes32(uint256(uint160(sender)))); 226 | } 227 | 228 | /** 229 | * @inheritdoc IStakingModule 230 | */ 231 | function clean(bytes calldata) external override {} 232 | } 233 | -------------------------------------------------------------------------------- /contracts/ERC20StakingModule.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20StakingModule 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 12 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 13 | 14 | import "./interfaces/IStakingModule.sol"; 15 | import "./OwnerController.sol"; 16 | import "./TokenUtils.sol"; 17 | 18 | /** 19 | * @title ERC20 staking module 20 | * 21 | * @notice this staking module allows users to deposit an amount of ERC20 token 22 | * in exchange for shares credited to their address. When the user 23 | * unstakes, these shares will be burned and a reward will be distributed. 24 | */ 25 | contract ERC20StakingModule is IStakingModule, OwnerController { 26 | using SafeERC20 for IERC20; 27 | using TokenUtils for IERC20; 28 | 29 | // events 30 | event Approval(address indexed user, address indexed operator, bool value); 31 | 32 | // members 33 | IERC20 private immutable _token; 34 | address private immutable _factory; 35 | 36 | mapping(address => uint256) public shares; 37 | uint256 public totalShares; 38 | mapping(address => mapping(address => bool)) public approvals; 39 | 40 | /** 41 | * @param token_ the token that will be staked 42 | * @param factory_ address of module factory 43 | */ 44 | constructor(address token_, address factory_) { 45 | require(token_ != address(0)); 46 | _token = IERC20(token_); 47 | _factory = factory_; 48 | } 49 | 50 | /** 51 | * @inheritdoc IStakingModule 52 | */ 53 | function tokens() 54 | external 55 | view 56 | override 57 | returns (address[] memory tokens_) 58 | { 59 | tokens_ = new address[](1); 60 | tokens_[0] = address(_token); 61 | } 62 | 63 | /** 64 | * @inheritdoc IStakingModule 65 | */ 66 | function balances( 67 | address user 68 | ) external view override returns (uint256[] memory balances_) { 69 | balances_ = new uint256[](1); 70 | balances_[0] = _balance(user); 71 | } 72 | 73 | /** 74 | * @inheritdoc IStakingModule 75 | */ 76 | function factory() external view override returns (address) { 77 | return _factory; 78 | } 79 | 80 | /** 81 | * @inheritdoc IStakingModule 82 | */ 83 | function totals() 84 | external 85 | view 86 | override 87 | returns (uint256[] memory totals_) 88 | { 89 | totals_ = new uint256[](1); 90 | totals_[0] = _token.balanceOf(address(this)); 91 | } 92 | 93 | /** 94 | * @inheritdoc IStakingModule 95 | */ 96 | function stake( 97 | address sender, 98 | uint256 amount, 99 | bytes calldata data 100 | ) external override onlyOwner returns (bytes32, uint256) { 101 | // validate 102 | require(amount > 0, "sm1"); 103 | address account = _account(sender, data); 104 | 105 | // transfer 106 | uint256 minted = _token.receiveAmount(totalShares, sender, amount); 107 | 108 | // update user staking info 109 | shares[account] += minted; 110 | 111 | // add newly minted shares to global total 112 | totalShares += minted; 113 | 114 | bytes32 account_ = bytes32(uint256(uint160(account))); 115 | emit Staked(account_, sender, address(_token), amount, minted); 116 | 117 | return (account_, minted); 118 | } 119 | 120 | /** 121 | * @inheritdoc IStakingModule 122 | */ 123 | function unstake( 124 | address sender, 125 | uint256 amount, 126 | bytes calldata data 127 | ) external override onlyOwner returns (bytes32, address, uint256) { 128 | // validate and get shares 129 | address account = _account(sender, data); 130 | uint256 burned = _shares(account, amount); 131 | 132 | // burn shares 133 | totalShares -= burned; 134 | shares[account] -= burned; 135 | 136 | // unstake 137 | _token.safeTransfer(sender, amount); 138 | 139 | bytes32 account_ = bytes32(uint256(uint160(account))); 140 | emit Unstaked(account_, sender, address(_token), amount, burned); 141 | 142 | return (account_, sender, burned); 143 | } 144 | 145 | /** 146 | * @inheritdoc IStakingModule 147 | */ 148 | function claim( 149 | address sender, 150 | uint256 amount, 151 | bytes calldata data 152 | ) external override onlyOwner returns (bytes32, address, uint256) { 153 | address account = _account(sender, data); 154 | uint256 s = _shares(account, amount); 155 | bytes32 account_ = bytes32(uint256(uint160(account))); 156 | emit Claimed(account_, sender, address(_token), amount, s); 157 | return (account_, sender, s); 158 | } 159 | 160 | /** 161 | * @inheritdoc IStakingModule 162 | */ 163 | function update( 164 | address sender, 165 | bytes calldata data 166 | ) external view override returns (bytes32) { 167 | return (bytes32(uint256(uint160(_account(sender, data))))); 168 | } 169 | 170 | /** 171 | * @inheritdoc IStakingModule 172 | */ 173 | function clean(bytes calldata) external override {} 174 | 175 | /** 176 | * @notice set approval for operators to act on user position 177 | * @param operator address of operator 178 | * @param value boolean to grant or revoke approval 179 | */ 180 | function approve(address operator, bool value) external { 181 | approvals[msg.sender][operator] = value; 182 | emit Approval(msg.sender, operator, value); 183 | } 184 | 185 | /** 186 | * @dev internal helper to get user balance 187 | * @param user address of interest 188 | */ 189 | function _balance(address user) private view returns (uint256) { 190 | return _token.getAmount(totalShares, shares[user]); 191 | } 192 | 193 | /** 194 | * @dev internal helper to validate and convert user stake amount to shares 195 | * @param user address of user 196 | * @param amount number of tokens to consider 197 | * @return shares_ equivalent number of shares 198 | */ 199 | function _shares( 200 | address user, 201 | uint256 amount 202 | ) private view returns (uint256 shares_) { 203 | // validate 204 | require(amount > 0, "sm3"); 205 | require(totalShares > 0, "sm4"); 206 | 207 | // convert token amount to shares 208 | shares_ = _token.getShares(totalShares, amount); 209 | 210 | require(shares_ > 0, "sm5"); 211 | require(shares[user] >= shares_, "sm6"); 212 | } 213 | 214 | /** 215 | * @dev internal helper to get account and validate approval 216 | * @param sender address of sender 217 | * @param data either empty bytes or encoded account address 218 | */ 219 | function _account( 220 | address sender, 221 | bytes calldata data 222 | ) private view returns (address) { 223 | require(data.length == 0 || data.length == 32, "sm7"); 224 | 225 | if (data.length > 0) { 226 | address account; 227 | assembly { 228 | account := calldataload(132) 229 | } 230 | require(approvals[account][sender], "sm8"); 231 | return account; 232 | } else { 233 | return sender; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /contracts/info/ERC20CompetitiveRewardModuleInfo.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ERC20CompetitiveRewardModuleInfo 3 | 4 | https://github.com/gysr-io/core 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | pragma solidity 0.8.18; 10 | 11 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 12 | 13 | import "../interfaces/IRewardModule.sol"; 14 | import "../ERC20CompetitiveRewardModule.sol"; 15 | import "../GysrUtils.sol"; 16 | 17 | /** 18 | * @title ERC20 competitive reward module info library 19 | * 20 | * @notice this library provides read-only convenience functions to query 21 | * additional information about the ERC20CompetitiveRewardModule contract. 22 | */ 23 | library ERC20CompetitiveRewardModuleInfo { 24 | using GysrUtils for uint256; 25 | 26 | /** 27 | * @notice get all token metadata 28 | * @param module address of reward module 29 | * @return addresses_ 30 | * @return names_ 31 | * @return symbols_ 32 | * @return decimals_ 33 | */ 34 | function tokens( 35 | address module 36 | ) 37 | external 38 | view 39 | returns ( 40 | address[] memory addresses_, 41 | string[] memory names_, 42 | string[] memory symbols_, 43 | uint8[] memory decimals_ 44 | ) 45 | { 46 | addresses_ = new address[](1); 47 | names_ = new string[](1); 48 | symbols_ = new string[](1); 49 | decimals_ = new uint8[](1); 50 | (addresses_[0], names_[0], symbols_[0], decimals_[0]) = token(module); 51 | } 52 | 53 | /** 54 | * @notice convenience function to get token metadata in a single call 55 | * @param module address of reward module 56 | * @return address 57 | * @return name 58 | * @return symbol 59 | * @return decimals 60 | */ 61 | function token( 62 | address module 63 | ) public view returns (address, string memory, string memory, uint8) { 64 | IRewardModule m = IRewardModule(module); 65 | IERC20Metadata tkn = IERC20Metadata(m.tokens()[0]); 66 | return (address(tkn), tkn.name(), tkn.symbol(), tkn.decimals()); 67 | } 68 | 69 | /** 70 | * @notice generic function to get pending reward balances 71 | * @param module address of reward module 72 | * @param account bytes32 account of interest for preview 73 | * @param shares number of shares that would be used 74 | * @return rewards_ estimated reward balances 75 | */ 76 | function rewards( 77 | address module, 78 | bytes32 account, 79 | uint256 shares, 80 | bytes calldata 81 | ) public view returns (uint256[] memory rewards_) { 82 | rewards_ = new uint256[](1); 83 | (rewards_[0], , ) = preview(module, account, shares, 0); 84 | } 85 | 86 | /** 87 | * @notice preview estimated rewards 88 | * @param module address of reward module 89 | * @param account bytes32 account of interest for preview 90 | * @param shares number of shares that would be unstaked 91 | * @param gysr number of GYSR tokens that would be applied 92 | * @return estimated reward 93 | * @return estimated time multiplier 94 | * @return estimated gysr multiplier 95 | */ 96 | function preview( 97 | address module, 98 | bytes32 account, 99 | uint256 shares, 100 | uint256 gysr 101 | ) public view returns (uint256, uint256, uint256) { 102 | ERC20CompetitiveRewardModule m = ERC20CompetitiveRewardModule(module); 103 | 104 | // get associated share seconds 105 | uint256 rawShareSeconds; 106 | uint256 bonusShareSeconds; 107 | (rawShareSeconds, bonusShareSeconds) = userShareSeconds( 108 | module, 109 | account, 110 | shares 111 | ); 112 | if (rawShareSeconds == 0) { 113 | return (0, 0, 0); 114 | } 115 | 116 | uint256 timeBonus = (bonusShareSeconds * 1e18) / rawShareSeconds; 117 | 118 | // apply gysr bonus 119 | uint256 gysrBonus = gysr.gysrBonus( 120 | shares, 121 | m.totalStakingShares(), 122 | m.usage() 123 | ); 124 | bonusShareSeconds = (gysrBonus * bonusShareSeconds) / 1e18; 125 | 126 | // compute rewards based on expected updates 127 | uint256 reward = (unlocked(module) * bonusShareSeconds) / 128 | (totalShareSeconds(module) + bonusShareSeconds - rawShareSeconds); 129 | 130 | return (reward, timeBonus, gysrBonus); 131 | } 132 | 133 | /** 134 | * @notice compute effective unlocked rewards 135 | * @param module address of reward module 136 | * @return estimated current unlocked rewards 137 | */ 138 | function unlocked(address module) public view returns (uint256) { 139 | ERC20CompetitiveRewardModule m = ERC20CompetitiveRewardModule(module); 140 | 141 | // compute expected updates to global totals 142 | uint256 deltaUnlocked; 143 | address tkn = m.tokens()[0]; 144 | uint256 totalLockedShares = m.lockedShares(tkn); 145 | if (totalLockedShares != 0) { 146 | uint256 sharesToUnlock; 147 | for (uint256 i = 0; i < m.fundingCount(tkn); i++) { 148 | sharesToUnlock = sharesToUnlock + m.unlockable(tkn, i); 149 | } 150 | deltaUnlocked = 151 | (sharesToUnlock * m.totalLocked()) / 152 | totalLockedShares; 153 | } 154 | return m.totalUnlocked() + deltaUnlocked; 155 | } 156 | 157 | /** 158 | * @notice compute user share seconds for given number of shares 159 | * @param module module contract address 160 | * @param account user account 161 | * @param shares number of shares 162 | * @return raw share seconds 163 | * @return time bonus share seconds 164 | */ 165 | function userShareSeconds( 166 | address module, 167 | bytes32 account, 168 | uint256 shares 169 | ) public view returns (uint256, uint256) { 170 | require(shares > 0, "crmi1"); 171 | 172 | ERC20CompetitiveRewardModule m = ERC20CompetitiveRewardModule(module); 173 | 174 | uint256 rawShareSeconds; 175 | uint256 timeBonusShareSeconds; 176 | 177 | // compute first-in-last-out, time bonus weighted, share seconds 178 | uint256 i = m.stakeCount(account); 179 | while (shares > 0) { 180 | require(i > 0, "crmi2"); 181 | i -= 1; 182 | uint256 s; 183 | uint256 time; 184 | (s, time) = m.stakes(account, i); 185 | time = block.timestamp - time; 186 | 187 | // only redeem partial stake if more shares left than needed to burn 188 | s = s < shares ? s : shares; 189 | 190 | rawShareSeconds += (s * time); 191 | timeBonusShareSeconds += ((s * time * m.timeBonus(time)) / 1e18); 192 | shares -= s; 193 | } 194 | return (rawShareSeconds, timeBonusShareSeconds); 195 | } 196 | 197 | /** 198 | * @notice compute total expected share seconds for a rewards module 199 | * @param module address for reward module 200 | * @return expected total shares seconds 201 | */ 202 | function totalShareSeconds(address module) public view returns (uint256) { 203 | ERC20CompetitiveRewardModule m = ERC20CompetitiveRewardModule(module); 204 | 205 | return 206 | m.totalStakingShareSeconds() + 207 | (block.timestamp - m.lastUpdated()) * 208 | m.totalStakingShares(); 209 | } 210 | } 211 | --------------------------------------------------------------------------------