diff --git a/packages/hardhat/contracts/IDStaking.sol b/packages/hardhat/contracts/IDStaking.sol new file mode 100644 index 0000000..95f5592 --- /dev/null +++ b/packages/hardhat/contracts/IDStaking.sol @@ -0,0 +1,208 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import {XStaking} from "./XStaking.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract IDStaking is XStaking, Ownable { + uint256 public latestRound; + + struct Round { + string meta; + uint256 tvl; + uint256 start; + uint256 duration; + } + + mapping(uint256 => Round) rounds; + + event roundCreated(uint256 id); + event tokenStaked(uint256 roundId, address staker, uint256 amount); + event xStaked( + uint256 roundId, + address staker, + address user, + uint256 amount, + bool staked + ); + event tokenUnstaked(uint256 roundId, address staker, uint256 amount); + event tokenMigrated(address staker, uint256 fromRound, uint256 toRound); + + modifier roundExists(uint256 roundId) { + require( + rounds[roundId].start > 0 && roundId > 0, + "Round does not exist" + ); + _; + } + + constructor(IERC20 _token) payable { + token = _token; + } + + function createRound( + uint256 start, + uint256 duration, + string memory meta + ) public onlyOwner { + if (latestRound > 0) { + require( + start > + rounds[latestRound].start + rounds[latestRound].duration, + "new rounds have to start after old rounds" + ); + } + + require(start > block.timestamp, "new rounds should be in the future"); + + latestRound++; + + rounds[latestRound].start = start; + rounds[latestRound].duration = duration; + rounds[latestRound].meta = meta; + + emit roundCreated(latestRound); + } + + // stake + function stake(uint256 roundId, uint256 amount) public { + require(isActiveRound(roundId), "Can't stake an inactive round"); + + _stake(roundId, amount); + + rounds[roundId].tvl += amount; + + emit tokenStaked(roundId, msg.sender, amount); + } + + // stakeUser + function stakeUsers( + uint256 roundId, + address[] memory users, + uint256[] memory amounts + ) public { + require(isActiveRound(roundId), "Can't stake an inactive round"); + require(users.length == amounts.length, "Unequal users and amount"); + + for (uint256 i = 0; i < users.length; i++) { + require(address(0) != users[i], "can't stake the zero address"); + require( + users[i] != msg.sender, + "You can't stake on your address here" + ); + _stakeUser(roundId, users[i], amounts[i]); + + rounds[roundId].tvl += amounts[i]; + + emit xStaked(roundId, msg.sender, users[i], amounts[i], true); + } + } + + // unstake + function unstake(uint256 roundId, uint256 amount) public { + require( + !isActiveRound(roundId), + "Can't unstake during an active round" + ); + require( + stakes[roundId][msg.sender] >= amount, + "Not enough balance to withdraw" + ); + + rounds[roundId].tvl -= amount; + + _unstake(roundId, amount); + + emit tokenUnstaked(roundId, msg.sender, amount); + } + + // unstakeUser + function unstakeUsers(uint256 roundId, address[] memory users) public { + require( + !isActiveRound(roundId), + "Can't unstake during an active round" + ); + + for (uint256 i = 0; i < users.length; i++) { + require(address(0) != users[i], "can't stake the zero address"); + require( + users[i] != msg.sender, + "You can't stake on your address here" + ); + + bytes32 stakeId = getStakeId(msg.sender, users[i]); + uint256 unstakeBalance = xStakes[roundId][stakeId]; + + if (unstakeBalance > 0) { + rounds[roundId].tvl -= unstakeBalance; + + _unstakeUser(roundId, users[i], unstakeBalance); + + emit xStaked( + roundId, + msg.sender, + users[i], + unstakeBalance, + false + ); + } + } + } + + // migrateStake + function migrateStake(uint256 fromRound) public { + require(fromRound < latestRound, "Can't migrate from an active round"); + + uint256 balance = stakes[fromRound][msg.sender]; + + require(balance > 0, "Not enough balance to migrate"); + + rounds[fromRound].tvl -= balance; + stakes[fromRound][msg.sender] = 0; + rounds[latestRound].tvl += balance; + stakes[latestRound][msg.sender] = balance; + + emit tokenUnstaked(fromRound, msg.sender, balance); + emit tokenStaked(latestRound, msg.sender, balance); + emit tokenMigrated(msg.sender, fromRound, latestRound); + } + + // VIEW + function fetchRoundMeta(uint256 roundId) + public + view + roundExists(roundId) + returns ( + uint256 start, + uint256 duration, + uint256 tvl + ) + { + return ( + rounds[roundId].start, + rounds[roundId].duration, + rounds[roundId].tvl + ); + } + + function isActiveRound(uint256 roundId) + public + view + returns (bool isActive) + { + (uint256 start, uint256 duration, ) = fetchRoundMeta(roundId); + isActive = + start < block.timestamp && + start + duration > block.timestamp; + } + + function getUserStakeForRound(uint256 roundId, address user) + public + view + roundExists(roundId) + returns (uint256) + { + return _getUserStakeForRound(roundId, user); + } +} diff --git a/packages/hardhat/contracts/Staking.sol b/packages/hardhat/contracts/Staking.sol index 0a8f254..f316bbf 100644 --- a/packages/hardhat/contracts/Staking.sol +++ b/packages/hardhat/contracts/Staking.sol @@ -1,152 +1,33 @@ //SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Staking is Ownable { +contract Staking { IERC20 public token; - uint256 public latestRound; - struct Round { - string meta; - uint256 tvl; - uint256 start; - uint256 duration; - mapping(address => uint256) stakes; - } - - mapping(uint256 => Round) rounds; - - event roundCreated(uint256 id); - event tokenStaked(uint256 roundId, address staker, uint256 amount); - event tokenUnstaked(uint256 roundId, address staker, uint256 amount); - - modifier roundExists(uint256 roundId) { - require( - rounds[roundId].start > 0 && roundId > 0, - "Round does not exist" - ); - _; - } - - constructor(IERC20 _token) payable { - token = _token; - } - - function createRound( - uint256 start, - uint256 duration, - string memory meta - //Removed onlyOwner modifier for testing purposes - ) public { - - // REMOVING these require statements because they are not necessary to test staking and locking - // if (latestRound > 0) { - // require( - // start > - // rounds[latestRound].start + rounds[latestRound].duration, - // "new rounds have to start after old rounds" - // ); - // } - - // require(start > block.timestamp, "new rounds should be in the future"); - - latestRound++; - - rounds[latestRound].start = start; - rounds[latestRound].duration = duration; - rounds[latestRound].meta = meta; - - emit roundCreated(latestRound); - } + mapping(uint256 => mapping(address => uint256)) public stakes; // stake - function stake(uint256 roundId, uint256 amount) public { - // require(isActiveRound(roundId), "Can't stake an inactive round"); - + function _stake(uint256 roundId, uint256 amount) internal { token.transferFrom(msg.sender, address(this), amount); - rounds[roundId].tvl += amount; - - rounds[roundId].stakes[msg.sender] += amount; - - emit tokenStaked(roundId, msg.sender, amount); + stakes[roundId][msg.sender] += amount; } // unstake - function unstake(uint256 roundId, uint256 amount) public { - // require( - // !isActiveRound(roundId), - // "Can't unstake during an active round" - // ); - require( - rounds[roundId].stakes[msg.sender] >= amount, - "Not enough balance to withdraw" - ); - - rounds[roundId].tvl -= amount; - - rounds[roundId].stakes[msg.sender] -= amount; + function _unstake(uint256 roundId, uint256 amount) internal { + stakes[roundId][msg.sender] -= amount; token.transfer(msg.sender, amount); - - emit tokenUnstaked(roundId, msg.sender, amount); - } - - // migrateStake - function migrateStake(uint256 fromRound) public { - require(fromRound < latestRound, "Can't migrate from an active round"); - - uint256 balance = rounds[fromRound].stakes[msg.sender]; - - require(balance > 0, "Not enough balance to migrate"); - - rounds[fromRound].tvl -= balance; - rounds[fromRound].stakes[msg.sender] = 0; - rounds[latestRound].tvl += balance; - rounds[latestRound].stakes[msg.sender] = balance; - - emit tokenUnstaked(fromRound, msg.sender, balance); - emit tokenStaked(latestRound, msg.sender, balance); } - // VIEW - function fetchRoundMeta(uint256 roundId) - public + function _getUserStakeForRound(uint256 roundId, address user) + internal view - roundExists(roundId) - returns ( - uint256 start, - uint256 duration, - uint256 tvl - ) - { - return ( - rounds[roundId].start, - rounds[roundId].duration, - rounds[roundId].tvl - ); - } - - function isActiveRound(uint256 roundId) - public - view - returns (bool isActive) - { - (uint256 start, uint256 duration, ) = fetchRoundMeta(roundId); - isActive = - start < block.timestamp && - start + duration > block.timestamp; - } - - function getUserStakeForRound(uint256 roundId, address user) - public - view - ///roundExists(roundId) returns (uint256) { - return rounds[roundId].stakes[user]; + return stakes[roundId][user]; } function getUserStakeFromLatestRound(address user) diff --git a/packages/hardhat/contracts/XStaking.sol b/packages/hardhat/contracts/XStaking.sol new file mode 100644 index 0000000..e671f79 --- /dev/null +++ b/packages/hardhat/contracts/XStaking.sol @@ -0,0 +1,48 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import {Staking} from "./Staking.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract XStaking is Staking { + mapping(uint256 => mapping(bytes32 => uint256)) public xStakes; + + // stake + function _stakeUser( + uint256 roundId, + address user, + uint256 amount + ) internal { + // TODO : Optimize to transfer tokens in IDStaking instead + token.transferFrom(msg.sender, address(this), amount); + + xStakes[roundId][getStakeId(msg.sender, user)] += amount; + } + + // unstake + function _unstakeUser( + uint256 roundId, + address user, + uint256 amount + ) internal { + xStakes[roundId][getStakeId(msg.sender, user)] -= amount; + + token.transfer(msg.sender, amount); + } + + function _getUserXStakeForRound( + uint256 roundId, + address staker, + address user + ) internal view returns (uint256) { + return xStakes[roundId][getStakeId(staker, user)]; + } + + function getStakeId(address staker, address user) + public + pure + returns (bytes32) + { + return keccak256(abi.encode(staker, user)); + } +} diff --git a/packages/hardhat/deploy/00_deploy_staking.js b/packages/hardhat/deploy/00_deploy_staking.js index a7622f1..5351e11 100644 --- a/packages/hardhat/deploy/00_deploy_staking.js +++ b/packages/hardhat/deploy/00_deploy_staking.js @@ -21,7 +21,7 @@ module.exports = async ({ getNamedAccounts, deployments, getChainId }) => { const stakingArgs = [Token.address]; - await deploy("Staking", { + await deploy("IDStaking", { // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy from: deployer, args: stakingArgs, @@ -30,8 +30,8 @@ module.exports = async ({ getNamedAccounts, deployments, getChainId }) => { }); // Getting a previously deployed contract - const Staking = await ethers.getContract("Staking", deployer); - await Staking.transferOwnership(admin); + const IDStaking = await ethers.getContract("IDStaking", deployer); + await IDStaking.transferOwnership(admin); // Verify from the command line by running `yarn verify` @@ -40,8 +40,8 @@ module.exports = async ({ getNamedAccounts, deployments, getChainId }) => { try { if (chainId !== localChainId) { await run("verify:verify", { - address: Staking.address, - contract: "contracts/Staking.sol:Staking", + address: IDStaking.address, + contract: "contracts/IDStaking.sol:IDStaking", constructorArguments: stakingArgs, }); } @@ -49,4 +49,4 @@ module.exports = async ({ getNamedAccounts, deployments, getChainId }) => { console.error(error); } }; -module.exports.tags = ["Staking", "Token"]; +module.exports.tags = ["IDStaking", "Token"]; diff --git a/packages/react-app/src/App.jsx b/packages/react-app/src/App.jsx index 6e89d57..36eaa2e 100644 --- a/packages/react-app/src/App.jsx +++ b/packages/react-app/src/App.jsx @@ -22,7 +22,7 @@ import externalContracts from "./contracts/external_contracts"; // contracts import deployedContracts from "./contracts/hardhat_contracts.json"; import { Transactor, Web3ModalSetup } from "./helpers"; -import { Admin, Home, Subgraph } from "./views"; +import { Admin, Home, Stakes, Subgraph } from "./views"; import { useStaticJsonRPC } from "./hooks"; const { ethers } = require("ethers"); @@ -60,7 +60,7 @@ const web3Modal = Web3ModalSetup(); const providers = [ "https://eth-mainnet.gateway.pokt.network/v1/lb/611156b4a585a20035148406", `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`, - "https://rpc.scaffoldeth.io:48544", + // "https://rpc.scaffoldeth.io:48544", ]; function App(props) { @@ -179,7 +179,7 @@ function App(props) { return (