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 (
{/* ✏️ Edit the header and change the title to your project name */} -
+
{/* 👨‍💼 Your account is in the top right with a wallet at connect options */}
@@ -222,6 +222,9 @@ function App(props) { App Home + + xStake Log + Admin Control @@ -254,6 +257,17 @@ function App(props) { mainnetProvider={mainnetProvider} /> + + {/* pass in any web3 props to this Home component. For example, yourLocalBalance */} + + {/* 🎛 this scaffolding is full of commonly used components @@ -271,7 +285,7 @@ function App(props) { contractConfig={contractConfig} /> {/* 🗺 Extra UI like gas price, eth price, faucet, and support: */} -
+ {/*
@@ -320,17 +334,14 @@ function App(props) { - { - /* if the local provider has a signer, let's show the faucet: */ - faucetAvailable ? ( - - ) : ( - "" - ) - } + {faucetAvailable ? ( + + ) : ( + "" + )} -
+
*/}
); } diff --git a/packages/react-app/src/components/Rounds.jsx b/packages/react-app/src/components/Rounds.jsx index 1412447..fa46dc4 100644 --- a/packages/react-app/src/components/Rounds.jsx +++ b/packages/react-app/src/components/Rounds.jsx @@ -1,16 +1,35 @@ -import { Button } from "antd"; +import { Button, Divider, Form, InputNumber } from "antd"; import { useContractReader } from "eth-hooks"; import { ethers } from "ethers"; import moment from "moment"; +import AddressInput from "./AddressInput"; const zero = ethers.BigNumber.from("0"); -const Rounds = ({ tokenSymbol, address, readContracts, stake, unstake, migrate, round, latestRound }) => { +const Rounds = ({ + tokenSymbol, + address, + readContracts, + stake, + unstake, + migrate, + round, + latestRound, + mainnetProvider, + stakeUsers, + unstakeUsers, +}) => { + const [form] = Form.useForm(); const stakedBalance = ethers.utils.formatUnits( - useContractReader(readContracts, "Staking", "getUserStakeForRound", [round, address]) || zero, + useContractReader(readContracts, "IDStaking", "getUserStakeForRound", [round, address]) || zero, ); - const [start, duration, tvl] = useContractReader(readContracts, "Staking", "fetchRoundMeta", [round]) || []; + const [start, duration, tvl] = useContractReader(readContracts, "IDStaking", "fetchRoundMeta", [round]) || []; + + const handleStakeUsers = async v => { + console.log(v); + await stakeUsers(round, [v.address], [ethers.utils.parseUnits(`${v.amount}`)]); + }; return (
-
+ Your Stakings +
@@ -48,6 +68,30 @@ const Rounds = ({ tokenSymbol, address, readContracts, stake, unstake, migrate, )}
+ + Cross Stakings +
+
+ + + + + + + + + +
+
); diff --git a/packages/react-app/src/views/Admin.jsx b/packages/react-app/src/views/Admin.jsx index 97c00c2..8e230e2 100644 --- a/packages/react-app/src/views/Admin.jsx +++ b/packages/react-app/src/views/Admin.jsx @@ -4,7 +4,7 @@ function Admin({ tx, writeContracts }) { const [form] = Form.useForm(); const initialize = async v => { - tx(writeContracts.Staking.createRound(v.start, v.duration, "")); + tx(writeContracts.IDStaking.createRound(v.start, v.duration, "")); }; return ( diff --git a/packages/react-app/src/views/Home.jsx b/packages/react-app/src/views/Home.jsx index d45d49d..8c2793e 100644 --- a/packages/react-app/src/views/Home.jsx +++ b/packages/react-app/src/views/Home.jsx @@ -11,7 +11,7 @@ function Home({ tx, readContracts, address, writeContracts, mainnetProvider }) { useContractReader(readContracts, "Token", "balanceOf", [address]) || zero, ); const tokenSymbol = useContractReader(readContracts, "Token", "symbol"); - const latestRound = (useContractReader(readContracts, "Staking", "latestRound", []) || zero).toNumber(); + const latestRound = (useContractReader(readContracts, "IDStaking", "latestRound", []) || zero).toNumber(); const rounds = [...Array(latestRound).keys()].map(i => i + 1).reverse(); @@ -20,19 +20,27 @@ function Home({ tx, readContracts, address, writeContracts, mainnetProvider }) { }; const approve = async () => { - tx(writeContracts.Token.approve(readContracts.Staking.address, ethers.utils.parseUnits("10000000"))); + tx(writeContracts.Token.approve(readContracts.IDStaking.address, ethers.utils.parseUnits("10000000"))); }; const stake = async (id, amount) => { - tx(writeContracts.Staking.stake(id + "", ethers.utils.parseUnits(amount))); + tx(writeContracts.IDStaking.stake(id + "", ethers.utils.parseUnits(amount))); + }; + + const stakeUsers = async (id, users, amounts) => { + tx(writeContracts.IDStaking.stakeUsers(id + "", users, amounts)); }; const unstake = async (id, amount) => { - tx(writeContracts.Staking.unstake(id + "", ethers.utils.parseUnits(amount))); + tx(writeContracts.IDStaking.unstake(id + "", ethers.utils.parseUnits(amount))); + }; + + const unstakeUsers = async (id, users, amount) => { + tx(writeContracts.IDStaking.unstakeUsers(id + "", users)); }; const migrate = async id => { - tx(writeContracts.Staking.migrateStake(id + "")); + tx(writeContracts.IDStaking.migrateStake(id + "")); }; return ( @@ -70,9 +78,12 @@ function Home({ tx, readContracts, address, writeContracts, mainnetProvider }) { unstake={unstake} address={address} migrate={migrate} + stakeUsers={stakeUsers} latestRound={latestRound} tokenSymbol={tokenSymbol} + unstakeUsers={unstakeUsers} readContracts={readContracts} + mainnetProvider={mainnetProvider} /> ))} diff --git a/packages/react-app/src/views/Stakes.jsx b/packages/react-app/src/views/Stakes.jsx new file mode 100644 index 0000000..29f1899 --- /dev/null +++ b/packages/react-app/src/views/Stakes.jsx @@ -0,0 +1,41 @@ +import { Typography } from "antd"; +import { ethers } from "ethers"; +import { useEventListener } from "eth-hooks/events/useEventListener"; +import { Address } from "../components"; +import { useContractReader } from "eth-hooks"; + +function Stakes({ readContracts, localProvider, mainnetProvider }) { + const tokenSymbol = useContractReader(readContracts, "Token", "symbol"); + + const stakeLogs = (useEventListener(readContracts, "IDStaking", "xStaked", localProvider) || []).reverse(); + + return ( + <> +
+
+ Stake Logs +
+
+ {stakeLogs.map(stake => ( + + Round {stake.args.roundId?.toString()}:{" "} +
{" "} + {stake.args.staked ? "staked" : "unstaked"} {ethers.utils.formatUnits(stake.args.amount)} {tokenSymbol} on{" "} +
{" "} + + ))} +
+
+ + ); +} + +export default Stakes; diff --git a/packages/react-app/src/views/index.js b/packages/react-app/src/views/index.js index f148037..41767e7 100644 --- a/packages/react-app/src/views/index.js +++ b/packages/react-app/src/views/index.js @@ -1,3 +1,4 @@ export { default as Admin } from "./Admin"; export { default as Home } from "./Home"; +export { default as Stakes } from "./Stakes"; export { default as Subgraph } from "./Subgraph";