From ecacc47c98c6a4b085eb120eecbdaee051a5af97 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 24 Jun 2024 15:58:48 +0300 Subject: [PATCH 01/60] Add base token pool --- contracts/staking/token/PoolsManager.sol | 11 +++ contracts/staking/token/ShareToken.sol | 19 +++++ contracts/staking/token/TokenPool.sol | 94 ++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 contracts/staking/token/PoolsManager.sol create mode 100644 contracts/staking/token/ShareToken.sol create mode 100644 contracts/staking/token/TokenPool.sol diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol new file mode 100644 index 00000000..bd610328 --- /dev/null +++ b/contracts/staking/token/PoolsManager.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +// This contract should +// - Deploy a new TokenPool contract on demand +// - Keep track of all deployed TokenPool contracts +// - Allow to query the list of deployed TokenPool contracts +// - Allow to deactivate the TokenPool contract + +contract PoolsManager { +} diff --git a/contracts/staking/token/ShareToken.sol b/contracts/staking/token/ShareToken.sol new file mode 100644 index 00000000..1fe770e0 --- /dev/null +++ b/contracts/staking/token/ShareToken.sol @@ -0,0 +1,19 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ShareToken is ERC20, Ownable { + + constructor() ERC20("PoolToken", "PT") Ownable() { + } + + function burn(address account, uint amount) public onlyOwner() { + _burn(account, amount); + } + + function mint(address to, uint amount) public onlyOwner() { + _mint(to, amount); + } +} diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol new file mode 100644 index 00000000..6ec70c0d --- /dev/null +++ b/contracts/staking/token/TokenPool.sol @@ -0,0 +1,94 @@ +//SPDX-License-Identifier: UNCLICENSED +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./ShareToken.sol"; +import "../../funds/RewardsBank.sol"; + +// This contract should +// - Accept sertain ERC20 token +// - Keep track of the balance of the token +// - Calculate the share of the token for each user +// - Able to increase the stake from the bank contract by the special call from the server +// - Allow users to withdraw their token with interest + +contract TokenPool is AccessControl { + bytes32 constant public BACKEND_ROLE = keccak256("BACKEND_ROLE"); + + uint constant public MILLION = 1_000_000; + //TODO: Get decimals from the token contract + uint constant public FIXED_POINT = 1e18; + //TODO: Add SafeMath??? + + uint public id; + IERC20 public token; + ShareToken public share; + uint public intereset; + uint public minStakeValue; + uint public totalStake; + bool public active; + RewardsBank public bank; + + constructor(uint _id, address _token, uint _intereset, uint _minStakeValue, RewardsBank _rewardsBank) { + id = _id; + token = IERC20(_token); + share = new ShareToken(); + intereset = _intereset; + minStakeValue = _minStakeValue; + active = true; + bank = _rewardsBank; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + event StakeChanched(address indexed user, uint amount); + + function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { + require(!active, "Pool is already active"); + active = true; + } + + function deactivate() public onlyRole(DEFAULT_ADMIN_ROLE) { + require(active, "Pool is not active"); + active = false; + } + + function stake(uint amount) public { + require(active, "Pool is not active"); + require(amount >= minStakeValue, "Amount is less than minStakeValue"); + require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + + uint shareAmount = calculateShare(amount); + + share.mint(msg.sender, shareAmount); + totalStake += amount; + emit Stake(msg.sender, amount); + } + + function unstake() public {} + + function increaseStake() public onlyRole(BACKEND_ROLE) { + require(active, "Pool is not active"); + uint amount = totalStake * intereset / MILLION; + bank.transfer( + } + + function getSharePrice() public view returns (uint) { + uint totalShare = share.totalSupply(); + if (totalShare == 0) return 1 ether; + + return totalShare * FIXED_POINT / totalStake; + } + + function calculateShare(uint tokenAmount) private view returns (uint) { + uint sharePrice = getSharePrice(); + return tokenAmount * sharePrice / FIXED_POINT; + } + + function calculateToken(uint shareAmount) private view returns (uint) { + uint sharePrice = getSharePrice(); + return shareAmount * FIXED_POINT / sharePrice; + } + +} From f54445cb28b01c3850020b734f7d4c63a3db090c Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 26 Jun 2024 13:44:01 +0300 Subject: [PATCH 02/60] Add contracts --- contracts/staking/token/IPoolManager.sol | 25 ++++++ contracts/staking/token/ITokenPool.sol | 31 +++++++ contracts/staking/token/PoolsManager.sol | 73 ++++++++++++++-- contracts/staking/token/TokenPool.sol | 102 +++++++++++++++-------- 4 files changed, 191 insertions(+), 40 deletions(-) create mode 100644 contracts/staking/token/IPoolManager.sol create mode 100644 contracts/staking/token/ITokenPool.sol diff --git a/contracts/staking/token/IPoolManager.sol b/contracts/staking/token/IPoolManager.sol new file mode 100644 index 00000000..f6c39851 --- /dev/null +++ b/contracts/staking/token/IPoolManager.sol @@ -0,0 +1,25 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +interface IPoolManager { + //OWNER METHODS + function createPool(address token_, uint interest_, uint minStakeValue) external returns (address); + function deactivatePool(address pool_) external; + function activatePool(address pool_) external; + function setInterest(address pool_, uint interest_) external; + function setMinStakeValue(address pool_, uint minStakeValue_) external; + + //VIEW METHODS + function getPool(address token_) external view returns (address); + function getPoolInfo(address pool_) external view returns (address token, uint interest, uint minStakeValue, uint totalStake, uint totalShare, bool active); + + //EVENTS + + event PoolCreated(address pool, address token, uint interest, uint minStakeValue); + event PoolDeactivated(address pool); + event PoolActivated(address pool); + event InterestChanged(address pool, uint interest); + event MinStakeValueChanged(address pool, uint minStakeValue); + + +} diff --git a/contracts/staking/token/ITokenPool.sol b/contracts/staking/token/ITokenPool.sol new file mode 100644 index 00000000..4dcb9a74 --- /dev/null +++ b/contracts/staking/token/ITokenPool.sol @@ -0,0 +1,31 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +interface ITokenPool { + //OWNER METHODS + function activate() external; + function deactivate() external; + function setInterest(uint _interest) external; + function setMinStakeValue(uint _minStakeValue) external; + + //BACKEND METHODS + function increaseStake() external; + + //PUBLIC METHODS + function stake(uint amount) external; + function unstake(uint amount) external; + + //VIEW METHODS + function getStake(address user) external view returns (uint); + function getSharePrice() external view returns (uint); + function totalShare() external view returns (uint); + + //EVENTS + event StakeChanged(address indexed user, uint amount); + event InterestChanged(uint interest); + event MinStakeValueChanged(uint minStakeValue); + event Deactivated(); + event Activated(); + + +} diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index bd610328..5e2ae1c9 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -1,11 +1,72 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -// This contract should -// - Deploy a new TokenPool contract on demand -// - Keep track of all deployed TokenPool contracts -// - Allow to query the list of deployed TokenPool contracts -// - Allow to deactivate the TokenPool contract +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./TokenPool.sol"; +import "./IPoolManager.sol"; +import "../../funds/RewardsBank.sol"; -contract PoolsManager { +contract PoolsManager is Ownable, IPoolManager { + + RewardsBank public bank; + + mapping(address => address) public pools; //maps the address of the token to the address of the pool + + constructor(RewardsBank bank_) Ownable() { + bank = bank_; + } + + // OWNER METHODS + + function createPool(address token_, uint interest_, uint minStakeValue) public onlyOwner() returns (address) { + TokenPool pool = new TokenPool(token_, bank, interest_, minStakeValue); + pools[address(pool)] = address(pool); + emit PoolCreated(address(pool), token_, interest_, minStakeValue); + pool.activate(); + emit PoolActivated(address(pool)); + return address(pool); + } + + function deactivatePool(address pool_) public onlyOwner() { + require(pools[pool_] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pool_); + pool.deactivate(); + emit PoolDeactivated(pool_); + } + + function activatePool(address pool_) public onlyOwner() { + require(pools[pool_] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pool_); + pool.activate(); + emit PoolActivated(pool_); + } + + function setInterest(address pool_, uint interest_) public onlyOwner() { + require(pools[pool_] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pool_); + pool.setInterest(interest_); + } + + function setMinStakeValue(address pool_, uint minStakeValue_) public onlyOwner() { + require(pools[pool_] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pool_); + pool.setMinStakeValue(minStakeValue_); + } + + function grantBackendRole(address pool_, address backend_) public onlyOwner() { + require(pools[pool_] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pool_); + pool.grantRole(pool.BACKEND_ROLE(), backend_); + } + + // VIEW METHODS + + function getPool(address token_) public view returns (address) { + return pools[token_]; + } + + function getPoolInfo(address pool_) public view returns (address token, uint interest, uint minStakeValue, uint totalStake, uint totalShare,bool) { + TokenPool pool = TokenPool(pool_); + return (address(pool.token()), pool.interest(), pool.minStakeValue(), pool.totalStake(), pool.totalShare(), pool.active()); + } } diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 6ec70c0d..c8e09f3e 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -2,93 +2,127 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/AccessControl.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./ShareToken.sol"; +import "./ITokenPool.sol"; import "../../funds/RewardsBank.sol"; -// This contract should -// - Accept sertain ERC20 token -// - Keep track of the balance of the token -// - Calculate the share of the token for each user -// - Able to increase the stake from the bank contract by the special call from the server -// - Allow users to withdraw their token with interest - -contract TokenPool is AccessControl { +contract TokenPool is AccessControl, ITokenPool { bytes32 constant public BACKEND_ROLE = keccak256("BACKEND_ROLE"); uint constant public MILLION = 1_000_000; - //TODO: Get decimals from the token contract - uint constant public FIXED_POINT = 1e18; - //TODO: Add SafeMath??? - uint public id; - IERC20 public token; + ERC20 public token; ShareToken public share; - uint public intereset; + RewardsBank public bank; + uint public interest; uint public minStakeValue; uint public totalStake; bool public active; - RewardsBank public bank; - constructor(uint _id, address _token, uint _intereset, uint _minStakeValue, RewardsBank _rewardsBank) { - id = _id; - token = IERC20(_token); + constructor(address _token, RewardsBank _rewardsBank, uint _intereset, uint _minStakeValue ) { + token = ERC20(_token); share = new ShareToken(); - intereset = _intereset; + bank = _rewardsBank; + interest = _intereset; minStakeValue = _minStakeValue; active = true; - bank = _rewardsBank; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } - event StakeChanched(address indexed user, uint amount); + // OWNER METHODS function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { require(!active, "Pool is already active"); active = true; + emit Activated(); } function deactivate() public onlyRole(DEFAULT_ADMIN_ROLE) { require(active, "Pool is not active"); active = false; + emit Deactivated(); + } + + function setInterest(uint _interest) public onlyRole(DEFAULT_ADMIN_ROLE) { + interest = _interest; + emit InterestChanged(_interest); + } + + function setMinStakeValue(uint _minStakeValue) public onlyRole(DEFAULT_ADMIN_ROLE) { + minStakeValue = _minStakeValue; + emit MinStakeValueChanged(_minStakeValue); } + // PUBLIC METHODS + function stake(uint amount) public { require(active, "Pool is not active"); require(amount >= minStakeValue, "Amount is less than minStakeValue"); require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); - uint shareAmount = calculateShare(amount); + uint shareAmount = _calculateShare(amount); share.mint(msg.sender, shareAmount); totalStake += amount; - emit Stake(msg.sender, amount); + emit StakeChanged(msg.sender, amount); + } + + function unstake(uint amount) public { + require(active, "Pool is not active"); + require(share.balanceOf(msg.sender) >= amount, "Not enough stake"); + + uint tokenAmount = _calculateToken(amount); + require(tokenAmount <= totalStake, "Amount is more than total stake"); + + share.burn(msg.sender, amount); + totalStake -= tokenAmount; + require(token.transfer(msg.sender, tokenAmount), "Transfer failed"); + emit StakeChanged(msg.sender, tokenAmount); } - function unstake() public {} + // BACKEND METHODS function increaseStake() public onlyRole(BACKEND_ROLE) { require(active, "Pool is not active"); - uint amount = totalStake * intereset / MILLION; - bank.transfer( + uint amount = totalStake * interest / MILLION; + bank.withdrawErc20(address(token), address(this), amount); + emit StakeChanged(address(this), amount); + } + + // VIEW METHODS + + function getStake(address user) public view returns (uint) { + return _calculateToken(share.balanceOf(user)); } function getSharePrice() public view returns (uint) { - uint totalShare = share.totalSupply(); - if (totalShare == 0) return 1 ether; + uint _totalShare = share.totalSupply(); + if (_totalShare == 0) return 1 ether; + uint decimals = token.decimals(); + + return _totalShare * decimals / totalStake; + } - return totalShare * FIXED_POINT / totalStake; + function totalShare() public view returns (uint) { + return share.totalSupply(); } - function calculateShare(uint tokenAmount) private view returns (uint) { + // INTERNAL METHODS + + function _calculateShare(uint tokenAmount) private view returns (uint) { uint sharePrice = getSharePrice(); - return tokenAmount * sharePrice / FIXED_POINT; + uint decimals = token.decimals(); + + return tokenAmount * sharePrice / decimals; } - function calculateToken(uint shareAmount) private view returns (uint) { + function _calculateToken(uint shareAmount) private view returns (uint) { uint sharePrice = getSharePrice(); - return shareAmount * FIXED_POINT / sharePrice; + uint decimals = token.decimals(); + + return shareAmount * decimals / sharePrice; } } From 9249cc026dac208dfac5a36bc23f194fc764ef0d Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 26 Jun 2024 14:42:57 +0300 Subject: [PATCH 03/60] Add deploy script --- .../{IPoolManager.sol => IPoolsManager.sol} | 2 +- contracts/staking/token/PoolsManager.sol | 4 +- scripts/staking/deploy_poolManager.ts | 52 +++++++++++++++++++ src/contracts/names.ts | 6 +++ 4 files changed, 61 insertions(+), 3 deletions(-) rename contracts/staking/token/{IPoolManager.sol => IPoolsManager.sol} (97%) create mode 100644 scripts/staking/deploy_poolManager.ts diff --git a/contracts/staking/token/IPoolManager.sol b/contracts/staking/token/IPoolsManager.sol similarity index 97% rename from contracts/staking/token/IPoolManager.sol rename to contracts/staking/token/IPoolsManager.sol index f6c39851..97e7707a 100644 --- a/contracts/staking/token/IPoolManager.sol +++ b/contracts/staking/token/IPoolsManager.sol @@ -1,7 +1,7 @@ //SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -interface IPoolManager { +interface IPoolsMsanager { //OWNER METHODS function createPool(address token_, uint interest_, uint minStakeValue) external returns (address); function deactivatePool(address pool_) external; diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index 5e2ae1c9..158009e0 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "./TokenPool.sol"; -import "./IPoolManager.sol"; +import "./IPoolsManager.sol"; import "../../funds/RewardsBank.sol"; -contract PoolsManager is Ownable, IPoolManager { +contract PoolsManager is Ownable, IPoolsManager { RewardsBank public bank; diff --git a/scripts/staking/deploy_poolManager.ts b/scripts/staking/deploy_poolManager.ts new file mode 100644 index 00000000..5c3604da --- /dev/null +++ b/scripts/staking/deploy_poolManager.ts @@ -0,0 +1,52 @@ +import { ethers } from "hardhat"; +import { deploy, loadDeployment } from "@airdao/deployments/deploying"; + +import { ContractNames } from "../../src"; + +import { + PoolsManager__factory, + Multisig__factory, + RewardsBank__factory, +} from "../../typechain-types"; +import {Roadmap2023MultisigSettings} from "../addresses"; +export async function main() { + const { chainId } = await ethers.provider.getNetwork(); + + const [deployer] = await ethers.getSigners(); + + + const masterMultisig = loadDeployment(ContractNames.MasterMultisig, chainId).address; + + const multisig = await deploy({ + contractName: ContractNames.PoolManagerMultisig, + artifactName: "Multisig", + deployArgs: [...Roadmap2023MultisigSettings, masterMultisig], + signer: deployer, + loadIfAlreadyDeployed: true, + }); + + const rewardsBank = await deploy({ + contractName: ContractNames.PoolManagerRewardsBank, + artifactName: "RewardsBank", + deployArgs: [], + signer: deployer, + }); + + const poolsManager = await deploy({ + contractName: ContractNames.PoolManager, + artifactName: "PoolsManager", + deployArgs: [rewardsBank.address], + signer: deployer, + }); + + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); + await (await poolsManager.transferOwnership(multisig.address)).wait(); +} + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/src/contracts/names.ts b/src/contracts/names.ts index 7e72d313..3322bb5b 100644 --- a/src/contracts/names.ts +++ b/src/contracts/names.ts @@ -55,6 +55,11 @@ export enum ContractNames { FeesMultisig = "Fees_Multisig", FeesTreasure = "Fees_Treasure", + TokenPool = "TokenPool", + PoolManager = "PoolManager", + PoolManagerMultisig = "PoolManager_Multisig", + PoolManagerRewardsBank = "PoolManager_RewardsBank", + // bond marketplace BondMarketplaceMultisig = "BondMarketplace_Multisig", @@ -81,6 +86,7 @@ export const MULTISIGS = { [ContractNames.FeesTreasure]: ContractNames.FeesMultisig, [ContractNames.BondMarketplaceRewardsBank]: ContractNames.BondMarketplaceMultisig, + [ContractNames.PoolManager]: ContractNames.PoolManagerMultisig, }; export const slavesMultisigsNames = [...new Set(Object.values(MULTISIGS))]; From 319995cce1da626d8f07307d9ac472697ffe7fda Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 26 Jun 2024 16:51:26 +0300 Subject: [PATCH 04/60] Add tests --- contracts/staking/token/IPoolsManager.sol | 2 +- contracts/staking/token/PoolsManager.sol | 20 +++--- test/staking/token/PoolsManager.ts | 87 +++++++++++++++++++++++ test/staking/token/TokenPool.ts | 0 4 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 test/staking/token/PoolsManager.ts create mode 100644 test/staking/token/TokenPool.ts diff --git a/contracts/staking/token/IPoolsManager.sol b/contracts/staking/token/IPoolsManager.sol index 97e7707a..f775cd20 100644 --- a/contracts/staking/token/IPoolsManager.sol +++ b/contracts/staking/token/IPoolsManager.sol @@ -1,7 +1,7 @@ //SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -interface IPoolsMsanager { +interface IPoolsManager { //OWNER METHODS function createPool(address token_, uint interest_, uint minStakeValue) external returns (address); function deactivatePool(address pool_) external; diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index 158009e0..f1050008 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -10,7 +10,8 @@ contract PoolsManager is Ownable, IPoolsManager { RewardsBank public bank; - mapping(address => address) public pools; //maps the address of the token to the address of the pool + mapping(address => address) public tokenToPool; //maps the address of the token to the address of the pool + mapping(address => address) public poolToToken; //maps the address of the pool to the address of the token constructor(RewardsBank bank_) Ownable() { bank = bank_; @@ -20,41 +21,40 @@ contract PoolsManager is Ownable, IPoolsManager { function createPool(address token_, uint interest_, uint minStakeValue) public onlyOwner() returns (address) { TokenPool pool = new TokenPool(token_, bank, interest_, minStakeValue); - pools[address(pool)] = address(pool); + tokenToPool[token_] = address(pool); + poolToToken[address(pool)] = token_; emit PoolCreated(address(pool), token_, interest_, minStakeValue); - pool.activate(); - emit PoolActivated(address(pool)); return address(pool); } function deactivatePool(address pool_) public onlyOwner() { - require(pools[pool_] != address(0), "Pool does not exist"); + require(poolToToken[pool_] != address(0), "Pool does not exist"); TokenPool pool = TokenPool(pool_); pool.deactivate(); emit PoolDeactivated(pool_); } function activatePool(address pool_) public onlyOwner() { - require(pools[pool_] != address(0), "Pool does not exist"); + require(poolToToken[pool_] != address(0), "Pool does not exist"); TokenPool pool = TokenPool(pool_); pool.activate(); emit PoolActivated(pool_); } function setInterest(address pool_, uint interest_) public onlyOwner() { - require(pools[pool_] != address(0), "Pool does not exist"); + require(poolToToken[pool_] != address(0), "Pool does not exist"); TokenPool pool = TokenPool(pool_); pool.setInterest(interest_); } function setMinStakeValue(address pool_, uint minStakeValue_) public onlyOwner() { - require(pools[pool_] != address(0), "Pool does not exist"); + require(poolToToken[pool_] != address(0), "Pool does not exist"); TokenPool pool = TokenPool(pool_); pool.setMinStakeValue(minStakeValue_); } function grantBackendRole(address pool_, address backend_) public onlyOwner() { - require(pools[pool_] != address(0), "Pool does not exist"); + require(poolToToken[pool_] != address(0), "Pool does not exist"); TokenPool pool = TokenPool(pool_); pool.grantRole(pool.BACKEND_ROLE(), backend_); } @@ -62,7 +62,7 @@ contract PoolsManager is Ownable, IPoolsManager { // VIEW METHODS function getPool(address token_) public view returns (address) { - return pools[token_]; + return tokenToPool[token_]; } function getPoolInfo(address pool_) public view returns (address token, uint interest, uint minStakeValue, uint totalStake, uint totalShare,bool) { diff --git a/test/staking/token/PoolsManager.ts b/test/staking/token/PoolsManager.ts new file mode 100644 index 00000000..f3fdca09 --- /dev/null +++ b/test/staking/token/PoolsManager.ts @@ -0,0 +1,87 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { ethers, upgrades } from "hardhat"; + +import { + PoolsManager, + RewardsBank, + AirBond__factory, + RewardsBank__factory, + PoolsManager__factory, +} from "../../../typechain-types"; + +import { expect } from "chai"; + +describe("PoolsManager", function () { + let poolsManager: PoolsManager; + let rewardsBank: RewardsBank; + let tokenAddr: string; + + async function deploy() { + const [owner] = await ethers.getSigners(); + + const rewardsBank = await new RewardsBank__factory(owner).deploy(); + const airBond = await new AirBond__factory(owner).deploy(owner.address); + + const poolsManager = await new PoolsManager__factory(owner).deploy(rewardsBank.address); + + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); + const tokenAddr = airBond.address; + + return { poolsManager, rewardsBank, tokenAddr }; + } + + beforeEach(async function () { + ({ poolsManager, rewardsBank, tokenAddr } = await loadFixture(deploy)); + }); + + describe("Pool Management", function () { + it("Should allow the owner to create a pool", async function () { + const interest = 100000; // 10% + const minStakeValue = 10; + + const tx = await poolsManager.createPool(tokenAddr, interest, minStakeValue); + const receipt = await tx.wait(); + const poolAddress = receipt.events![2].args!.pool; + + expect(await poolsManager.getPool(tokenAddr)).to.equal(poolAddress); + }); + + it("Should activate and deactivate a pool", async function () { + const interest = 100000; // 10% + const minStakeValue = 10; + + const tx = await poolsManager.createPool(tokenAddr, interest, minStakeValue); + const receipt = await tx.wait(); + console.log(receipt.events); + const poolAddress = receipt.events![2].args!.pool; + + await poolsManager.deactivatePool(poolAddress); + expect(await poolsManager.getPoolInfo(poolAddress)).to.include(false); // Pool should be inactive + + await poolsManager.activatePool(poolAddress); + expect(await poolsManager.getPoolInfo(poolAddress)).to.include(true); // Pool should be active + }); + + it("Should allow the owner to set interest and min stake value", async function () { + const interest = 100000; // 10% + const minStakeValue = 10; + + const tx = await poolsManager.createPool(tokenAddr, interest, minStakeValue); + const receipt = await tx.wait(); + const poolAddress = receipt.events![2].args![0]; + + const newInterest = 300000; // 30% + await poolsManager.setInterest(poolAddress, newInterest); + const [,interestSet,,,] = await poolsManager.getPoolInfo(poolAddress); + expect(interestSet).to.equal(newInterest); + + const newMinStakeValue = 20; + await poolsManager.setMinStakeValue(poolAddress, newMinStakeValue); + const [,,minStakeValueSet,,,] = await poolsManager.getPoolInfo(poolAddress); + expect(minStakeValueSet).to.equal(newMinStakeValue); + }); + + }); + +}); + diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts new file mode 100644 index 00000000..e69de29b From 13a653e5e2dcdfb6b5f3e3f8a7ae812b6c870813 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 26 Jun 2024 18:01:45 +0300 Subject: [PATCH 05/60] Fix permission --- contracts/staking/token/PoolsManager.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index f1050008..75b2bec4 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -23,6 +23,7 @@ contract PoolsManager is Ownable, IPoolsManager { TokenPool pool = new TokenPool(token_, bank, interest_, minStakeValue); tokenToPool[token_] = address(pool); poolToToken[address(pool)] = token_; + bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); emit PoolCreated(address(pool), token_, interest_, minStakeValue); return address(pool); } From 05b65372631ed281e1b41db45f94242dab7f2024 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 27 Jun 2024 12:35:41 +0300 Subject: [PATCH 06/60] Add tests for tokenPool --- contracts/staking/token/TokenPool.sol | 5 ++ test/staking/token/TokenPool.ts | 116 ++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index c8e09f3e..c6f34202 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -88,6 +88,7 @@ contract TokenPool is AccessControl, ITokenPool { require(active, "Pool is not active"); uint amount = totalStake * interest / MILLION; bank.withdrawErc20(address(token), address(this), amount); + totalStake += amount; emit StakeChanged(address(this), amount); } @@ -97,6 +98,10 @@ contract TokenPool is AccessControl, ITokenPool { return _calculateToken(share.balanceOf(user)); } + function getShare(address user) public view returns (uint) { + return share.balanceOf(user); + } + function getSharePrice() public view returns (uint) { uint _totalShare = share.totalSupply(); if (_totalShare == 0) return 1 ether; diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index e69de29b..5af77afa 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -0,0 +1,116 @@ +import { ethers } from "hardhat"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import { + RewardsBank, + AirBond, + TokenPool, + RewardsBank__factory, + AirBond__factory, + TokenPool__factory, +} from "../../../typechain-types"; + +import { expect } from "chai"; +describe("TokenPool", function () { + let owner: SignerWithAddress; + let tokenPool: TokenPool; + let rewardsBank: RewardsBank; + let token: AirBond; + + async function deploy() { + const [owner] = await ethers.getSigners(); + const rewardsBank = await new RewardsBank__factory(owner).deploy(); + const token = await new AirBond__factory(owner).deploy(owner.address); + + const interest = 100000; // 10% + const minStakeValue = 10; + const tokenPool = await new TokenPool__factory(owner).deploy(token.address, rewardsBank.address, interest, minStakeValue); + + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), tokenPool.address)).wait(); + await (await token.grantRole(await token.MINTER_ROLE(), owner.address)).wait(); + + await token.mint(owner.address, 100000000000); + + return { owner, tokenPool, rewardsBank, token }; + } + + beforeEach(async function () { + ({ owner, tokenPool, rewardsBank, token } = await loadFixture(deploy)); + }); + + describe("Owner Methods", function () { + it("Should activate and deactivate the pool", async function () { + await tokenPool.deactivate(); + expect(await tokenPool.active()).to.equal(false); + + await tokenPool.activate(); + expect(await tokenPool.active()).to.equal(true); + }); + + it("Should set interest and min stake value", async function () { + const newInterest = 300000; // 30% + await tokenPool.setInterest(newInterest); + expect(await tokenPool.interest()).to.equal(newInterest); + + const newMinStakeValue = 20; + await tokenPool.setMinStakeValue(newMinStakeValue); + expect(await tokenPool.minStakeValue()).to.equal(newMinStakeValue); + }); + }); + + describe("Staking and Unstaking", function () { + beforeEach(async function () { + await token.approve(tokenPool.address, 1000000); + }); + + it("Should allow staking", async function () { + const stake = 1000; + await tokenPool.stake(stake); + expect(await tokenPool.totalStake()).to.equal(stake); + expect(await tokenPool.getStake(owner.address)).to.equal(stake); + }); + + it("Should not allow staking below minimum stake value", async function () { + await expect(tokenPool.stake(1)).to.be.revertedWith("Amount is less than minStakeValue"); + }); + + it("Should allow unstaking", async function () { + const stake = 1000; + await tokenPool.stake(stake); + const shares = tokenPool.getShare(owner.address); + await tokenPool.unstake(shares); + expect(await tokenPool.totalStake()).to.equal(0); + expect(await tokenPool.getStake(owner.address)).to.equal(0); + }); + + it("Should not allow unstaking more than staked", async function () { + const stake = 1000; + await tokenPool.stake(stake); + const shares = await tokenPool.getShare(owner.address); + await expect(tokenPool.unstake(shares.mul(2))).to.be.revertedWith("Not enough stake"); + }); + }); + + describe("Backend Methods", function () { + beforeEach(async function () { + await tokenPool.grantRole(await tokenPool.BACKEND_ROLE(), owner.address); + await tokenPool.setInterest(100000); // 10% + await token.transfer(rewardsBank.address, 10000000); + await token.approve(tokenPool.address, 100000000); + await tokenPool.stake(100000); + }); + + it("Should allow increasing stake", async function () { + await tokenPool.increaseStake(); + const expectedStake = 110000; + expect(await tokenPool.totalStake()).to.equal(expectedStake); + }); + + it("Should not allow increasing stake if not active", async function () { + await tokenPool.deactivate(); + await expect(tokenPool.connect(owner).increaseStake()).to.be.revertedWith("Pool is not active"); + }); + }); +}); + From 69d33a425430080f5d1ffce91f76c46a7a5e6ba9 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 27 Jun 2024 16:29:22 +0300 Subject: [PATCH 07/60] Add interest rate param --- contracts/staking/token/IPoolsManager.sol | 6 ++++-- contracts/staking/token/ITokenPool.sol | 1 + contracts/staking/token/PoolsManager.sol | 14 ++++++++++---- contracts/staking/token/TokenPool.sol | 17 ++++++++++++++++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/contracts/staking/token/IPoolsManager.sol b/contracts/staking/token/IPoolsManager.sol index f775cd20..3f9196af 100644 --- a/contracts/staking/token/IPoolsManager.sol +++ b/contracts/staking/token/IPoolsManager.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.0; interface IPoolsManager { //OWNER METHODS - function createPool(address token_, uint interest_, uint minStakeValue) external returns (address); + function createPool(address token_, uint interest_, uint interestRate_, uint minStakeValue) external returns (address); function deactivatePool(address pool_) external; function activatePool(address pool_) external; function setInterest(address pool_, uint interest_) external; function setMinStakeValue(address pool_, uint minStakeValue_) external; + function setInterestRate(address pool_, uint interestRate_) external; //VIEW METHODS function getPool(address token_) external view returns (address); @@ -15,10 +16,11 @@ interface IPoolsManager { //EVENTS - event PoolCreated(address pool, address token, uint interest, uint minStakeValue); + event PoolCreated(address pool, address token, uint interest, uint interestRate, uint minStakeValue); event PoolDeactivated(address pool); event PoolActivated(address pool); event InterestChanged(address pool, uint interest); + event InterestrateChanged(address pool, uint interestRate); event MinStakeValueChanged(address pool, uint minStakeValue); diff --git a/contracts/staking/token/ITokenPool.sol b/contracts/staking/token/ITokenPool.sol index 4dcb9a74..cb725018 100644 --- a/contracts/staking/token/ITokenPool.sol +++ b/contracts/staking/token/ITokenPool.sol @@ -23,6 +23,7 @@ interface ITokenPool { //EVENTS event StakeChanged(address indexed user, uint amount); event InterestChanged(uint interest); + event InterestRateChanged(uint interestRate); event MinStakeValueChanged(uint minStakeValue); event Deactivated(); event Activated(); diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index 75b2bec4..f800497f 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -19,12 +19,12 @@ contract PoolsManager is Ownable, IPoolsManager { // OWNER METHODS - function createPool(address token_, uint interest_, uint minStakeValue) public onlyOwner() returns (address) { - TokenPool pool = new TokenPool(token_, bank, interest_, minStakeValue); + function createPool(address token_, uint interest_, uint interestRate_, uint minStakeValue) public onlyOwner() returns (address) { + TokenPool pool = new TokenPool(token_, bank, interest_, interestRate_, minStakeValue); tokenToPool[token_] = address(pool); poolToToken[address(pool)] = token_; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit PoolCreated(address(pool), token_, interest_, minStakeValue); + emit PoolCreated(address(pool), token_, interest_, interestRate_, minStakeValue); return address(pool); } @@ -54,6 +54,12 @@ contract PoolsManager is Ownable, IPoolsManager { pool.setMinStakeValue(minStakeValue_); } + function setInterestRate(address pool_, uint interestRate_) public onlyOwner() { + require(poolToToken[pool_] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pool_); + pool.setInterestRate(interestRate_); + } + function grantBackendRole(address pool_, address backend_) public onlyOwner() { require(poolToToken[pool_] != address(0), "Pool does not exist"); TokenPool pool = TokenPool(pool_); @@ -66,7 +72,7 @@ contract PoolsManager is Ownable, IPoolsManager { return tokenToPool[token_]; } - function getPoolInfo(address pool_) public view returns (address token, uint interest, uint minStakeValue, uint totalStake, uint totalShare,bool) { + function getPoolInfo(address pool_) public view returns (address token, uint interest, uint minStakeValue, uint totalStake, uint totalShare,bool active) { TokenPool pool = TokenPool(pool_); return (address(pool.token()), pool.interest(), pool.minStakeValue(), pool.totalStake(), pool.totalShare(), pool.active()); } diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index c6f34202..f224953b 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -17,15 +17,17 @@ contract TokenPool is AccessControl, ITokenPool { ShareToken public share; RewardsBank public bank; uint public interest; + uint public interestRate; //Time in seconds to how often the stake is increased uint public minStakeValue; uint public totalStake; bool public active; - constructor(address _token, RewardsBank _rewardsBank, uint _intereset, uint _minStakeValue ) { + constructor(address _token, RewardsBank _rewardsBank, uint _intereset,uint _interestRate, uint _minStakeValue ) { token = ERC20(_token); share = new ShareToken(); bank = _rewardsBank; interest = _intereset; + interestRate = _interestRate; minStakeValue = _minStakeValue; active = true; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); @@ -55,6 +57,11 @@ contract TokenPool is AccessControl, ITokenPool { emit MinStakeValueChanged(_minStakeValue); } + function setInterestRate(uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { + interestRate = _interestRate; + emit InterestRateChanged(_interestRate); + } + // PUBLIC METHODS function stake(uint amount) public { @@ -110,6 +117,14 @@ contract TokenPool is AccessControl, ITokenPool { return _totalShare * decimals / totalStake; } + function getInterest() public view returns (uint) { + return interest; + } + + function getInterestRate() public view returns (uint) { + return interestRate; + } + function totalShare() public view returns (uint) { return share.totalSupply(); } From cdbb43b7d85e3f2b1553762698f90b1cce394cb0 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 3 Jul 2024 12:08:35 +0300 Subject: [PATCH 08/60] Update stake increase --- contracts/staking/token/IPoolsManager.sol | 1 - contracts/staking/token/ITokenPool.sol | 3 --- contracts/staking/token/PoolsManager.sol | 6 ------ contracts/staking/token/TokenPool.sol | 26 +++++++++++++++-------- test/staking/token/TokenPool.ts | 24 ++------------------- 5 files changed, 19 insertions(+), 41 deletions(-) diff --git a/contracts/staking/token/IPoolsManager.sol b/contracts/staking/token/IPoolsManager.sol index 3f9196af..bc22087c 100644 --- a/contracts/staking/token/IPoolsManager.sol +++ b/contracts/staking/token/IPoolsManager.sol @@ -23,5 +23,4 @@ interface IPoolsManager { event InterestrateChanged(address pool, uint interestRate); event MinStakeValueChanged(address pool, uint minStakeValue); - } diff --git a/contracts/staking/token/ITokenPool.sol b/contracts/staking/token/ITokenPool.sol index cb725018..0d64ae39 100644 --- a/contracts/staking/token/ITokenPool.sol +++ b/contracts/staking/token/ITokenPool.sol @@ -8,9 +8,6 @@ interface ITokenPool { function setInterest(uint _interest) external; function setMinStakeValue(uint _minStakeValue) external; - //BACKEND METHODS - function increaseStake() external; - //PUBLIC METHODS function stake(uint amount) external; function unstake(uint amount) external; diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index f800497f..ee9892e5 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -60,12 +60,6 @@ contract PoolsManager is Ownable, IPoolsManager { pool.setInterestRate(interestRate_); } - function grantBackendRole(address pool_, address backend_) public onlyOwner() { - require(poolToToken[pool_] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pool_); - pool.grantRole(pool.BACKEND_ROLE(), backend_); - } - // VIEW METHODS function getPool(address token_) public view returns (address) { diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index f224953b..4822168a 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -9,13 +9,12 @@ import "./ITokenPool.sol"; import "../../funds/RewardsBank.sol"; contract TokenPool is AccessControl, ITokenPool { - bytes32 constant public BACKEND_ROLE = keccak256("BACKEND_ROLE"); - uint constant public MILLION = 1_000_000; ERC20 public token; ShareToken public share; RewardsBank public bank; + uint public lastStakeIncrease; uint public interest; uint public interestRate; //Time in seconds to how often the stake is increased uint public minStakeValue; @@ -29,6 +28,7 @@ contract TokenPool is AccessControl, ITokenPool { interest = _intereset; interestRate = _interestRate; minStakeValue = _minStakeValue; + lastStakeIncrease = block.timestamp; active = true; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -89,14 +89,12 @@ contract TokenPool is AccessControl, ITokenPool { emit StakeChanged(msg.sender, tokenAmount); } - // BACKEND METHODS - - function increaseStake() public onlyRole(BACKEND_ROLE) { + function onBlock() external { require(active, "Pool is not active"); - uint amount = totalStake * interest / MILLION; - bank.withdrawErc20(address(token), address(this), amount); - totalStake += amount; - emit StakeChanged(address(this), amount); + if (block.timestamp > lastStakeIncrease + interestRate) { + _increaseStake(); + lastStakeIncrease = block.timestamp; + } } // VIEW METHODS @@ -145,4 +143,14 @@ contract TokenPool is AccessControl, ITokenPool { return shareAmount * decimals / sharePrice; } + function _increaseStake() internal { + require(active, "Pool is not active"); + uint amount = totalStake * interest / MILLION; + bank.withdrawErc20(address(token), address(this), amount); + totalStake += amount; + emit StakeChanged(address(this), amount); + } + + + } diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index 5af77afa..b4e99119 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -24,8 +24,9 @@ describe("TokenPool", function () { const token = await new AirBond__factory(owner).deploy(owner.address); const interest = 100000; // 10% + const interestRate = 24 * 60 * 60; // 1 day const minStakeValue = 10; - const tokenPool = await new TokenPool__factory(owner).deploy(token.address, rewardsBank.address, interest, minStakeValue); + const tokenPool = await new TokenPool__factory(owner).deploy(token.address, rewardsBank.address, interest, interestRate, minStakeValue); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), tokenPool.address)).wait(); await (await token.grantRole(await token.MINTER_ROLE(), owner.address)).wait(); @@ -91,26 +92,5 @@ describe("TokenPool", function () { await expect(tokenPool.unstake(shares.mul(2))).to.be.revertedWith("Not enough stake"); }); }); - - describe("Backend Methods", function () { - beforeEach(async function () { - await tokenPool.grantRole(await tokenPool.BACKEND_ROLE(), owner.address); - await tokenPool.setInterest(100000); // 10% - await token.transfer(rewardsBank.address, 10000000); - await token.approve(tokenPool.address, 100000000); - await tokenPool.stake(100000); - }); - - it("Should allow increasing stake", async function () { - await tokenPool.increaseStake(); - const expectedStake = 110000; - expect(await tokenPool.totalStake()).to.equal(expectedStake); - }); - - it("Should not allow increasing stake if not active", async function () { - await tokenPool.deactivate(); - await expect(tokenPool.connect(owner).increaseStake()).to.be.revertedWith("Pool is not active"); - }); - }); }); From d88600e5f324806134c3cc528bb7411ed7bbeb0e Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 3 Jul 2024 14:25:23 +0300 Subject: [PATCH 09/60] Fix tests --- test/staking/token/PoolsManager.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/staking/token/PoolsManager.ts b/test/staking/token/PoolsManager.ts index f3fdca09..d4bec21b 100644 --- a/test/staking/token/PoolsManager.ts +++ b/test/staking/token/PoolsManager.ts @@ -37,23 +37,24 @@ describe("PoolsManager", function () { describe("Pool Management", function () { it("Should allow the owner to create a pool", async function () { const interest = 100000; // 10% + const interestRate = 24 * 60 * 60; // 24 hours const minStakeValue = 10; - const tx = await poolsManager.createPool(tokenAddr, interest, minStakeValue); + const tx = await poolsManager.createPool(tokenAddr, interest,interestRate, minStakeValue); const receipt = await tx.wait(); - const poolAddress = receipt.events![2].args!.pool; + const poolAddress = receipt.events![3].args!.pool; expect(await poolsManager.getPool(tokenAddr)).to.equal(poolAddress); }); it("Should activate and deactivate a pool", async function () { const interest = 100000; // 10% + const interestRate = 24 * 60 * 60; // 24 hours const minStakeValue = 10; - const tx = await poolsManager.createPool(tokenAddr, interest, minStakeValue); + const tx = await poolsManager.createPool(tokenAddr, interest, interestRate, minStakeValue); const receipt = await tx.wait(); - console.log(receipt.events); - const poolAddress = receipt.events![2].args!.pool; + const poolAddress = receipt.events![3].args!.pool; await poolsManager.deactivatePool(poolAddress); expect(await poolsManager.getPoolInfo(poolAddress)).to.include(false); // Pool should be inactive @@ -64,11 +65,12 @@ describe("PoolsManager", function () { it("Should allow the owner to set interest and min stake value", async function () { const interest = 100000; // 10% + const interestRate = 24 * 60 * 60; // 24 hours const minStakeValue = 10; - const tx = await poolsManager.createPool(tokenAddr, interest, minStakeValue); + const tx = await poolsManager.createPool(tokenAddr, interest, interestRate, minStakeValue); const receipt = await tx.wait(); - const poolAddress = receipt.events![2].args![0]; + const poolAddress = receipt.events![3].args![0]; const newInterest = 300000; // 30% await poolsManager.setInterest(poolAddress, newInterest); From dbefe051477c0c6cbbf763845f20531d8d394322 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 10 Jul 2024 19:15:36 +0300 Subject: [PATCH 10/60] Complex update - Add possibility to unstake even if pool is inactive - Deploy pools using proxies - Add rewards in other tokens - Update rewards calculation --- contracts/staking/token/IPoolsManager.sol | 31 +++++---- contracts/staking/token/PoolsManager.sol | 80 +++++++++++++---------- contracts/staking/token/ShareToken.sol | 19 ------ contracts/staking/token/TokenPool.sol | 79 ++++++++++++---------- 4 files changed, 108 insertions(+), 101 deletions(-) delete mode 100644 contracts/staking/token/ShareToken.sol diff --git a/contracts/staking/token/IPoolsManager.sol b/contracts/staking/token/IPoolsManager.sol index bc22087c..9308598e 100644 --- a/contracts/staking/token/IPoolsManager.sol +++ b/contracts/staking/token/IPoolsManager.sol @@ -2,25 +2,28 @@ pragma solidity ^0.8.0; interface IPoolsManager { + //OWNER METHODS - function createPool(address token_, uint interest_, uint interestRate_, uint minStakeValue) external returns (address); - function deactivatePool(address pool_) external; - function activatePool(address pool_) external; - function setInterest(address pool_, uint interest_) external; - function setMinStakeValue(address pool_, uint minStakeValue_) external; - function setInterestRate(address pool_, uint interestRate_) external; + function createPool( + string memory name, address _token, uint _interest, + uint _interestRate, uint _minStakeValue, address _rewardToken, uint _rewardTokenPrice + ) external returns (address); + function deactivatePool(string memory _pool) external; + function activatePool(string memory _pool) external; + function setInterest(string memory _pool, uint _interest) external; + function setMinStakeValue(string memory _pool, uint _minStakeValue) external; + function setInterestRate(string memory _pool, uint _interestRate) external; //VIEW METHODS - function getPool(address token_) external view returns (address); - function getPoolInfo(address pool_) external view returns (address token, uint interest, uint minStakeValue, uint totalStake, uint totalShare, bool active); + function getPoolAddress(string memory _pool) external view returns (address); //EVENTS - event PoolCreated(address pool, address token, uint interest, uint interestRate, uint minStakeValue); - event PoolDeactivated(address pool); - event PoolActivated(address pool); - event InterestChanged(address pool, uint interest); - event InterestrateChanged(address pool, uint interestRate); - event MinStakeValueChanged(address pool, uint minStakeValue); + event PoolCreated(string indexed name, address indexed token); + event PoolDeactivated(string indexed name); + event PoolActivated(string indexed name); + event InterestChanged(string indexed name, uint interest); + event InterestRateChanged(string indexed name, uint interestRate); + event MinStakeValueChanged(string indexed name, uint minStakeValue); } diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index ee9892e5..a634f37d 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -1,73 +1,83 @@ -// SPDX-License-Identifier: UNLICENSED +//_ SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./TokenPool.sol"; import "./IPoolsManager.sol"; import "../../funds/RewardsBank.sol"; -contract PoolsManager is Ownable, IPoolsManager { +contract PoolsManager is UUPSUpgradeable, AccessControlUpgradeable, IPoolsManager { RewardsBank public bank; + UpgradeableBeacon public beacon; - mapping(address => address) public tokenToPool; //maps the address of the token to the address of the pool - mapping(address => address) public poolToToken; //maps the address of the pool to the address of the token + mapping(string => address) public pools; - constructor(RewardsBank bank_) Ownable() { + function initialize(RewardsBank bank_, UpgradeableBeacon beacon_) public initializer { bank = bank_; + beacon = beacon_; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } // OWNER METHODS - function createPool(address token_, uint interest_, uint interestRate_, uint minStakeValue) public onlyOwner() returns (address) { - TokenPool pool = new TokenPool(token_, bank, interest_, interestRate_, minStakeValue); - tokenToPool[token_] = address(pool); - poolToToken[address(pool)] = token_; + function createPool( + string memory name, address token_, uint interest_, uint interestRate_, + uint minStakeValue_, address rewardToken_, uint rewardTokenPrice_ + ) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + bytes memory data = abi.encodeWithSignature( + "initialize(string,address,address,uint,uint,uint,address,uint", + token_, interest_, interestRate_, minStakeValue_, rewardToken_, rewardTokenPrice_); + address pool = address(new BeaconProxy(address(beacon), data)); + pools[name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit PoolCreated(address(pool), token_, interest_, interestRate_, minStakeValue); + emit PoolCreated(name, token_); return address(pool); } - function deactivatePool(address pool_) public onlyOwner() { - require(poolToToken[pool_] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pool_); + function deactivatePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); pool.deactivate(); - emit PoolDeactivated(pool_); + emit PoolDeactivated(_pool); } - function activatePool(address pool_) public onlyOwner() { - require(poolToToken[pool_] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pool_); + function activatePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); pool.activate(); - emit PoolActivated(pool_); + emit PoolActivated(_pool); } - function setInterest(address pool_, uint interest_) public onlyOwner() { - require(poolToToken[pool_] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pool_); + function setInterest(string memory pool_, uint interest_) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[pool_] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[pool_]); pool.setInterest(interest_); } - function setMinStakeValue(address pool_, uint minStakeValue_) public onlyOwner() { - require(poolToToken[pool_] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pool_); - pool.setMinStakeValue(minStakeValue_); + function setMinStakeValue(string memory _pool, uint _minStakeValue) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); + pool.setMinStakeValue(_minStakeValue); } - function setInterestRate(address pool_, uint interestRate_) public onlyOwner() { - require(poolToToken[pool_] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pool_); + function setInterestRate(string memory _pool, uint interestRate_) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); pool.setInterestRate(interestRate_); } + // INTERNAL METHODS + + function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} + // VIEW METHODS - function getPool(address token_) public view returns (address) { - return tokenToPool[token_]; + function getPoolAddress(string memory name) public view returns (address) { + return pools[name]; } - function getPoolInfo(address pool_) public view returns (address token, uint interest, uint minStakeValue, uint totalStake, uint totalShare,bool active) { - TokenPool pool = TokenPool(pool_); - return (address(pool.token()), pool.interest(), pool.minStakeValue(), pool.totalStake(), pool.totalShare(), pool.active()); - } } diff --git a/contracts/staking/token/ShareToken.sol b/contracts/staking/token/ShareToken.sol deleted file mode 100644 index 1fe770e0..00000000 --- a/contracts/staking/token/ShareToken.sol +++ /dev/null @@ -1,19 +0,0 @@ -//SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract ShareToken is ERC20, Ownable { - - constructor() ERC20("PoolToken", "PT") Ownable() { - } - - function burn(address account, uint amount) public onlyOwner() { - _burn(account, amount); - } - - function mint(address to, uint amount) public onlyOwner() { - _mint(to, amount); - } -} diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 4822168a..9cf22f43 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -1,35 +1,49 @@ //SPDX-License-Identifier: UNCLICENSED pragma solidity ^0.8.0; -import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "./ShareToken.sol"; import "./ITokenPool.sol"; import "../../funds/RewardsBank.sol"; -contract TokenPool is AccessControl, ITokenPool { +contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { uint constant public MILLION = 1_000_000; ERC20 public token; - ShareToken public share; RewardsBank public bank; - uint public lastStakeIncrease; - uint public interest; - uint public interestRate; //Time in seconds to how often the stake is increased + + string public name; uint public minStakeValue; uint public totalStake; + uint public totalShare; bool public active; + address public rewardToken; + uint public rewardTokenPrice; // The coefficient to calculate the reward token amount - constructor(address _token, RewardsBank _rewardsBank, uint _intereset,uint _interestRate, uint _minStakeValue ) { - token = ERC20(_token); - share = new ShareToken(); - bank = _rewardsBank; - interest = _intereset; - interestRate = _interestRate; - minStakeValue = _minStakeValue; - lastStakeIncrease = block.timestamp; + uint public interest; + uint public interestRate; //Time in seconds to how often the stake is increased + uint public lastStakeIncrease; + + mapping(address => uint) public stakes; + mapping(address => uint) public shares; + + function initialize( + string memory name_, address token_, RewardsBank rewardsBank_, uint intereset_, + uint interestRate_, uint minStakeValue_, address rewardToken_, uint rewardTokenPrice_ + ) public initializer { + token = ERC20(token_); + bank = rewardsBank_; + + name = name_; + minStakeValue = minStakeValue_; active = true; + rewardToken = rewardToken_; + rewardTokenPrice = rewardTokenPrice_; + + interest = intereset_; + interestRate = interestRate_; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -71,21 +85,25 @@ contract TokenPool is AccessControl, ITokenPool { uint shareAmount = _calculateShare(amount); - share.mint(msg.sender, shareAmount); + shares[msg.sender] += shareAmount; totalStake += amount; emit StakeChanged(msg.sender, amount); } function unstake(uint amount) public { - require(active, "Pool is not active"); - require(share.balanceOf(msg.sender) >= amount, "Not enough stake"); + require(shares[msg.sender] >= amount, "Not enough stake"); uint tokenAmount = _calculateToken(amount); - require(tokenAmount <= totalStake, "Amount is more than total stake"); + uint rewardAmount = tokenAmount - stakes[msg.sender]; - share.burn(msg.sender, amount); + shares[msg.sender] -= amount; totalStake -= tokenAmount; - require(token.transfer(msg.sender, tokenAmount), "Transfer failed"); + + uint rewardToPay = rewardAmount * rewardTokenPrice; + + bank.withdrawErc20(rewardToken, msg.sender, rewardToPay); + + require(token.transfer(msg.sender, stakes[msg.sender]), "Transfer failed"); emit StakeChanged(msg.sender, tokenAmount); } @@ -100,19 +118,18 @@ contract TokenPool is AccessControl, ITokenPool { // VIEW METHODS function getStake(address user) public view returns (uint) { - return _calculateToken(share.balanceOf(user)); + return _calculateToken(shares[user]); } function getShare(address user) public view returns (uint) { - return share.balanceOf(user); + return shares[user]; } function getSharePrice() public view returns (uint) { - uint _totalShare = share.totalSupply(); - if (_totalShare == 0) return 1 ether; + if (totalShare == 0) return 1 ether; uint decimals = token.decimals(); - return _totalShare * decimals / totalStake; + return totalShare * decimals / totalStake; } function getInterest() public view returns (uint) { @@ -123,10 +140,6 @@ contract TokenPool is AccessControl, ITokenPool { return interestRate; } - function totalShare() public view returns (uint) { - return share.totalSupply(); - } - // INTERNAL METHODS function _calculateShare(uint tokenAmount) private view returns (uint) { @@ -144,13 +157,13 @@ contract TokenPool is AccessControl, ITokenPool { } function _increaseStake() internal { - require(active, "Pool is not active"); + // Function call onBlock() must not be reverted + if (!active) return; uint amount = totalStake * interest / MILLION; bank.withdrawErc20(address(token), address(this), amount); totalStake += amount; emit StakeChanged(address(this), amount); } - - + function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} } From 49d236fb69c198978e8d469d37191a6efad942c2 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 11 Jul 2024 11:38:40 +0300 Subject: [PATCH 11/60] Fixes --- contracts/staking/token/TokenPool.sol | 11 +++++++---- test/staking/token/TokenPool.ts | 12 +++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 9cf22f43..e9bb2475 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -86,17 +86,21 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { uint shareAmount = _calculateShare(amount); shares[msg.sender] += shareAmount; + stakes[msg.sender] += amount; totalStake += amount; emit StakeChanged(msg.sender, amount); } function unstake(uint amount) public { - require(shares[msg.sender] >= amount, "Not enough stake"); + require(shares[msg.sender] >= amount, "Not enough share"); uint tokenAmount = _calculateToken(amount); + require(stakes[msg.sender] >= tokenAmount, "Not enough stake"); + uint rewardAmount = tokenAmount - stakes[msg.sender]; shares[msg.sender] -= amount; + stakes[msg.sender] -= tokenAmount; totalStake -= tokenAmount; uint rewardToPay = rewardAmount * rewardTokenPrice; @@ -118,7 +122,7 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { // VIEW METHODS function getStake(address user) public view returns (uint) { - return _calculateToken(shares[user]); + return stakes[user]; } function getShare(address user) public view returns (uint) { @@ -151,9 +155,8 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { function _calculateToken(uint shareAmount) private view returns (uint) { uint sharePrice = getSharePrice(); - uint decimals = token.decimals(); - return shareAmount * decimals / sharePrice; + return shareAmount * 1 ether / sharePrice; } function _increaseStake() internal { diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index b4e99119..e9ba56ff 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -1,4 +1,4 @@ -import { ethers } from "hardhat"; +import { ethers, upgrades } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; @@ -22,11 +22,17 @@ describe("TokenPool", function () { const [owner] = await ethers.getSigners(); const rewardsBank = await new RewardsBank__factory(owner).deploy(); const token = await new AirBond__factory(owner).deploy(owner.address); + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); + const name = "Test"; const interest = 100000; // 10% const interestRate = 24 * 60 * 60; // 1 day const minStakeValue = 10; - const tokenPool = await new TokenPool__factory(owner).deploy(token.address, rewardsBank.address, interest, interestRate, minStakeValue); + const rewardTokenPrice = 2; + const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [ + name, token.address, rewardsBank.address, interest, + interestRate, minStakeValue, token.address, rewardTokenPrice + ])) as TokenPool; await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), tokenPool.address)).wait(); await (await token.grantRole(await token.MINTER_ROLE(), owner.address)).wait(); @@ -89,7 +95,7 @@ describe("TokenPool", function () { const stake = 1000; await tokenPool.stake(stake); const shares = await tokenPool.getShare(owner.address); - await expect(tokenPool.unstake(shares.mul(2))).to.be.revertedWith("Not enough stake"); + await expect(tokenPool.unstake(shares.mul(2))).to.be.revertedWith("Not enough share"); }); }); }); From 23727f01174551e47b1ea57d84b4ba6b800c28d4 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 11 Jul 2024 20:44:31 +0300 Subject: [PATCH 12/60] Fix tokenPool math --- contracts/staking/token/TokenPool.sol | 17 +++++++---------- test/staking/token/TokenPool.ts | 3 ++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index e9bb2475..c33f412e 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -10,6 +10,7 @@ import "../../funds/RewardsBank.sol"; contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { uint constant public MILLION = 1_000_000; + uint constant public FIXED_POINT = 1 ether; ERC20 public token; RewardsBank public bank; @@ -95,18 +96,16 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { require(shares[msg.sender] >= amount, "Not enough share"); uint tokenAmount = _calculateToken(amount); - require(stakes[msg.sender] >= tokenAmount, "Not enough stake"); - uint rewardAmount = tokenAmount - stakes[msg.sender]; + if (rewardAmount < 0) rewardAmount = 0; shares[msg.sender] -= amount; - stakes[msg.sender] -= tokenAmount; - totalStake -= tokenAmount; + totalStake -= stakes[msg.sender]; + stakes[msg.sender] = 0; uint rewardToPay = rewardAmount * rewardTokenPrice; bank.withdrawErc20(rewardToken, msg.sender, rewardToPay); - require(token.transfer(msg.sender, stakes[msg.sender]), "Transfer failed"); emit StakeChanged(msg.sender, tokenAmount); } @@ -131,9 +130,8 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { function getSharePrice() public view returns (uint) { if (totalShare == 0) return 1 ether; - uint decimals = token.decimals(); - return totalShare * decimals / totalStake; + return totalStake * FIXED_POINT / totalShare; } function getInterest() public view returns (uint) { @@ -148,15 +146,14 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { function _calculateShare(uint tokenAmount) private view returns (uint) { uint sharePrice = getSharePrice(); - uint decimals = token.decimals(); - return tokenAmount * sharePrice / decimals; + return tokenAmount * FIXED_POINT / sharePrice ; } function _calculateToken(uint shareAmount) private view returns (uint) { uint sharePrice = getSharePrice(); - return shareAmount * 1 ether / sharePrice; + return shareAmount * sharePrice / FIXED_POINT; } function _increaseStake() internal { diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index e9ba56ff..509c41e4 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -76,6 +76,7 @@ describe("TokenPool", function () { await tokenPool.stake(stake); expect(await tokenPool.totalStake()).to.equal(stake); expect(await tokenPool.getStake(owner.address)).to.equal(stake); + expect(await tokenPool.getShare(owner.address)).to.equal(1000); }); it("Should not allow staking below minimum stake value", async function () { @@ -85,7 +86,7 @@ describe("TokenPool", function () { it("Should allow unstaking", async function () { const stake = 1000; await tokenPool.stake(stake); - const shares = tokenPool.getShare(owner.address); + const shares = await tokenPool.getShare(owner.address); await tokenPool.unstake(shares); expect(await tokenPool.totalStake()).to.equal(0); expect(await tokenPool.getStake(owner.address)).to.equal(0); From cfea619608fbb6de319be4b2ccece8ff6271524d Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 25 Jul 2024 11:43:38 +0300 Subject: [PATCH 13/60] Update tokenomics --- contracts/staking/token/IPoolsManager.sol | 7 -- contracts/staking/token/ITokenPool.sol | 9 +-- contracts/staking/token/PoolsManager.sol | 19 ----- contracts/staking/token/TokenPool.sol | 95 ++++++++--------------- package-lock.json | 2 +- scripts/staking/deploy_poolManager.ts | 2 +- test/staking/token/TokenPool.ts | 54 ++++++++----- 7 files changed, 73 insertions(+), 115 deletions(-) diff --git a/contracts/staking/token/IPoolsManager.sol b/contracts/staking/token/IPoolsManager.sol index 9308598e..9d66a652 100644 --- a/contracts/staking/token/IPoolsManager.sol +++ b/contracts/staking/token/IPoolsManager.sol @@ -10,9 +10,6 @@ interface IPoolsManager { ) external returns (address); function deactivatePool(string memory _pool) external; function activatePool(string memory _pool) external; - function setInterest(string memory _pool, uint _interest) external; - function setMinStakeValue(string memory _pool, uint _minStakeValue) external; - function setInterestRate(string memory _pool, uint _interestRate) external; //VIEW METHODS function getPoolAddress(string memory _pool) external view returns (address); @@ -22,8 +19,4 @@ interface IPoolsManager { event PoolCreated(string indexed name, address indexed token); event PoolDeactivated(string indexed name); event PoolActivated(string indexed name); - event InterestChanged(string indexed name, uint interest); - event InterestRateChanged(string indexed name, uint interestRate); - event MinStakeValueChanged(string indexed name, uint minStakeValue); - } diff --git a/contracts/staking/token/ITokenPool.sol b/contracts/staking/token/ITokenPool.sol index 0d64ae39..360c13e4 100644 --- a/contracts/staking/token/ITokenPool.sol +++ b/contracts/staking/token/ITokenPool.sol @@ -5,8 +5,6 @@ interface ITokenPool { //OWNER METHODS function activate() external; function deactivate() external; - function setInterest(uint _interest) external; - function setMinStakeValue(uint _minStakeValue) external; //PUBLIC METHODS function stake(uint amount) external; @@ -14,16 +12,11 @@ interface ITokenPool { //VIEW METHODS function getStake(address user) external view returns (uint); - function getSharePrice() external view returns (uint); - function totalShare() external view returns (uint); //EVENTS event StakeChanged(address indexed user, uint amount); - event InterestChanged(uint interest); - event InterestRateChanged(uint interestRate); event MinStakeValueChanged(uint minStakeValue); + event RewardClaimed(address indexed user, uint amount); event Deactivated(); event Activated(); - - } diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/PoolsManager.sol index a634f37d..4f317106 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/PoolsManager.sol @@ -10,7 +10,6 @@ import "./IPoolsManager.sol"; import "../../funds/RewardsBank.sol"; contract PoolsManager is UUPSUpgradeable, AccessControlUpgradeable, IPoolsManager { - RewardsBank public bank; UpgradeableBeacon public beacon; @@ -52,24 +51,6 @@ contract PoolsManager is UUPSUpgradeable, AccessControlUpgradeable, IPoolsManage emit PoolActivated(_pool); } - function setInterest(string memory pool_, uint interest_) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[pool_] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[pool_]); - pool.setInterest(interest_); - } - - function setMinStakeValue(string memory _pool, uint _minStakeValue) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.setMinStakeValue(_minStakeValue); - } - - function setInterestRate(string memory _pool, uint interestRate_) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.setInterestRate(interestRate_); - } - // INTERNAL METHODS function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index c33f412e..0df73917 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -10,7 +10,6 @@ import "../../funds/RewardsBank.sol"; contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { uint constant public MILLION = 1_000_000; - uint constant public FIXED_POINT = 1 ether; ERC20 public token; RewardsBank public bank; @@ -18,17 +17,16 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { string public name; uint public minStakeValue; uint public totalStake; - uint public totalShare; bool public active; address public rewardToken; uint public rewardTokenPrice; // The coefficient to calculate the reward token amount uint public interest; uint public interestRate; //Time in seconds to how often the stake is increased - uint public lastStakeIncrease; mapping(address => uint) public stakes; - mapping(address => uint) public shares; + mapping(address => uint) public rewards; + mapping(address => uint) private _lastChanged; function initialize( string memory name_, address token_, RewardsBank rewardsBank_, uint intereset_, @@ -62,21 +60,6 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { emit Deactivated(); } - function setInterest(uint _interest) public onlyRole(DEFAULT_ADMIN_ROLE) { - interest = _interest; - emit InterestChanged(_interest); - } - - function setMinStakeValue(uint _minStakeValue) public onlyRole(DEFAULT_ADMIN_ROLE) { - minStakeValue = _minStakeValue; - emit MinStakeValueChanged(_minStakeValue); - } - - function setInterestRate(uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { - interestRate = _interestRate; - emit InterestRateChanged(_interestRate); - } - // PUBLIC METHODS function stake(uint amount) public { @@ -84,38 +67,33 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { require(amount >= minStakeValue, "Amount is less than minStakeValue"); require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); - uint shareAmount = _calculateShare(amount); + _storeReward(msg.sender); - shares[msg.sender] += shareAmount; stakes[msg.sender] += amount; totalStake += amount; + emit StakeChanged(msg.sender, amount); } function unstake(uint amount) public { - require(shares[msg.sender] >= amount, "Not enough share"); + require(stakes[msg.sender] >= amount, "Not enough stake"); - uint tokenAmount = _calculateToken(amount); - uint rewardAmount = tokenAmount - stakes[msg.sender]; - if (rewardAmount < 0) rewardAmount = 0; + _storeReward(msg.sender); - shares[msg.sender] -= amount; - totalStake -= stakes[msg.sender]; - stakes[msg.sender] = 0; + totalStake -= amount; + stakes[msg.sender] -= amount; - uint rewardToPay = rewardAmount * rewardTokenPrice; + // If all stake is withdrawn, claim rewards + if (stakes[msg.sender] == 0 && rewards[msg.sender] > 0) { + _claim(msg.sender); + } - bank.withdrawErc20(rewardToken, msg.sender, rewardToPay); require(token.transfer(msg.sender, stakes[msg.sender]), "Transfer failed"); - emit StakeChanged(msg.sender, tokenAmount); + emit StakeChanged(msg.sender, stakes[msg.sender]); } - function onBlock() external { - require(active, "Pool is not active"); - if (block.timestamp > lastStakeIncrease + interestRate) { - _increaseStake(); - lastStakeIncrease = block.timestamp; - } + function claim() public { + _claim(msg.sender); } // VIEW METHODS @@ -124,16 +102,6 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { return stakes[user]; } - function getShare(address user) public view returns (uint) { - return shares[user]; - } - - function getSharePrice() public view returns (uint) { - if (totalShare == 0) return 1 ether; - - return totalStake * FIXED_POINT / totalShare; - } - function getInterest() public view returns (uint) { return interest; } @@ -142,27 +110,32 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { return interestRate; } + function getReward(address user) public view returns (uint) { + uint unstoredReward = _calculateRewards(user); + return rewards[user] + unstoredReward; + } + // INTERNAL METHODS - function _calculateShare(uint tokenAmount) private view returns (uint) { - uint sharePrice = getSharePrice(); + function _claim(address user) internal { + _storeReward(user); + uint rewardAmount = rewards[user]; + require(rewardAmount > 0, "No rewards to claim"); - return tokenAmount * FIXED_POINT / sharePrice ; + bank.withdrawErc20(rewardToken, user, rewardAmount); + emit RewardClaimed(user, rewardAmount); } - function _calculateToken(uint shareAmount) private view returns (uint) { - uint sharePrice = getSharePrice(); - - return shareAmount * sharePrice / FIXED_POINT; + function _storeReward(address user) internal { + uint rewardAmount = _calculateRewards(user); + rewards[user] += rewardAmount; + _lastChanged[user] = block.timestamp; } - function _increaseStake() internal { - // Function call onBlock() must not be reverted - if (!active) return; - uint amount = totalStake * interest / MILLION; - bank.withdrawErc20(address(token), address(this), amount); - totalStake += amount; - emit StakeChanged(address(this), amount); + function _calculateRewards(address user) internal view returns (uint) { + uint timePassed = block.timestamp - _lastChanged[user]; + uint reward = stakes[user] * interest / MILLION * timePassed / interestRate; + return reward * rewardTokenPrice; } function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} diff --git a/package-lock.json b/package-lock.json index 478e549f..7ad07817 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@airdao/airdao-node-contracts", - "version": "1.2.26", + "version": "1.2.29", "dependencies": { "ethers": "^5.7.2" }, diff --git a/scripts/staking/deploy_poolManager.ts b/scripts/staking/deploy_poolManager.ts index 5c3604da..db546898 100644 --- a/scripts/staking/deploy_poolManager.ts +++ b/scripts/staking/deploy_poolManager.ts @@ -41,7 +41,7 @@ export async function main() { await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); - await (await poolsManager.transferOwnership(multisig.address)).wait(); + await (await poolsManager.grantRole(await poolsManager.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); } if (require.main === module) { diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index 509c41e4..61bb9bbc 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -1,4 +1,4 @@ -import { ethers, upgrades } from "hardhat"; +import { ethers, upgrades, network } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; @@ -28,7 +28,7 @@ describe("TokenPool", function () { const interest = 100000; // 10% const interestRate = 24 * 60 * 60; // 1 day const minStakeValue = 10; - const rewardTokenPrice = 2; + const rewardTokenPrice = 1; const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [ name, token.address, rewardsBank.address, interest, interestRate, minStakeValue, token.address, rewardTokenPrice @@ -54,19 +54,9 @@ describe("TokenPool", function () { await tokenPool.activate(); expect(await tokenPool.active()).to.equal(true); }); - - it("Should set interest and min stake value", async function () { - const newInterest = 300000; // 30% - await tokenPool.setInterest(newInterest); - expect(await tokenPool.interest()).to.equal(newInterest); - - const newMinStakeValue = 20; - await tokenPool.setMinStakeValue(newMinStakeValue); - expect(await tokenPool.minStakeValue()).to.equal(newMinStakeValue); - }); }); - describe("Staking and Unstaking", function () { + describe("Staking", function () { beforeEach(async function () { await token.approve(tokenPool.address, 1000000); }); @@ -76,7 +66,6 @@ describe("TokenPool", function () { await tokenPool.stake(stake); expect(await tokenPool.totalStake()).to.equal(stake); expect(await tokenPool.getStake(owner.address)).to.equal(stake); - expect(await tokenPool.getShare(owner.address)).to.equal(1000); }); it("Should not allow staking below minimum stake value", async function () { @@ -86,8 +75,7 @@ describe("TokenPool", function () { it("Should allow unstaking", async function () { const stake = 1000; await tokenPool.stake(stake); - const shares = await tokenPool.getShare(owner.address); - await tokenPool.unstake(shares); + await tokenPool.unstake(stake); expect(await tokenPool.totalStake()).to.equal(0); expect(await tokenPool.getStake(owner.address)).to.equal(0); }); @@ -95,8 +83,38 @@ describe("TokenPool", function () { it("Should not allow unstaking more than staked", async function () { const stake = 1000; await tokenPool.stake(stake); - const shares = await tokenPool.getShare(owner.address); - await expect(tokenPool.unstake(shares.mul(2))).to.be.revertedWith("Not enough share"); + await expect(tokenPool.unstake(stake * 2)).to.be.revertedWith("Not enough stake"); + }); + }); + + describe("Rewards", function () { + beforeEach(async function () { + await token.mint(owner.address, 100000000000); + await token.approve(tokenPool.address, 1000000); + await token.transfer(rewardsBank.address, 10000); + }); + + it("Should allow claiming rewards", async function () { + const stake = 1000; + await tokenPool.stake(stake); + + const rewardBefore = await tokenPool.getReward(owner.address); + console.log("rewardBefore", rewardBefore.toString()); + + // Wait for 1 day + const futureTime = Math.floor(Date.now() / 1000) + 24 * 60 * 60; + await network.provider.send("evm_setNextBlockTimestamp", [futureTime]); + await network.provider.send("evm_mine"); // Mine a block to apply the new timestamp + + const expectedReward = 100; + const rewards = await tokenPool.getReward(owner.address); + console.log("reward", rewards.toString()); + expect (rewards).to.equal(expectedReward); + + const balanceBefore = await token.balanceOf(owner.address); + await tokenPool.claim(); + const balanceAfter = await token.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(100); }); }); }); From 41d7c2ffe83cf2ebf04c23f4770a10cfd39fb90b Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 25 Jul 2024 18:27:40 +0300 Subject: [PATCH 14/60] Fix beacon --- .../{IPoolsManager.sol => ITokenPoolsManager.sol} | 2 +- contracts/staking/token/PoolBeacon.sol | 10 ++++++++++ contracts/staking/token/TokenPool.sol | 12 +++++++----- .../{PoolsManager.sol => TokenPoolsManager.sol} | 13 ++++--------- scripts/staking/deploy_poolManager.ts | 1 + 5 files changed, 23 insertions(+), 15 deletions(-) rename contracts/staking/token/{IPoolsManager.sol => ITokenPoolsManager.sol} (95%) create mode 100644 contracts/staking/token/PoolBeacon.sol rename contracts/staking/token/{PoolsManager.sol => TokenPoolsManager.sol} (80%) diff --git a/contracts/staking/token/IPoolsManager.sol b/contracts/staking/token/ITokenPoolsManager.sol similarity index 95% rename from contracts/staking/token/IPoolsManager.sol rename to contracts/staking/token/ITokenPoolsManager.sol index 9d66a652..79bb48e6 100644 --- a/contracts/staking/token/IPoolsManager.sol +++ b/contracts/staking/token/ITokenPoolsManager.sol @@ -1,7 +1,7 @@ //SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -interface IPoolsManager { +interface ITokenPoolsManager { //OWNER METHODS function createPool( diff --git a/contracts/staking/token/PoolBeacon.sol b/contracts/staking/token/PoolBeacon.sol new file mode 100644 index 00000000..c24c4f93 --- /dev/null +++ b/contracts/staking/token/PoolBeacon.sol @@ -0,0 +1,10 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +contract PoolBeacon is UpgradeableBeacon { + constructor(address _implementation) UpgradeableBeacon(_implementation) {} +} + + diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 0df73917..cbbb5c82 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -1,14 +1,14 @@ //SPDX-License-Identifier: UNCLICENSED pragma solidity ^0.8.0; -import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./ITokenPool.sol"; import "../../funds/RewardsBank.sol"; -contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { +contract TokenPool is Initializable, AccessControl, ITokenPool { uint constant public MILLION = 1_000_000; ERC20 public token; @@ -27,6 +27,10 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { mapping(address => uint) public stakes; mapping(address => uint) public rewards; mapping(address => uint) private _lastChanged; + + constructor() { + _disableInitializers(); + } function initialize( string memory name_, address token_, RewardsBank rewardsBank_, uint intereset_, @@ -137,6 +141,4 @@ contract TokenPool is UUPSUpgradeable, AccessControlUpgradeable, ITokenPool { uint reward = stakes[user] * interest / MILLION * timePassed / interestRate; return reward * rewardTokenPrice; } - - function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} } diff --git a/contracts/staking/token/PoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol similarity index 80% rename from contracts/staking/token/PoolsManager.sol rename to contracts/staking/token/TokenPoolsManager.sol index 4f317106..023c2e7e 100644 --- a/contracts/staking/token/PoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -1,21 +1,20 @@ //_ SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./TokenPool.sol"; -import "./IPoolsManager.sol"; +import "./ITokenPoolsManager.sol"; import "../../funds/RewardsBank.sol"; -contract PoolsManager is UUPSUpgradeable, AccessControlUpgradeable, IPoolsManager { +contract TokenPoolsManager is AccessControl, ITokenPoolsManager { RewardsBank public bank; UpgradeableBeacon public beacon; mapping(string => address) public pools; - function initialize(RewardsBank bank_, UpgradeableBeacon beacon_) public initializer { + constructor(RewardsBank bank_, UpgradeableBeacon beacon_) { bank = bank_; beacon = beacon_; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); @@ -51,10 +50,6 @@ contract PoolsManager is UUPSUpgradeable, AccessControlUpgradeable, IPoolsManage emit PoolActivated(_pool); } - // INTERNAL METHODS - - function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} - // VIEW METHODS function getPoolAddress(string memory name) public view returns (address) { diff --git a/scripts/staking/deploy_poolManager.ts b/scripts/staking/deploy_poolManager.ts index db546898..0e7a1db8 100644 --- a/scripts/staking/deploy_poolManager.ts +++ b/scripts/staking/deploy_poolManager.ts @@ -1,5 +1,6 @@ import { ethers } from "hardhat"; import { deploy, loadDeployment } from "@airdao/deployments/deploying"; +import { import { ContractNames } from "../../src"; From 0158f57414ac822722384c61a06d9621f051154c Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Fri, 26 Jul 2024 12:27:50 +0300 Subject: [PATCH 15/60] Fix deploy script --- .../{PoolBeacon.sol => TokenPoolBeacon.sol} | 2 +- ...Manager.ts => deploy_tokenPoolsManager.ts} | 31 ++++++++++++++----- src/contracts/names.ts | 13 ++++---- 3 files changed, 32 insertions(+), 14 deletions(-) rename contracts/staking/token/{PoolBeacon.sol => TokenPoolBeacon.sol} (81%) rename scripts/staking/{deploy_poolManager.ts => deploy_tokenPoolsManager.ts} (60%) diff --git a/contracts/staking/token/PoolBeacon.sol b/contracts/staking/token/TokenPoolBeacon.sol similarity index 81% rename from contracts/staking/token/PoolBeacon.sol rename to contracts/staking/token/TokenPoolBeacon.sol index c24c4f93..e23bf476 100644 --- a/contracts/staking/token/PoolBeacon.sol +++ b/contracts/staking/token/TokenPoolBeacon.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -contract PoolBeacon is UpgradeableBeacon { +contract TokenPoolBeacon is UpgradeableBeacon { constructor(address _implementation) UpgradeableBeacon(_implementation) {} } diff --git a/scripts/staking/deploy_poolManager.ts b/scripts/staking/deploy_tokenPoolsManager.ts similarity index 60% rename from scripts/staking/deploy_poolManager.ts rename to scripts/staking/deploy_tokenPoolsManager.ts index 0e7a1db8..1cff30ae 100644 --- a/scripts/staking/deploy_poolManager.ts +++ b/scripts/staking/deploy_tokenPoolsManager.ts @@ -1,13 +1,14 @@ import { ethers } from "hardhat"; import { deploy, loadDeployment } from "@airdao/deployments/deploying"; -import { import { ContractNames } from "../../src"; import { - PoolsManager__factory, + TokenPoolsManager__factory, Multisig__factory, RewardsBank__factory, + TokenPoolBeacon__factory, + TokenPool__factory, } from "../../typechain-types"; import {Roadmap2023MultisigSettings} from "../addresses"; export async function main() { @@ -19,7 +20,7 @@ export async function main() { const masterMultisig = loadDeployment(ContractNames.MasterMultisig, chainId).address; const multisig = await deploy({ - contractName: ContractNames.PoolManagerMultisig, + contractName: ContractNames.TokenPoolsManagerMultisig, artifactName: "Multisig", deployArgs: [...Roadmap2023MultisigSettings, masterMultisig], signer: deployer, @@ -27,16 +28,32 @@ export async function main() { }); const rewardsBank = await deploy({ - contractName: ContractNames.PoolManagerRewardsBank, + contractName: ContractNames.TokenPoolsManagerRewardsBank, artifactName: "RewardsBank", deployArgs: [], signer: deployer, }); + + const zeroAddr = "0x0000000000000000000000000000000000000000"; + + const tokenPool = await deploy({ + contractName: ContractNames.TokenPool, + artifactName: "TokenPool", + deployArgs: ["reference", zeroAddr, zeroAddr, 1, 1, 1, zeroAddr, 1], + signer: deployer, + }); + + const tokenPoolBeacon = await deploy({ + contractName: ContractNames.TokenPoolBeacon, + artifactName: "TokenPoolBeacon", + deployArgs: [tokenPool.address], + signer: deployer, + }); - const poolsManager = await deploy({ - contractName: ContractNames.PoolManager, + const poolsManager = await deploy({ + contractName: ContractNames.TokenPoolsManager, artifactName: "PoolsManager", - deployArgs: [rewardsBank.address], + deployArgs: [rewardsBank.address, tokenPoolBeacon.address], signer: deployer, }); diff --git a/src/contracts/names.ts b/src/contracts/names.ts index 3322bb5b..f23454ca 100644 --- a/src/contracts/names.ts +++ b/src/contracts/names.ts @@ -41,6 +41,12 @@ export enum ContractNames { Treasury = "Treasury", TreasuryMultisig = "Treasury_Multisig", + TokenPool = "TokenPool", + TokenPoolBeacon = "TokenPoolBeacon", + TokenPoolsManager = "TokenPoolsManager", + TokenPoolsManagerMultisig = "TokenPoolsManager_Multisig", + TokenPoolsManagerRewardsBank = "TokenPoolsManager_RewardsBank", + // funds AirBond = "AirBond", @@ -55,11 +61,6 @@ export enum ContractNames { FeesMultisig = "Fees_Multisig", FeesTreasure = "Fees_Treasure", - TokenPool = "TokenPool", - PoolManager = "PoolManager", - PoolManagerMultisig = "PoolManager_Multisig", - PoolManagerRewardsBank = "PoolManager_RewardsBank", - // bond marketplace BondMarketplaceMultisig = "BondMarketplace_Multisig", @@ -86,7 +87,7 @@ export const MULTISIGS = { [ContractNames.FeesTreasure]: ContractNames.FeesMultisig, [ContractNames.BondMarketplaceRewardsBank]: ContractNames.BondMarketplaceMultisig, - [ContractNames.PoolManager]: ContractNames.PoolManagerMultisig, + [ContractNames.TokenPoolsManager]: ContractNames.TokenPoolsManagerMultisig, }; export const slavesMultisigsNames = [...new Set(Object.values(MULTISIGS))]; From be98d323db88c923ca7efa50334f3d5e83e1737d Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Fri, 26 Jul 2024 14:53:30 +0300 Subject: [PATCH 16/60] Tests WIP --- contracts/staking/token/TokenPool.sol | 4 -- contracts/staking/token/TokenPoolsManager.sol | 8 +-- test/staking/token/PoolsManager.ts | 72 +++++++++---------- test/staking/token/TokenPool.ts | 4 +- 4 files changed, 42 insertions(+), 46 deletions(-) diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index cbbb5c82..c905ef19 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -28,10 +28,6 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { mapping(address => uint) public rewards; mapping(address => uint) private _lastChanged; - constructor() { - _disableInitializers(); - } - function initialize( string memory name_, address token_, RewardsBank rewardsBank_, uint intereset_, uint interestRate_, uint minStakeValue_, address rewardToken_, uint rewardTokenPrice_ diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 023c2e7e..cab191dd 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -27,13 +27,13 @@ contract TokenPoolsManager is AccessControl, ITokenPoolsManager { uint minStakeValue_, address rewardToken_, uint rewardTokenPrice_ ) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { bytes memory data = abi.encodeWithSignature( - "initialize(string,address,address,uint,uint,uint,address,uint", - token_, interest_, interestRate_, minStakeValue_, rewardToken_, rewardTokenPrice_); + "initialize(string,address,address,uint256,uint256,uint256,address,uint256)", + name, token_, bank, interest_, interestRate_, minStakeValue_, rewardToken_, rewardTokenPrice_); address pool = address(new BeaconProxy(address(beacon), data)); pools[name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit PoolCreated(name, token_); - return address(pool); + emit PoolCreated(name, pool); + return pool; } function deactivatePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/test/staking/token/PoolsManager.ts b/test/staking/token/PoolsManager.ts index d4bec21b..d835d46d 100644 --- a/test/staking/token/PoolsManager.ts +++ b/test/staking/token/PoolsManager.ts @@ -2,36 +2,55 @@ import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { ethers, upgrades } from "hardhat"; import { - PoolsManager, + TokenPool, + TokenPoolsManager, RewardsBank, AirBond__factory, RewardsBank__factory, - PoolsManager__factory, + TokenPoolsManager__factory, + TokenPoolBeacon__factory, } from "../../../typechain-types"; +import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; + import { expect } from "chai"; describe("PoolsManager", function () { - let poolsManager: PoolsManager; + let poolsManager: TokenPoolsManager; let rewardsBank: RewardsBank; let tokenAddr: string; + let owner: SignerWithAddress; async function deploy() { const [owner] = await ethers.getSigners(); const rewardsBank = await new RewardsBank__factory(owner).deploy(); const airBond = await new AirBond__factory(owner).deploy(owner.address); + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); + + const interest = 100000; // 10% + const interestRate = 24 * 60 * 60; // 24 hours + const minStakeValue = 10; + const rewardTokenPrice = 1; + + //Deploy the implementation + const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [ + "Test", airBond.address, rewardsBank.address, interest, + interestRate, minStakeValue, airBond.address, rewardTokenPrice + ])) as TokenPool; + + const tokenPoolBeacon = await new TokenPoolBeacon__factory(owner).deploy(tokenPool.address); - const poolsManager = await new PoolsManager__factory(owner).deploy(rewardsBank.address); + const poolsManager = await new TokenPoolsManager__factory(owner).deploy(rewardsBank.address, tokenPoolBeacon.address); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); const tokenAddr = airBond.address; - return { poolsManager, rewardsBank, tokenAddr }; + return { poolsManager, rewardsBank, tokenAddr, owner }; } beforeEach(async function () { - ({ poolsManager, rewardsBank, tokenAddr } = await loadFixture(deploy)); + ({ poolsManager, rewardsBank, tokenAddr, owner } = await loadFixture(deploy)); }); describe("Pool Management", function () { @@ -40,11 +59,11 @@ describe("PoolsManager", function () { const interestRate = 24 * 60 * 60; // 24 hours const minStakeValue = 10; - const tx = await poolsManager.createPool(tokenAddr, interest,interestRate, minStakeValue); + const tx = await poolsManager.createPool("TestProxy", tokenAddr, interest, interestRate, minStakeValue, tokenAddr, 1); const receipt = await tx.wait(); - const poolAddress = receipt.events![3].args!.pool; + const poolAddress = receipt.events![2].args![1]; - expect(await poolsManager.getPool(tokenAddr)).to.equal(poolAddress); + expect(await poolsManager.getPoolAddress("TestProxy")).to.equal(poolAddress); }); it("Should activate and deactivate a pool", async function () { @@ -52,35 +71,14 @@ describe("PoolsManager", function () { const interestRate = 24 * 60 * 60; // 24 hours const minStakeValue = 10; - const tx = await poolsManager.createPool(tokenAddr, interest, interestRate, minStakeValue); - const receipt = await tx.wait(); - const poolAddress = receipt.events![3].args!.pool; - - await poolsManager.deactivatePool(poolAddress); - expect(await poolsManager.getPoolInfo(poolAddress)).to.include(false); // Pool should be inactive - - await poolsManager.activatePool(poolAddress); - expect(await poolsManager.getPoolInfo(poolAddress)).to.include(true); // Pool should be active - }); - - it("Should allow the owner to set interest and min stake value", async function () { - const interest = 100000; // 10% - const interestRate = 24 * 60 * 60; // 24 hours - const minStakeValue = 10; - - const tx = await poolsManager.createPool(tokenAddr, interest, interestRate, minStakeValue); - const receipt = await tx.wait(); - const poolAddress = receipt.events![3].args![0]; - - const newInterest = 300000; // 30% - await poolsManager.setInterest(poolAddress, newInterest); - const [,interestSet,,,] = await poolsManager.getPoolInfo(poolAddress); - expect(interestSet).to.equal(newInterest); + await poolsManager.createPool("TestProxy", tokenAddr, interest, interestRate, minStakeValue, tokenAddr, 1); + const poolAddress = await poolsManager.getPoolAddress("TestProxy"); + console.log("Pool Address: ", poolAddress); - const newMinStakeValue = 20; - await poolsManager.setMinStakeValue(poolAddress, newMinStakeValue); - const [,,minStakeValueSet,,,] = await poolsManager.getPoolInfo(poolAddress); - expect(minStakeValueSet).to.equal(newMinStakeValue); + //await poolsManager.deactivatePool("TestProxy"); + const proxyPool = new ethers.Contract(poolAddress, TokenPoolJson.abi, owner); + const active = await proxyPool.active(); + console.log("Active: ", active); }); }); diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index 61bb9bbc..2d21ee3a 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -8,10 +8,12 @@ import { TokenPool, RewardsBank__factory, AirBond__factory, - TokenPool__factory, } from "../../../typechain-types"; +import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; + import { expect } from "chai"; + describe("TokenPool", function () { let owner: SignerWithAddress; let tokenPool: TokenPool; From d9f58fef0c15aed2200df22ec7ce76e3466a4305 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 29 Jul 2024 15:34:54 +0300 Subject: [PATCH 17/60] Fix resetting rewards --- contracts/staking/token/TokenPool.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index c905ef19..f45fb6c1 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -123,6 +123,7 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { require(rewardAmount > 0, "No rewards to claim"); bank.withdrawErc20(rewardToken, user, rewardAmount); + rewards[user] = 0; emit RewardClaimed(user, rewardAmount); } From 7c819d5c67647e4a45804c9950b8b7f49052ce4b Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 29 Jul 2024 17:42:32 +0300 Subject: [PATCH 18/60] Deploy on dev --- .openzeppelin/unknown-30746.json | 208 ++++++++++++++++++++ contracts/staking/token/TokenPoolBeacon.sol | 10 - deployments/30746.json | 88 +++++++++ scripts/staking/deploy_tokenPoolsManager.ts | 23 +-- 4 files changed, 302 insertions(+), 27 deletions(-) create mode 100644 .openzeppelin/unknown-30746.json delete mode 100644 contracts/staking/token/TokenPoolBeacon.sol diff --git a/.openzeppelin/unknown-30746.json b/.openzeppelin/unknown-30746.json new file mode 100644 index 00000000..e73d8cf6 --- /dev/null +++ b/.openzeppelin/unknown-30746.json @@ -0,0 +1,208 @@ +{ + "manifestVersion": "3.2", + "proxies": [], + "impls": { + "69b1a6d3238e23fc5797272349e6b8637583715c4adf7fc7231c7066ef6c1c1f": { + "address": "0x92e2502fcF33EE739373E6DaB1Db25e1318018a4", + "txHash": "0xf06469508466a4af7711e151e2306c80063e82c49e322572c6373c0e0fbee870", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:68" + }, + { + "label": "_roles", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:56" + }, + { + "label": "token", + "offset": 0, + "slot": "2", + "type": "t_contract(ERC20)5159", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:14" + }, + { + "label": "bank", + "offset": 0, + "slot": "3", + "type": "t_contract(RewardsBank)10830", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:15" + }, + { + "label": "name", + "offset": 0, + "slot": "4", + "type": "t_string_storage", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:17" + }, + { + "label": "minStakeValue", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:18" + }, + { + "label": "totalStake", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:19" + }, + { + "label": "active", + "offset": 0, + "slot": "7", + "type": "t_bool", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:20" + }, + { + "label": "rewardToken", + "offset": 1, + "slot": "7", + "type": "t_address", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:21" + }, + { + "label": "rewardTokenPrice", + "offset": 0, + "slot": "8", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:22" + }, + { + "label": "interest", + "offset": 0, + "slot": "9", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:24" + }, + { + "label": "interestRate", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:25" + }, + { + "label": "stakes", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:27" + }, + { + "label": "rewards", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:28" + }, + { + "label": "_lastChanged", + "offset": 0, + "slot": "13", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:29" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(ERC20)5159": { + "label": "contract ERC20", + "numberOfBytes": "20" + }, + "t_contract(RewardsBank)10830": { + "label": "contract RewardsBank", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoleData)3267_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + } + } +} diff --git a/contracts/staking/token/TokenPoolBeacon.sol b/contracts/staking/token/TokenPoolBeacon.sol deleted file mode 100644 index e23bf476..00000000 --- a/contracts/staking/token/TokenPoolBeacon.sol +++ /dev/null @@ -1,10 +0,0 @@ -//SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; - -contract TokenPoolBeacon is UpgradeableBeacon { - constructor(address _implementation) UpgradeableBeacon(_implementation) {} -} - - diff --git a/deployments/30746.json b/deployments/30746.json index db1b9609..bb118ac5 100644 --- a/deployments/30746.json +++ b/deployments/30746.json @@ -1113,5 +1113,93 @@ ], "deployTx": "0x0a9cf5c7b2bf4b772508d528839b7dcd86655ffe2149eb19240d18748b910651", "fullyQualifiedName": "contracts/funds/RewardsBank.sol:RewardsBank" + }, + "TokenPoolsManager_Multisig": { + "address": "0xEF0474bF72323AAc90Fd406fC7Df6A79A08E1593", + "abi": [ + "constructor(address[] _signers, bool[] isInitiatorFlags, uint256 _threshold, address owner)", + "event Confirmation(address indexed sender, uint256 indexed txId)", + "event Execution(uint256 indexed txId)", + "event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)", + "event Revocation(address indexed sender, uint256 indexed txId)", + "event SignerAddition(address indexed signer, bool isInitiator)", + "event SignerRemoval(address indexed signer)", + "event Submission(uint256 indexed txId)", + "event ThresholdChange(uint256 required)", + "function changeSigners(address[] signersToRemove, address[] signersToAdd, bool[] isInitiatorFlags)", + "function changeThreshold(uint256 _threshold)", + "function checkBeforeSubmitTransaction(address destination, uint256 value, bytes data) payable", + "function confirmTransaction(uint256 txId)", + "function confirmations(uint256, address) view returns (bool)", + "function getConfirmations(uint256 txId) view returns (address[])", + "function getInitiatorsCount() view returns (uint256)", + "function getRequiredSignersCount() view returns (uint256)", + "function getSigners() view returns (address[], bool[])", + "function getTransactionData(uint256 txId) view returns (tuple(address destination, uint256 value, bytes data, bool executed), address[])", + "function getTransactionIds(uint256 from, uint256 to, bool pending, bool executed) view returns (uint256[] result)", + "function isConfirmed(uint256 txId) view returns (bool)", + "function isInitiator(address) view returns (bool)", + "function isSigner(address) view returns (bool)", + "function owner() view returns (address)", + "function renounceOwnership()", + "function revokeConfirmation(uint256 txId)", + "function signers(uint256) view returns (address)", + "function submitTransaction(address destination, uint256 value, bytes data) payable returns (uint256 txId)", + "function threshold() view returns (uint256)", + "function transactionCount() view returns (uint256)", + "function transactions(uint256) view returns (address destination, uint256 value, bytes data, bool executed)", + "function transferOwnership(address newOwner)", + "function withdraw(address to, uint256 amount)" + ], + "deployTx": "0xf0eb8bdb4c5b7791d21cf7f91c3b11962ee33fb2dec732051f91f38804e6b814", + "fullyQualifiedName": "contracts/multisig/Multisig.sol:Multisig" + }, + "TokenPoolsManager_RewardsBank": { + "address": "0xBBFF4f169315334883696CE241e1c8FD7da149b0", + "abi": [ + "constructor()", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function grantRole(bytes32 role, address account)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function withdrawAmb(address addressTo, uint256 amount)", + "function withdrawErc20(address tokenAddress, address addressTo, uint256 amount)" + ], + "deployTx": "0x8e7db1954997e0a7a701e459c9169b7d92c123b02f1a7ebc30e237d73fd4353e", + "fullyQualifiedName": "contracts/funds/RewardsBank.sol:RewardsBank" + }, + "TokenPoolsManager": { + "address": "0xCc9DCDdbFBd19444975dA4CabD97d8E6496C087F", + "abi": [ + "constructor(address bank_, address beacon_)", + "event PoolActivated(string indexed name)", + "event PoolCreated(string indexed name, address indexed token)", + "event PoolDeactivated(string indexed name)", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function activatePool(string _pool)", + "function bank() view returns (address)", + "function beacon() view returns (address)", + "function createPool(string name, address token_, uint256 interest_, uint256 interestRate_, uint256 minStakeValue_, address rewardToken_, uint256 rewardTokenPrice_) returns (address)", + "function deactivatePool(string _pool)", + "function getPoolAddress(string name) view returns (address)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function grantRole(bytes32 role, address account)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function pools(string) view returns (address)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)" + ], + "deployTx": "0x8a262dbbb8b2f97d1a175083808d1c1a5d790ff68a9e5ae651a5a88331717bd1", + "fullyQualifiedName": "contracts/staking/token/TokenPoolsManager.sol:TokenPoolsManager" } } \ No newline at end of file diff --git a/scripts/staking/deploy_tokenPoolsManager.ts b/scripts/staking/deploy_tokenPoolsManager.ts index 1cff30ae..0e582608 100644 --- a/scripts/staking/deploy_tokenPoolsManager.ts +++ b/scripts/staking/deploy_tokenPoolsManager.ts @@ -1,4 +1,4 @@ -import { ethers } from "hardhat"; +import { ethers, upgrades } from "hardhat"; import { deploy, loadDeployment } from "@airdao/deployments/deploying"; import { ContractNames } from "../../src"; @@ -32,29 +32,18 @@ export async function main() { artifactName: "RewardsBank", deployArgs: [], signer: deployer, + loadIfAlreadyDeployed: true, }); - const zeroAddr = "0x0000000000000000000000000000000000000000"; - - const tokenPool = await deploy({ - contractName: ContractNames.TokenPool, - artifactName: "TokenPool", - deployArgs: ["reference", zeroAddr, zeroAddr, 1, 1, 1, zeroAddr, 1], - signer: deployer, - }); - - const tokenPoolBeacon = await deploy({ - contractName: ContractNames.TokenPoolBeacon, - artifactName: "TokenPoolBeacon", - deployArgs: [tokenPool.address], - signer: deployer, - }); + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); + const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); const poolsManager = await deploy({ contractName: ContractNames.TokenPoolsManager, - artifactName: "PoolsManager", + artifactName: "TokenPoolsManager", deployArgs: [rewardsBank.address, tokenPoolBeacon.address], signer: deployer, + loadIfAlreadyDeployed: true, }); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); From 545f193df7c099a318cd1092ddaf71ef129fe9a2 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 29 Jul 2024 18:19:35 +0300 Subject: [PATCH 19/60] Fix tests --- test/staking/token/TokenPool.ts | 4 +--- .../{PoolsManager.ts => TokenPoolsManager.ts} | 16 ++-------------- 2 files changed, 3 insertions(+), 17 deletions(-) rename test/staking/token/{PoolsManager.ts => TokenPoolsManager.ts} (82%) diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index 2d21ee3a..61bb9bbc 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -8,12 +8,10 @@ import { TokenPool, RewardsBank__factory, AirBond__factory, + TokenPool__factory, } from "../../../typechain-types"; -import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; - import { expect } from "chai"; - describe("TokenPool", function () { let owner: SignerWithAddress; let tokenPool: TokenPool; diff --git a/test/staking/token/PoolsManager.ts b/test/staking/token/TokenPoolsManager.ts similarity index 82% rename from test/staking/token/PoolsManager.ts rename to test/staking/token/TokenPoolsManager.ts index d835d46d..58e9b2ee 100644 --- a/test/staking/token/PoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -27,19 +27,7 @@ describe("PoolsManager", function () { const rewardsBank = await new RewardsBank__factory(owner).deploy(); const airBond = await new AirBond__factory(owner).deploy(owner.address); const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - - const interest = 100000; // 10% - const interestRate = 24 * 60 * 60; // 24 hours - const minStakeValue = 10; - const rewardTokenPrice = 1; - - //Deploy the implementation - const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [ - "Test", airBond.address, rewardsBank.address, interest, - interestRate, minStakeValue, airBond.address, rewardTokenPrice - ])) as TokenPool; - - const tokenPoolBeacon = await new TokenPoolBeacon__factory(owner).deploy(tokenPool.address); + const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); const poolsManager = await new TokenPoolsManager__factory(owner).deploy(rewardsBank.address, tokenPoolBeacon.address); @@ -61,7 +49,7 @@ describe("PoolsManager", function () { const tx = await poolsManager.createPool("TestProxy", tokenAddr, interest, interestRate, minStakeValue, tokenAddr, 1); const receipt = await tx.wait(); - const poolAddress = receipt.events![2].args![1]; + const poolAddress = receipt.events![4].args![1]; expect(await poolsManager.getPoolAddress("TestProxy")).to.equal(poolAddress); }); From 7769557d5dce1e7b4bdbddfb80b4f75c6f77a24a Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 22 Aug 2024 20:04:57 +0300 Subject: [PATCH 20/60] WIP --- contracts/staking/token/ITokenPool.sol | 5 + .../staking/token/ITokenPoolsManager.sol | 22 --- contracts/staking/token/TokenPool.sol | 151 ++++++++++++++---- contracts/staking/token/TokenPoolsManager.sol | 7 +- package-lock.json | 4 +- 5 files changed, 129 insertions(+), 60 deletions(-) delete mode 100644 contracts/staking/token/ITokenPoolsManager.sol diff --git a/contracts/staking/token/ITokenPool.sol b/contracts/staking/token/ITokenPool.sol index 360c13e4..547f4dbd 100644 --- a/contracts/staking/token/ITokenPool.sol +++ b/contracts/staking/token/ITokenPool.sol @@ -16,6 +16,11 @@ interface ITokenPool { //EVENTS event StakeChanged(address indexed user, uint amount); event MinStakeValueChanged(uint minStakeValue); + event InterestAdded(uint amount); + event InterestRateChanged(uint interest, uint interestRate); + event LockPeriodChanged(uint period); + event RewardTokenPriceChanged(uint price); + event FastUnstakePenaltyChanged(uint penalty); event RewardClaimed(address indexed user, uint amount); event Deactivated(); event Activated(); diff --git a/contracts/staking/token/ITokenPoolsManager.sol b/contracts/staking/token/ITokenPoolsManager.sol deleted file mode 100644 index 79bb48e6..00000000 --- a/contracts/staking/token/ITokenPoolsManager.sol +++ /dev/null @@ -1,22 +0,0 @@ -//SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -interface ITokenPoolsManager { - - //OWNER METHODS - function createPool( - string memory name, address _token, uint _interest, - uint _interestRate, uint _minStakeValue, address _rewardToken, uint _rewardTokenPrice - ) external returns (address); - function deactivatePool(string memory _pool) external; - function activatePool(string memory _pool) external; - - //VIEW METHODS - function getPoolAddress(string memory _pool) external view returns (address); - - //EVENTS - - event PoolCreated(string indexed name, address indexed token); - event PoolDeactivated(string indexed name); - event PoolActivated(string indexed name); -} diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index f45fb6c1..23f23a32 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -7,42 +7,55 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./ITokenPool.sol"; import "../../funds/RewardsBank.sol"; +import "../../LockKeeper.sol"; contract TokenPool is Initializable, AccessControl, ITokenPool { uint constant public MILLION = 1_000_000; ERC20 public token; RewardsBank public bank; + LockKeeper public lockKeeper; string public name; uint public minStakeValue; - uint public totalStake; - bool public active; + uint public fastUnstakePenalty; + uint public interest; + uint public interestRate; //Time in seconds to how often the stake is increased + uint public lockPeriod; address public rewardToken; uint public rewardTokenPrice; // The coefficient to calculate the reward token amount - uint public interest; - uint public interestRate; //Time in seconds to how often the stake is increased + bool public active; + uint public totalStake; + uint public totalRewards; + + uint private lastInterestUpdate; + uint private totalRewardsDebt; mapping(address => uint) public stakes; - mapping(address => uint) public rewards; - mapping(address => uint) private _lastChanged; + mapping(address => uint) private rewardsDebt; + mapping(address => uint) private claimableRewards; + mapping(address => uint) private lockedWithdrawals; function initialize( - string memory name_, address token_, RewardsBank rewardsBank_, uint intereset_, - uint interestRate_, uint minStakeValue_, address rewardToken_, uint rewardTokenPrice_ + address token_, RewardsBank rewardsBank_, LockKeeper lockkeeper_, string memory name_, uint minStakeValue_, + uint fastUnstakePenalty_, uint intereset_, uint interestRate_, uint lockPeriod_, address rewardToken_, uint rewardTokenPrice_ ) public initializer { token = ERC20(token_); bank = rewardsBank_; + lockKeeper = lockkeeper_; name = name_; minStakeValue = minStakeValue_; - active = true; + fastUnstakePenalty = fastUnstakePenalty_; + interest = intereset_; + interestRate = interestRate_; + lockPeriod = lockPeriod_; rewardToken = rewardToken_; rewardTokenPrice = rewardTokenPrice_; - interest = intereset_; - interestRate = interestRate_; + active = true; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -60,6 +73,33 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { emit Deactivated(); } + function setMinStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + minStakeValue = value; + emit MinStakeValueChanged(value); + } + + function setInterest(uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { + _addInterest(); + interest = _interest; + interestRate = _interestRate; + emit InterestRateChanged(interest, interestRate); + } + + function setLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + lockPeriod = period; + emit LockPeriodChanged(period); + } + + function setRewardTokenPrice(uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { + rewardTokenPrice = price; + emit RewardTokenPriceChanged(price); + } + + function setFastUnstakePenalty(uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { + fastUnstakePenalty = penalty; + emit FastUnstakePenaltyChanged(penalty); + } + // PUBLIC METHODS function stake(uint amount) public { @@ -67,28 +107,51 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { require(amount >= minStakeValue, "Amount is less than minStakeValue"); require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); - _storeReward(msg.sender); + _calcClaimableRewards(msg.sender); + + uint rewardsAmount = _calcRewards(amount); stakes[msg.sender] += amount; totalStake += amount; + totalRewards += rewardsAmount; + rewardsDebt[msg.sender] += rewardsAmount; + totalRewardsDebt += rewardsAmount; + emit StakeChanged(msg.sender, amount); } function unstake(uint amount) public { require(stakes[msg.sender] >= amount, "Not enough stake"); - _storeReward(msg.sender); + _calcClaimableRewards(msg.sender); + + uint rewardsAmount = _calcRewards(amount); - totalStake -= amount; stakes[msg.sender] -= amount; + totalStake -= amount; - // If all stake is withdrawn, claim rewards - if (stakes[msg.sender] == 0 && rewards[msg.sender] > 0) { - _claim(msg.sender); - } + totalRewards -= rewardsAmount; + rewardsDebt[msg.sender] -= rewardsAmount; + totalRewardsDebt -= rewardsAmount; + + // cancel previous lock (if exists). canceledAmount will be added to new lock + uint canceledAmount; + if (lockKeeper.getLock(lockedWithdrawals[msg.sender]).totalClaims > 0) // prev lock exists + canceledAmount = lockKeeper.cancelLock(lockedWithdrawals[msg.sender]); + + // lock funds + lockedWithdrawals[msg.sender] = lockKeeper.lockSingle( + msg.sender, address(token), uint64(block.timestamp + lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(token)))) + ); + + emit StakeChanged(msg.sender, stakes[msg.sender]); + } + + function unstakeFast(uint amount) public { + require(stakes[msg.sender] >= amount, "Not enough stake"); - require(token.transfer(msg.sender, stakes[msg.sender]), "Transfer failed"); emit StakeChanged(msg.sender, stakes[msg.sender]); } @@ -110,32 +173,52 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { return interestRate; } - function getReward(address user) public view returns (uint) { - uint unstoredReward = _calculateRewards(user); - return rewards[user] + unstoredReward; + function getUserRewards(address user) public view returns (uint) { + //TODO: implement } // INTERNAL METHODS + // store claimable rewards + function _calcClaimableRewards(address user) internal { + uint rewardsAmount = _calcRewards(stakes[user]); + uint rewardsWithoutDebt = rewardsAmount - rewardsDebt[user]; + claimableRewards[user] += rewardsWithoutDebt; + totalRewardsDebt += rewardsWithoutDebt; + rewardsDebt[user] += rewardsWithoutDebt; + } + + function _addInterest() internal { + uint timePassed = block.timestamp - lastInterestUpdate; + uint newRewards = totalStake * interest * timePassed / (MILLION * interestRate); + + totalRewards += newRewards; + lastInterestUpdate = block.timestamp; + emit InterestAdded(newRewards); + } + function _claim(address user) internal { - _storeReward(user); - uint rewardAmount = rewards[user]; - require(rewardAmount > 0, "No rewards to claim"); + //TODO: Implement bank.withdrawErc20(rewardToken, user, rewardAmount); - rewards[user] = 0; emit RewardClaimed(user, rewardAmount); } - function _storeReward(address user) internal { - uint rewardAmount = _calculateRewards(user); - rewards[user] += rewardAmount; - _lastChanged[user] = block.timestamp; + function _calcRewards(uint amount) internal view returns (uint) { + if (totalStake == 0 && totalRewards == 0) return amount; + return amount * totalRewards / totalStake; } - function _calculateRewards(address user) internal view returns (uint) { - uint timePassed = block.timestamp - _lastChanged[user]; - uint reward = stakes[user] * interest / MILLION * timePassed / interestRate; - return reward * rewardTokenPrice; + function _addressToString(address x) internal pure returns (string memory) { + bytes memory s = new bytes(40); + for (uint i = 0; i < 20; i++) { + uint8 b = uint8(uint(uint160(x)) / (2 ** (8 * (19 - i)))); + uint8 hi = (b / 16); + uint8 lo = (b - 16 * hi); + s[2 * i] = _char(hi); + s[2 * i + 1] = _char(lo); + } + return string(s); } + } diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index cab191dd..9f17ca21 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -5,10 +5,9 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./TokenPool.sol"; -import "./ITokenPoolsManager.sol"; import "../../funds/RewardsBank.sol"; -contract TokenPoolsManager is AccessControl, ITokenPoolsManager { +contract TokenPoolsManager is AccessControl{ RewardsBank public bank; UpgradeableBeacon public beacon; @@ -20,6 +19,10 @@ contract TokenPoolsManager is AccessControl, ITokenPoolsManager { _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } + event PoolCreated(string name, address pool); + event PoolDeactivated(string name); + event PoolActivated(string name); + // OWNER METHODS function createPool( diff --git a/package-lock.json b/package-lock.json index e8947c04..04b16b0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@airdao/airdao-node-contracts", - "version": "1.3.7", + "version": "1.3.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@airdao/airdao-node-contracts", - "version": "1.3.7", + "version": "1.3.15", "dependencies": { "ethers": "^5.7.2" }, From 897e562eb2bf86ff3ca7a584a3e2e0f94f2e5256 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 2 Sep 2024 20:15:15 +0300 Subject: [PATCH 21/60] Update token pool --- contracts/staking/token/ITokenPool.sol | 27 ----- contracts/staking/token/TokenPool.sol | 133 +++++++++++++++++------- test/staking/token/TokenPool.ts | 138 ++++++++++++++++++++----- 3 files changed, 206 insertions(+), 92 deletions(-) delete mode 100644 contracts/staking/token/ITokenPool.sol diff --git a/contracts/staking/token/ITokenPool.sol b/contracts/staking/token/ITokenPool.sol deleted file mode 100644 index 547f4dbd..00000000 --- a/contracts/staking/token/ITokenPool.sol +++ /dev/null @@ -1,27 +0,0 @@ -//SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -interface ITokenPool { - //OWNER METHODS - function activate() external; - function deactivate() external; - - //PUBLIC METHODS - function stake(uint amount) external; - function unstake(uint amount) external; - - //VIEW METHODS - function getStake(address user) external view returns (uint); - - //EVENTS - event StakeChanged(address indexed user, uint amount); - event MinStakeValueChanged(uint minStakeValue); - event InterestAdded(uint amount); - event InterestRateChanged(uint interest, uint interestRate); - event LockPeriodChanged(uint period); - event RewardTokenPriceChanged(uint price); - event FastUnstakePenaltyChanged(uint penalty); - event RewardClaimed(address indexed user, uint amount); - event Deactivated(); - event Activated(); -} diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 23f23a32..67503a16 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -2,18 +2,19 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/AccessControl.sol"; -import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./ITokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -contract TokenPool is Initializable, AccessControl, ITokenPool { +import "hardhat/console.sol"; + +contract TokenPool is Initializable, AccessControl, IOnBlockListener { uint constant public MILLION = 1_000_000; - ERC20 public token; - RewardsBank public bank; + IERC20 public token; + RewardsBank public rewardsBank; LockKeeper public lockKeeper; string public name; @@ -37,12 +38,27 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { mapping(address => uint) private claimableRewards; mapping(address => uint) private lockedWithdrawals; + //EVENTS + + event Deactivated(); + event Activated(); + event MinStakeValueChanged(uint minStakeValue); + event InterestRateChanged(uint interest, uint interestRate); + event LockPeriodChanged(uint period); + event RewardTokenPriceChanged(uint price); + event FastUnstakePenaltyChanged(uint penalty); + event StakeChanged(address indexed user, uint amount); + event Claim(address indexed user, uint amount); + event Interest(uint amount); + event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); + event UnstakeFast(address indexed user, uint amount, uint penalty); + function initialize( address token_, RewardsBank rewardsBank_, LockKeeper lockkeeper_, string memory name_, uint minStakeValue_, uint fastUnstakePenalty_, uint intereset_, uint interestRate_, uint lockPeriod_, address rewardToken_, uint rewardTokenPrice_ ) public initializer { - token = ERC20(token_); - bank = rewardsBank_; + token = IERC20(token_); + rewardsBank = rewardsBank_; lockKeeper = lockkeeper_; name = name_; @@ -53,6 +69,7 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { lockPeriod = lockPeriod_; rewardToken = rewardToken_; rewardTokenPrice = rewardTokenPrice_; + lastInterestUpdate = block.timestamp; active = true; @@ -104,19 +121,10 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { function stake(uint amount) public { require(active, "Pool is not active"); - require(amount >= minStakeValue, "Amount is less than minStakeValue"); + require(amount >= minStakeValue, "Pool: stake value is too low"); require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); - _calcClaimableRewards(msg.sender); - - uint rewardsAmount = _calcRewards(amount); - - stakes[msg.sender] += amount; - totalStake += amount; - - totalRewards += rewardsAmount; - rewardsDebt[msg.sender] += rewardsAmount; - totalRewardsDebt += rewardsAmount; + _stake(msg.sender, amount); emit StakeChanged(msg.sender, amount); } @@ -124,39 +132,49 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { function unstake(uint amount) public { require(stakes[msg.sender] >= amount, "Not enough stake"); - _calcClaimableRewards(msg.sender); - - uint rewardsAmount = _calcRewards(amount); - - stakes[msg.sender] -= amount; - totalStake -= amount; - - totalRewards -= rewardsAmount; - rewardsDebt[msg.sender] -= rewardsAmount; - totalRewardsDebt -= rewardsAmount; + _unstake(msg.sender, amount); // cancel previous lock (if exists). canceledAmount will be added to new lock uint canceledAmount; if (lockKeeper.getLock(lockedWithdrawals[msg.sender]).totalClaims > 0) // prev lock exists canceledAmount = lockKeeper.cancelLock(lockedWithdrawals[msg.sender]); + token.approve(address(lockKeeper), amount + canceledAmount); + // lock funds lockedWithdrawals[msg.sender] = lockKeeper.lockSingle( msg.sender, address(token), uint64(block.timestamp + lockPeriod), amount + canceledAmount, string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(token)))) ); + _claimRewards(msg.sender); + + emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + lockPeriod, block.timestamp); emit StakeChanged(msg.sender, stakes[msg.sender]); } function unstakeFast(uint amount) public { require(stakes[msg.sender] >= amount, "Not enough stake"); + _unstake(msg.sender, amount); + + uint penalty = amount * fastUnstakePenalty / MILLION; + SafeERC20.safeTransfer(token, msg.sender, amount - penalty); + + _claimRewards(msg.sender); + + emit UnstakeFast(msg.sender, amount, penalty); emit StakeChanged(msg.sender, stakes[msg.sender]); } function claim() public { - _claim(msg.sender); + console.log("claiming rewards"); + _calcClaimableRewards(msg.sender); + _claimRewards(msg.sender); + } + + function onBlock() external { + _addInterest(); } // VIEW METHODS @@ -174,7 +192,11 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { } function getUserRewards(address user) public view returns (uint) { - //TODO: implement + uint rewardsAmount = _calcRewards(stakes[user]); + if (rewardsAmount + claimableRewards[user] <= rewardsDebt[user]) + return 0; + + return rewardsAmount + claimableRewards[user] - rewardsDebt[user]; } // INTERNAL METHODS @@ -189,19 +211,52 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { } function _addInterest() internal { + if (lastInterestUpdate + interestRate > block.timestamp) return; uint timePassed = block.timestamp - lastInterestUpdate; - uint newRewards = totalStake * interest * timePassed / (MILLION * interestRate); + uint newRewards = totalStake * interest * timePassed / MILLION / interestRate; totalRewards += newRewards; lastInterestUpdate = block.timestamp; - emit InterestAdded(newRewards); + emit Interest(newRewards); + } + + function _stake(address user, uint amount) internal { + uint rewardsAmount = _calcRewards(amount); + + stakes[msg.sender] += amount; + totalStake += amount; + + totalRewards += rewardsAmount; + + _updateRewardsDebt(user, _calcRewards(stakes[user])); } - function _claim(address user) internal { - //TODO: Implement + function _unstake(address user, uint amount) internal { + uint rewardsAmount = _calcRewards(amount); + + stakes[msg.sender] -= amount; + totalStake -= amount; + + totalRewards -= rewardsAmount; + _updateRewardsDebt(user, _calcRewards(stakes[user])); + } - bank.withdrawErc20(rewardToken, user, rewardAmount); - emit RewardClaimed(user, rewardAmount); + function _updateRewardsDebt(address user, uint newDebt) internal { + uint oldDebt = rewardsDebt[user]; + if (newDebt < oldDebt) totalRewardsDebt -= oldDebt - newDebt; + else totalRewardsDebt += newDebt - oldDebt; + rewardsDebt[user] = newDebt; + } + + function _claimRewards(address user) internal { + uint amount = claimableRewards[user]; + if (amount == 0) return; + + claimableRewards[user] = 0; + + uint rewardTokenAmount = amount * rewardTokenPrice; + rewardsBank.withdrawErc20(rewardToken, payable(user), rewardTokenAmount); + emit Claim(user, rewardTokenAmount); } function _calcRewards(uint amount) internal view returns (uint) { @@ -221,4 +276,8 @@ contract TokenPool is Initializable, AccessControl, ITokenPool { return string(s); } + function _char(uint8 b) internal pure returns (bytes1 c) { + return bytes1(b + (b < 10 ? 0x30 : 0x57)); + } + } diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index 61bb9bbc..bf67dd67 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -1,5 +1,5 @@ -import { ethers, upgrades, network } from "hardhat"; -import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { ethers, upgrades } from "hardhat"; +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { @@ -8,30 +8,40 @@ import { TokenPool, RewardsBank__factory, AirBond__factory, - TokenPool__factory, + LockKeeper__factory, + LockKeeper, } from "../../../typechain-types"; +const D1 = 24 * 60 * 60; +const MILLION = 1000000; + +const name = "Test"; +const minStakeValue = 10; +const fastUnstakePenalty = 100000; // 10% +const interest = 100000; // 10% +const interestRate = D1; // 1 day +const lockPeriod = 24 * 60 * 60; // 1 day +const rewardTokenPrice = 1; + import { expect } from "chai"; describe("TokenPool", function () { let owner: SignerWithAddress; let tokenPool: TokenPool; let rewardsBank: RewardsBank; + let lockKeeper: LockKeeper; let token: AirBond; async function deploy() { const [owner] = await ethers.getSigners(); const rewardsBank = await new RewardsBank__factory(owner).deploy(); const token = await new AirBond__factory(owner).deploy(owner.address); + const lockKeeper = await new LockKeeper__factory(owner).deploy(); + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - const name = "Test"; - const interest = 100000; // 10% - const interestRate = 24 * 60 * 60; // 1 day - const minStakeValue = 10; - const rewardTokenPrice = 1; const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [ - name, token.address, rewardsBank.address, interest, - interestRate, minStakeValue, token.address, rewardTokenPrice + token.address, rewardsBank.address, lockKeeper.address, name, minStakeValue, fastUnstakePenalty, + interest, interestRate, lockPeriod, token.address, rewardTokenPrice ])) as TokenPool; await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), tokenPool.address)).wait(); @@ -39,15 +49,15 @@ describe("TokenPool", function () { await token.mint(owner.address, 100000000000); - return { owner, tokenPool, rewardsBank, token }; + return { owner, tokenPool, rewardsBank, lockKeeper, token }; } beforeEach(async function () { - ({ owner, tokenPool, rewardsBank, token } = await loadFixture(deploy)); + ({ owner, tokenPool, rewardsBank, lockKeeper, token } = await loadFixture(deploy)); }); describe("Owner Methods", function () { - it("Should activate and deactivate the pool", async function () { + it("Should deactivate and activate the pool", async function () { await tokenPool.deactivate(); expect(await tokenPool.active()).to.equal(false); @@ -68,23 +78,79 @@ describe("TokenPool", function () { expect(await tokenPool.getStake(owner.address)).to.equal(stake); }); + it("Should not allow staking when pool is deactivated", async function () { + await tokenPool.deactivate(); + await expect(tokenPool.stake(1000)).to.be.revertedWith("Pool is not active"); + }); + it("Should not allow staking below minimum stake value", async function () { - await expect(tokenPool.stake(1)).to.be.revertedWith("Amount is less than minStakeValue"); + await expect(tokenPool.stake(1)).to.be.revertedWith("Pool: stake value is too low"); }); - it("Should allow unstaking", async function () { - const stake = 1000; + }); + + describe("Unstaking", function () { + const stake = 1000; + + beforeEach(async function () { + await token.approve(tokenPool.address, 1000000000); await tokenPool.stake(stake); - await tokenPool.unstake(stake); + }); + + it("Should allow unstaking with rewards", async function () { + await time.increase(D1); + await tokenPool.onBlock(); + + await expect(await tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); expect(await tokenPool.totalStake()).to.equal(0); expect(await tokenPool.getStake(owner.address)).to.equal(0); }); + it("Should allow fast unstaking with rewards", async function () { + await time.increase(D1); + await tokenPool.onBlock(); + + const balanceBefore = await token.balanceOf(owner.address); + await tokenPool.unstakeFast(stake); + const balanceAfter = await token.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); + }); + + it("Should allow unstaking without rewards", async function () { + await expect(tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); + }); + + it("Should allow fast unstaking without rewards", async function () { + const balanceBefore = await token.balanceOf(owner.address); + await tokenPool.unstakeFast(stake); + const balanceAfter = await token.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); + }); + it("Should not allow unstaking more than staked", async function () { - const stake = 1000; - await tokenPool.stake(stake); await expect(tokenPool.unstake(stake * 2)).to.be.revertedWith("Not enough stake"); }); + + it("Should not allow fast unstaking more than staked", async function () { + const balanceBefore = await token.balanceOf(owner.address); + await tokenPool.unstakeFast(stake); + const balanceAfter = await token.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); + + }); + + it("Should allow unstaking when pool is deactivated", async function () { + await tokenPool.deactivate(); + await expect(tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); + }); + + it("Should allow fast unstaking when pool is deactivated", async function () { + await tokenPool.deactivate(); + const balanceBefore = await token.balanceOf(owner.address); + await tokenPool.unstakeFast(stake); + const balanceAfter = await token.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); + }); }); describe("Rewards", function () { @@ -95,20 +161,33 @@ describe("TokenPool", function () { }); it("Should allow claiming rewards", async function () { - const stake = 1000; - await tokenPool.stake(stake); + await tokenPool.stake(1000); - const rewardBefore = await tokenPool.getReward(owner.address); - console.log("rewardBefore", rewardBefore.toString()); + // Wait for 1 day + await time.increase(D1); + await tokenPool.onBlock(); + + const expectedReward = 100; + const rewards = await tokenPool.getUserRewards(owner.address); + expect (rewards).to.equal(expectedReward); + + const balanceBefore = await token.balanceOf(owner.address); + await tokenPool.claim(); + const balanceAfter = await token.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(100); + }); + + it("Should allow claiming rewards when pool is deactivated", async function () { + await tokenPool.stake(1000); // Wait for 1 day - const futureTime = Math.floor(Date.now() / 1000) + 24 * 60 * 60; - await network.provider.send("evm_setNextBlockTimestamp", [futureTime]); - await network.provider.send("evm_mine"); // Mine a block to apply the new timestamp + await time.increase(D1); + await tokenPool.onBlock(); + + await tokenPool.deactivate(); const expectedReward = 100; - const rewards = await tokenPool.getReward(owner.address); - console.log("reward", rewards.toString()); + const rewards = await tokenPool.getUserRewards(owner.address); expect (rewards).to.equal(expectedReward); const balanceBefore = await token.balanceOf(owner.address); @@ -117,5 +196,8 @@ describe("TokenPool", function () { expect(balanceAfter.sub(balanceBefore)).to.equal(100); }); }); + + //TODO: Initialize for coverage + //TODO: Sets for coverage??? }); From e9aaef7e4f0693275be9a2563ef7c37de9ec972a Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Tue, 3 Sep 2024 13:05:51 +0300 Subject: [PATCH 22/60] Fix token pools manager --- contracts/staking/token/TokenPoolsManager.sol | 17 +++++++++---- scripts/staking/deploy_tokenPoolsManager.ts | 5 ++-- test/staking/token/TokenPoolsManager.ts | 24 +++++++++++++------ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 9f17ca21..6469a59b 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -6,14 +6,19 @@ import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./TokenPool.sol"; import "../../funds/RewardsBank.sol"; +import "../../LockKeeper.sol"; + +import "hardhat/console.sol"; contract TokenPoolsManager is AccessControl{ + LockKeeper lockKeeper; RewardsBank public bank; UpgradeableBeacon public beacon; mapping(string => address) public pools; - constructor(RewardsBank bank_, UpgradeableBeacon beacon_) { + constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon beacon_) { + lockKeeper = lockKeeper_; bank = bank_; beacon = beacon_; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); @@ -26,13 +31,15 @@ contract TokenPoolsManager is AccessControl{ // OWNER METHODS function createPool( - string memory name, address token_, uint interest_, uint interestRate_, - uint minStakeValue_, address rewardToken_, uint rewardTokenPrice_ + address token_, string memory name, uint minStakeValue_, + uint fastUnstakePenalty_, uint interest_, uint interestRate_, uint lockPeriod_, address rewardToken_, uint rewardTokenPrice_ ) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + console.log("Entered createPool"); bytes memory data = abi.encodeWithSignature( - "initialize(string,address,address,uint256,uint256,uint256,address,uint256)", - name, token_, bank, interest_, interestRate_, minStakeValue_, rewardToken_, rewardTokenPrice_); + "initialize(address,address,address,string,uint256,uint256,uint256,uint256,uint256,address,uint256)", + token_, bank, lockKeeper, name, minStakeValue_, fastUnstakePenalty_, interest_, interestRate_, lockPeriod_, rewardToken_, rewardTokenPrice_); address pool = address(new BeaconProxy(address(beacon), data)); + console.log("Pool created at address: %s", pool); pools[name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); emit PoolCreated(name, pool); diff --git a/scripts/staking/deploy_tokenPoolsManager.ts b/scripts/staking/deploy_tokenPoolsManager.ts index 0e582608..d2ee5074 100644 --- a/scripts/staking/deploy_tokenPoolsManager.ts +++ b/scripts/staking/deploy_tokenPoolsManager.ts @@ -7,8 +7,6 @@ import { TokenPoolsManager__factory, Multisig__factory, RewardsBank__factory, - TokenPoolBeacon__factory, - TokenPool__factory, } from "../../typechain-types"; import {Roadmap2023MultisigSettings} from "../addresses"; export async function main() { @@ -16,7 +14,6 @@ export async function main() { const [deployer] = await ethers.getSigners(); - const masterMultisig = loadDeployment(ContractNames.MasterMultisig, chainId).address; const multisig = await deploy({ @@ -49,6 +46,8 @@ export async function main() { await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); await (await poolsManager.grantRole(await poolsManager.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); + + //TODO: Rewoke roles from deployer } if (require.main === module) { diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index 58e9b2ee..4083a0c3 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -1,14 +1,14 @@ import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { ethers, upgrades } from "hardhat"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { - TokenPool, TokenPoolsManager, RewardsBank, AirBond__factory, RewardsBank__factory, TokenPoolsManager__factory, - TokenPoolBeacon__factory, + LockKeeper__factory, } from "../../../typechain-types"; import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; @@ -29,7 +29,9 @@ describe("PoolsManager", function () { const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); - const poolsManager = await new TokenPoolsManager__factory(owner).deploy(rewardsBank.address, tokenPoolBeacon.address); + const lockKeeper = await new LockKeeper__factory(owner).deploy(); + + const poolsManager = await new TokenPoolsManager__factory(owner).deploy(rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); const tokenAddr = airBond.address; @@ -43,23 +45,31 @@ describe("PoolsManager", function () { describe("Pool Management", function () { it("Should allow the owner to create a pool", async function () { + const minStakeValue = 10; + const fastUnstakePenalty = 100000; // 10% const interest = 100000; // 10% const interestRate = 24 * 60 * 60; // 24 hours - const minStakeValue = 10; + const lockPeriod = 24 * 60 * 60; // 24 hours + const rewardsTokenPrice = 1; - const tx = await poolsManager.createPool("TestProxy", tokenAddr, interest, interestRate, minStakeValue, tokenAddr, 1); + console.log("before createPool"); + const tx = await poolsManager.createPool(tokenAddr, "TestProxy", minStakeValue, fastUnstakePenalty, interest, interestRate, lockPeriod, tokenAddr, rewardsTokenPrice); const receipt = await tx.wait(); + console.log("Receipt: ", receipt); const poolAddress = receipt.events![4].args![1]; expect(await poolsManager.getPoolAddress("TestProxy")).to.equal(poolAddress); }); it("Should activate and deactivate a pool", async function () { + const minStakeValue = 10; + const fastUnstakePenalty = 100000; // 10% const interest = 100000; // 10% const interestRate = 24 * 60 * 60; // 24 hours - const minStakeValue = 10; + const lockPeriod = 24 * 60 * 60; // 24 hours + const rewardsTokenPrice = 1; - await poolsManager.createPool("TestProxy", tokenAddr, interest, interestRate, minStakeValue, tokenAddr, 1); + await poolsManager.createPool(tokenAddr, "TestProxy", minStakeValue, fastUnstakePenalty, interest, interestRate, lockPeriod, tokenAddr, rewardsTokenPrice); const poolAddress = await poolsManager.getPoolAddress("TestProxy"); console.log("Pool Address: ", poolAddress); From 85eb1802814f0d69f27c48ac1c3fd1a93898e54a Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Tue, 3 Sep 2024 13:53:09 +0300 Subject: [PATCH 23/60] Update deploy script --- scripts/staking/deploy_tokenPoolsManager.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/staking/deploy_tokenPoolsManager.ts b/scripts/staking/deploy_tokenPoolsManager.ts index d2ee5074..8a4f7d6d 100644 --- a/scripts/staking/deploy_tokenPoolsManager.ts +++ b/scripts/staking/deploy_tokenPoolsManager.ts @@ -7,6 +7,7 @@ import { TokenPoolsManager__factory, Multisig__factory, RewardsBank__factory, + LockKeeper__factory } from "../../typechain-types"; import {Roadmap2023MultisigSettings} from "../addresses"; export async function main() { @@ -31,6 +32,17 @@ export async function main() { signer: deployer, loadIfAlreadyDeployed: true, }); + + const lockKeeper = await deploy({ + contractName: ContractNames.LockKeeper, + artifactName: "LockKeeper", + deployArgs: [], + signer: deployer, + loadIfAlreadyDeployed: true, + isUpgradeableProxy: true, + }); + + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); @@ -38,7 +50,7 @@ export async function main() { const poolsManager = await deploy({ contractName: ContractNames.TokenPoolsManager, artifactName: "TokenPoolsManager", - deployArgs: [rewardsBank.address, tokenPoolBeacon.address], + deployArgs: [rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address], signer: deployer, loadIfAlreadyDeployed: true, }); From 7bc326fca4a82839a1c1a85445814acb1d915161 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 4 Sep 2024 14:29:20 +0300 Subject: [PATCH 24/60] Update deploy script --- package.json | 1 + .../{deploy_tokenPoolsManager.ts => deploy_token_staking.ts} | 0 2 files changed, 1 insertion(+) rename scripts/staking/{deploy_tokenPoolsManager.ts => deploy_token_staking.ts} (100%) diff --git a/package.json b/package.json index 8c9d9bba..44420bcb 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "deploy_servernodes_manager": "hardhat run scripts/staking/deploy_servernodes_manager.ts --network dev", "deploy_legacy_pool_manager": "hardhat run scripts/staking/deploy_legacy_pool_manager.ts --network dev", "deploy_liquid_staking": "hardhat run scripts/staking/deploy_liquid_staking.ts --network dev", + "deploy_token_staking": "hardhat run scripts/staking/deploy_token_staking.ts --network dev", "deploy_all": "npm run deploy_multisig && npm run deploy_finance && npm run deploy_airdrop && npm run deploy_staking && npm run deploy_fees && npm run deploy_multisig:ecosystem && npm run deploy_bond", "migration_to_new_staking": "hardhat run scripts/staking/migrate_to_new_staking.ts --network dev", "deploy_fees": "hardhat run scripts/fees/deploy_fees.ts --network dev", diff --git a/scripts/staking/deploy_tokenPoolsManager.ts b/scripts/staking/deploy_token_staking.ts similarity index 100% rename from scripts/staking/deploy_tokenPoolsManager.ts rename to scripts/staking/deploy_token_staking.ts From a85a93f9e15265951e50bb75810c5044793d0332 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Fri, 6 Sep 2024 14:03:01 +0300 Subject: [PATCH 25/60] Update deploy script --- package.json | 2 +- scripts/ecosystem/token_staking/deploy.ts | 69 +++++++++++++++++++++++ src/contracts/names.ts | 12 ++-- 3 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 scripts/ecosystem/token_staking/deploy.ts diff --git a/package.json b/package.json index f7da2397..e43efb5c 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "deploy_astradex_tokensafe": "hardhat run scripts/ecosystem/astradex/deployTokensSafe.ts --network dev", "deploy_government": "hardhat run scripts/ecosystem/government/deploy.ts --network dev", "deploy_liquid_staking": "hardhat run scripts/ecosystem/liquid_staking/deploy.ts --network dev", - "deploy_token_staking": "hardhat run scripts/staking/deploy_token_staking.ts --network dev", + "deploy_token_staking": "hardhat run scripts/ecosystem/token_staking/deploy.ts --network dev", "sourcify:dev": "hardhat sourcify --network dev", diff --git a/scripts/ecosystem/token_staking/deploy.ts b/scripts/ecosystem/token_staking/deploy.ts new file mode 100644 index 00000000..13efbdb0 --- /dev/null +++ b/scripts/ecosystem/token_staking/deploy.ts @@ -0,0 +1,69 @@ +import { ethers, upgrades } from "hardhat"; +import { deploy, loadDeployment } from "@airdao/deployments/deploying"; + +import { ContractNames } from "../../../src"; + +import { + TokenPoolsManager__factory, + Multisig__factory, + RewardsBank__factory, + LockKeeper__factory +} from "../../../typechain-types"; + +import { deployMultisig } from "../../utils/deployMultisig"; + +export async function main() { + const { chainId } = await ethers.provider.getNetwork(); + + const [deployer] = await ethers.getSigners(); + + const multisig = await deployMultisig(ContractNames.Ecosystem_TokenPoolsManagerMultisig, deployer); + + const rewardsBank = await deploy({ + contractName: ContractNames.Ecosystem_TokenPoolsManagerRewardsBank, + artifactName: "RewardsBank", + deployArgs: [], + signer: deployer, + loadIfAlreadyDeployed: true, + }); + + const lockKeeper = await deploy({ + contractName: ContractNames.LockKeeper, + artifactName: "LockKeeper", + deployArgs: [], + signer: deployer, + loadIfAlreadyDeployed: true, + isUpgradeableProxy: true, + }); + + + + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); + const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); + + const poolsManager = await deploy({ + contractName: ContractNames.Ecosystem_TokenPoolsManager, + artifactName: "TokenPoolsManager", + deployArgs: [rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address], + signer: deployer, + loadIfAlreadyDeployed: true, + }); + + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); + await (await poolsManager.grantRole(await poolsManager.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); + + if (chainId != 16718) return; // continue only on prod + + console.log("Reworking roles from deployer"); + await (await rewardsBank.revokeRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), deployer.address)).wait(); + await (await poolsManager.revokeRole(await poolsManager.DEFAULT_ADMIN_ROLE(), deployer.address)).wait(); + +} + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/src/contracts/names.ts b/src/contracts/names.ts index 9a910f06..1ee75c40 100644 --- a/src/contracts/names.ts +++ b/src/contracts/names.ts @@ -46,12 +46,6 @@ export enum ContractNames { Treasury = "Treasury", TreasuryMultisig = "Treasury_Multisig", - TokenPool = "TokenPool", - TokenPoolBeacon = "TokenPoolBeacon", - TokenPoolsManager = "TokenPoolsManager", - TokenPoolsManagerMultisig = "TokenPoolsManager_Multisig", - TokenPoolsManagerRewardsBank = "TokenPoolsManager_RewardsBank", - // funds AirBond = "AirBond", @@ -90,6 +84,12 @@ export enum ContractNames { Ecosystem_LiquidNodesManagerTreasuryFees = "Ecosystem_LiquidNodesManager_TreasuryFees", Ecosystem_LiquidPoolStakingTiers = "Ecosystem_LiquidPool_StakingTiers", + Ecosystem_TokenPool = "Ecosystem_TokenPool", + Ecosystem_TokenPoolBeacon = "Ecosystem_TokenPoolBeacon", + Ecosystem_TokenPoolsManager = "Ecosystem_TokenPoolsManager", + Ecosystem_TokenPoolsManagerMultisig = "Ecosystem_TokenPoolsManager_Multisig", + Ecosystem_TokenPoolsManagerRewardsBank = "Ecosystem_TokenPoolsManager_RewardsBank", + Ecosystem_GovernmentMultisig = "Ecosystem_Government_Multisig", } From bfcf7c2b419c7e06b356158e48882cfa9105a521 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Fri, 6 Sep 2024 14:45:01 +0300 Subject: [PATCH 26/60] Deploy to devnet --- .openzeppelin/unknown-30746.json | 262 ++++++++++++++++++++++ deployments/30746.json | 62 ++++- scripts/ecosystem/token_staking/deploy.ts | 4 +- 3 files changed, 325 insertions(+), 3 deletions(-) diff --git a/.openzeppelin/unknown-30746.json b/.openzeppelin/unknown-30746.json index 609ecaab..28a24401 100644 --- a/.openzeppelin/unknown-30746.json +++ b/.openzeppelin/unknown-30746.json @@ -675,6 +675,268 @@ } } } + }, + "4dba536864bac2e18b3c549e5476fda8ed690a7699e8bb7d2942ee988c71eaa3": { + "address": "0x38742FA88FD3b16318Ba64F4747E41954e9FFc0e", + "txHash": "0x5a2a82687088b88524dd42f42f727739a95ff7e2ab183603dda689f6be3b543c", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "_roles", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(RoleData)2225_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:56" + }, + { + "label": "token", + "offset": 0, + "slot": "2", + "type": "t_contract(IERC20)3439", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:16" + }, + { + "label": "rewardsBank", + "offset": 0, + "slot": "3", + "type": "t_contract(RewardsBank)6551", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:17" + }, + { + "label": "lockKeeper", + "offset": 0, + "slot": "4", + "type": "t_contract(LockKeeper)6343", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:18" + }, + { + "label": "name", + "offset": 0, + "slot": "5", + "type": "t_string_storage", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:20" + }, + { + "label": "minStakeValue", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:21" + }, + { + "label": "fastUnstakePenalty", + "offset": 0, + "slot": "7", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:22" + }, + { + "label": "interest", + "offset": 0, + "slot": "8", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:23" + }, + { + "label": "interestRate", + "offset": 0, + "slot": "9", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:24" + }, + { + "label": "lockPeriod", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:25" + }, + { + "label": "rewardToken", + "offset": 0, + "slot": "11", + "type": "t_address", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:26" + }, + { + "label": "rewardTokenPrice", + "offset": 0, + "slot": "12", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:27" + }, + { + "label": "active", + "offset": 0, + "slot": "13", + "type": "t_bool", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:29" + }, + { + "label": "totalStake", + "offset": 0, + "slot": "14", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:30" + }, + { + "label": "totalRewards", + "offset": 0, + "slot": "15", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:31" + }, + { + "label": "lastInterestUpdate", + "offset": 0, + "slot": "16", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:33" + }, + { + "label": "totalRewardsDebt", + "offset": 0, + "slot": "17", + "type": "t_uint256", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:34" + }, + { + "label": "stakes", + "offset": 0, + "slot": "18", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:36" + }, + { + "label": "rewardsDebt", + "offset": 0, + "slot": "19", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:37" + }, + { + "label": "claimableRewards", + "offset": 0, + "slot": "20", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:38" + }, + { + "label": "lockedWithdrawals", + "offset": 0, + "slot": "21", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:39" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IERC20)3439": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(LockKeeper)6343": { + "label": "contract LockKeeper", + "numberOfBytes": "20" + }, + "t_contract(RewardsBank)6551": { + "label": "contract RewardsBank", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)2225_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoleData)2225_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/deployments/30746.json b/deployments/30746.json index 6973f854..b91f605b 100644 --- a/deployments/30746.json +++ b/deployments/30746.json @@ -1661,5 +1661,65 @@ "implementation": "0x638d7ABE51CE0FFddDb382caAfAfb32cdfb9aa84", "fullyQualifiedName": "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy" } + }, + "Ecosystem_TokenPoolsManager_Multisig": { + "address": "0xF7c8f345Ac1d29F13c16d8Ae34f534D9056E3FF2", + "abi": [ + "constructor(address[] _signers, bool[] isInitiatorFlags, uint256 _threshold, address owner)", + "event Confirmation(address indexed sender, uint256 indexed txId)", + "event Execution(uint256 indexed txId)", + "event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)", + "event Revocation(address indexed sender, uint256 indexed txId)", + "event SignerAddition(address indexed signer, bool isInitiator)", + "event SignerRemoval(address indexed signer)", + "event Submission(uint256 indexed txId)", + "event ThresholdChange(uint256 required)", + "function changeSigners(address[] signersToRemove, address[] signersToAdd, bool[] isInitiatorFlags)", + "function changeThreshold(uint256 _threshold)", + "function checkBeforeSubmitTransaction(address destination, uint256 value, bytes data) payable", + "function confirmTransaction(uint256 txId)", + "function confirmations(uint256, address) view returns (bool)", + "function getConfirmations(uint256 txId) view returns (address[])", + "function getInitiatorsCount() view returns (uint256)", + "function getRequiredSignersCount() view returns (uint256)", + "function getSigners() view returns (address[], bool[])", + "function getTransactionData(uint256 txId) view returns (tuple(address destination, uint256 value, bytes data, bool executed), address[])", + "function getTransactionIds(uint256 from, uint256 to, bool pending, bool executed) view returns (uint256[] result)", + "function isConfirmed(uint256 txId) view returns (bool)", + "function isInitiator(address) view returns (bool)", + "function isSigner(address) view returns (bool)", + "function owner() view returns (address)", + "function renounceOwnership()", + "function revokeConfirmation(uint256 txId)", + "function signers(uint256) view returns (address)", + "function submitTransaction(address destination, uint256 value, bytes data) payable returns (uint256 txId)", + "function threshold() view returns (uint256)", + "function transactionCount() view returns (uint256)", + "function transactions(uint256) view returns (address destination, uint256 value, bytes data, bool executed)", + "function transferOwnership(address newOwner)", + "function withdraw(address to, uint256 amount)" + ], + "deployTx": "0xbd191d729a142b5c69cba31f0ce5154b5b91722444e14b89b68b2fba88b699f6", + "fullyQualifiedName": "contracts/multisig/Multisig.sol:Multisig" + }, + "Ecosystem_TokenPoolsManager_RewardsBank": { + "address": "0x92f47Ee54B8320A4E74Ddb256dc6e9129bCFD053", + "abi": [ + "constructor()", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function grantRole(bytes32 role, address account)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function withdrawAmb(address addressTo, uint256 amount)", + "function withdrawErc20(address tokenAddress, address addressTo, uint256 amount)" + ], + "deployTx": "0x9ed3d31b3b7615386da8defd5f0ce2ba6a0f209479bded528fc73989765472c1", + "fullyQualifiedName": "contracts/funds/RewardsBank.sol:RewardsBank" } -} +} \ No newline at end of file diff --git a/scripts/ecosystem/token_staking/deploy.ts b/scripts/ecosystem/token_staking/deploy.ts index 13efbdb0..8fa7a4b4 100644 --- a/scripts/ecosystem/token_staking/deploy.ts +++ b/scripts/ecosystem/token_staking/deploy.ts @@ -35,11 +35,11 @@ export async function main() { loadIfAlreadyDeployed: true, isUpgradeableProxy: true, }); - - + console.log("deploying TokenPool Beacon"); const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); + console.log("TokenPool Beacon deployed to:", tokenPoolBeacon.address); const poolsManager = await deploy({ contractName: ContractNames.Ecosystem_TokenPoolsManager, From ea54624a8339707b2ea8c5986b43acadc34305b2 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 11 Sep 2024 16:14:53 +0300 Subject: [PATCH 27/60] Add double side pool --- contracts/staking/token/DoubleSidePool.sol | 574 +++++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 contracts/staking/token/DoubleSidePool.sol diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol new file mode 100644 index 00000000..773d8a15 --- /dev/null +++ b/contracts/staking/token/DoubleSidePool.sol @@ -0,0 +1,574 @@ +//SPDX-License-Identifier: UNCLICENSED +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../../funds/RewardsBank.sol"; +import "../../LockKeeper.sol"; + +import "hardhat/console.sol"; + +//The side defined by the address of the token. Zero address means native coin +contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { + + struct MainSideConfig { + address token; + address rewardToken; + uint rewardTokenPrice; + uint minStakeValue; + uint unstakeLockPeriod; + uint fastUnstakePenalty; + uint lockPeriod; + uint interest; + uint interestRate; + } + + struct DependantSideConfig { + address token; + address rewardToken; + uint rewardTokenPrice; + uint minStakeValue; + uint unstakeLockPeriod; // Time in seconds to how long the amount is locker after unstake + uint fastUnstakePenalty; + uint lockPeriod; + uint interest; + uint interestRate; + uint maxTotalStakeValue; + uint maxStakePerUserValue; + uint stakeLockPeriod; // Time in seconds to how long the stake is locker before unstake + uint stakeLimitsMultiplier; + } + + struct SideInfo { + uint totalStake; + uint totalRewards; + uint lastInterestUpdate; + uint totalRewardsDebt; + } + + struct SideStaker { + uint stake; + uint rewardsDebt; + uint claimableRewards; + uint lockedWithdrawal; + uint stakedAt; + } + + uint constant public MILLION = 1_000_000; + + LockKeeper public lockKeeper; + RewardsBank public rewardsBank; + + string public name; + bool public active; + bool public hasSecondSide; + + MainSideConfig public mainSideConfig; + SideInfo public mainSideInfo; + DependantSideConfig public dependantSideConfig; + SideInfo public dependantSideInfo; + + mapping(address => SideStaker) public mainSideStakers; + mapping(address => SideStaker) public dependantSideStakers; + + //EVENTS + + event Deactivated(); + event Activated(); + event MinStakeValueChanged(bool dependant, uint minStakeValue); + event UnstakeLockePeriodChanged(bool dependant, uint period); + event RewardTokenPriceChanged(bool dependant, uint price); + event FastUnstakePenaltyChanged(bool dependant, uint penalty); + event InterestChanged(bool dependant, uint interest); + event InterestRateChanged(bool dependant, uint interestRate); + + event PoolMaxStakeValueChanged(uint poolMaxStakeValue); + event StakeLockPeriodChanged(uint period); + event StakeLimitsMultiplierChanged(uint value); + event ApyBonusMultiplierChanged(uint value); + + event StakeChanged(bool dependant, address indexed user, uint amount); + event Claim(bool dependant, address indexed user, uint amount); + event Interest(bool dependant, uint amount); + event UnstakeLocked(bool dependant, address indexed user, uint amount, uint unlockTime, uint creationTime); + event UnstakeFast(bool dependant, address indexed user, uint amount, uint penalty); + + // TODO: Add more dependant side specific events + + function initialize( + RewardsBank rewardsBank_, LockKeeper lockkeeper_, string memory name_, + MainSideConfig memory mainSideConfig_ + ) public initializer { + lockKeeper = lockkeeper_; + rewardsBank = rewardsBank_; + name = name_; + active = true; + hasSecondSide = false; + + mainSideConfig = mainSideConfig_; + + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + // OWNER METHODS + + function addDependantSide(DependantSideConfig memory dependantSideConfig_) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(!hasSecondSide, "Second side already exists"); + hasSecondSide = true; + dependantSideConfig = dependantSideConfig_; + } + + function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { + require(!active, "Pool is already active"); + active = true; + emit Activated(); + } + + function deactivate() public onlyRole(DEFAULT_ADMIN_ROLE) { + require(active, "Pool is not active"); + active = false; + emit Deactivated(); + } + + // SETTERS FOR PARAMS + + function setMinStakeValue(bool dependant, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (dependant) { + dependantSideConfig.minStakeValue = value; + } else { + mainSideConfig.minStakeValue = value; + } + emit MinStakeValueChanged(dependant, value); + } + + function setInterest(bool dependant, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (dependant) { + dependantSideConfig.interest = _interest; + dependantSideConfig.interestRate = _interestRate; + } else { + mainSideConfig.interest = _interest; + mainSideConfig.interestRate = _interestRate; + } + emit InterestRateChanged(dependant, _interestRate); + emit InterestChanged(dependant, _interest); + } + + function setUnstakeLockPeriod(bool dependant, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (dependant) { + dependantSideConfig.unstakeLockPeriod = period; + } else { + mainSideConfig.unstakeLockPeriod = period; + } + emit UnstakeLockePeriodChanged(dependant, period); + } + + function setRewardTokenPrice(bool dependant, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (dependant) { + dependantSideConfig.rewardTokenPrice = price; + } else { + mainSideConfig.rewardTokenPrice = price; + } + emit RewardTokenPriceChanged(dependant, price); + } + + function setFastUnstakePenalty(bool dependant, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (dependant) { + dependantSideConfig.fastUnstakePenalty = penalty; + } else { + mainSideConfig.fastUnstakePenalty = penalty; + } + emit FastUnstakePenaltyChanged(dependant, penalty); + } + + function setMaxTotalStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + dependantSideConfig.maxTotalStakeValue = value; + emit PoolMaxStakeValueChanged(value); + } + + function setStakeLimitsMultiplier(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + dependantSideConfig.stakeLimitsMultiplier = value; + emit StakeLimitsMultiplierChanged(value); + } + + function setStakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + dependantSideConfig.stakeLockPeriod = period; + emit StakeLockPeriodChanged(period); + } + + //TODO: Add more setters + + // PUBLIC METHODS + + function stake(bool dependant, uint amount) public { + if (dependant) { + _stakeDependantSide(msg.sender, amount); + } else { + _stakeMainSide(msg.sender, amount); + } + } + + function unstake(bool dependant, uint amount) public { + if (dependant) { + _unstakeDependantSide(msg.sender, amount); + } else { + _unstakeMainSide(msg.sender, amount); + } + } + + function unstakeFast(bool dependant, uint amount) public { + if (dependant) { + _unstakeFastDependantSide(msg.sender, amount); + } else { + _unstakeFastMainSide(msg.sender, amount); + } + } + + function claim(bool dependant) public { + if (dependant) { + _calcClaimableRewards(true, msg.sender); + _claimRewards(true, msg.sender); + } else { + _calcClaimableRewards(false, msg.sender); + _claimRewards(false, msg.sender); + } + } + + function onBlock() external { + _addInterestMainSide(); + _addInterestDependantSide(); + } + + // VIEW METHODS + + function getMainSideConfig() public view returns (MainSideConfig memory) { + return mainSideConfig; + } + + function getMainSideInfo() public view returns (SideInfo memory) { + return mainSideInfo; + } + + function getMainSideStaker(address user) public view returns (SideStaker memory) { + return mainSideStakers[user]; + } + + function getDependantSideConfig() public view returns (DependantSideConfig memory) { + return dependantSideConfig; + } + + function getDependantSideInfo() public view returns (SideInfo memory) { + return dependantSideInfo; + } + + function getDependantSideStaker(address user) public view returns (SideStaker memory) { + return dependantSideStakers[user]; + } + + function getUserMainSideRewards(address user) public view returns (uint) { + uint rewardsAmount = _calcRewards(false, mainSideStakers[user].stake); + if (rewardsAmount + mainSideStakers[user].claimableRewards <= mainSideStakers[user].rewardsDebt) + return 0; + + return rewardsAmount + mainSideStakers[user].claimableRewards - mainSideStakers[user].rewardsDebt; + } + + function getUserDependantSideRewards(address user) public view returns (uint) { + uint rewardsAmount = _calcRewards(false, dependantSideStakers[user].stake); + if (rewardsAmount + dependantSideStakers[user].claimableRewards <= dependantSideStakers[user].rewardsDebt) + return 0; + + return rewardsAmount + dependantSideStakers[user].claimableRewards - dependantSideStakers[user].rewardsDebt; + } + + // INTERNAL METHODS + + // MAIN SIDE METHODS + + function _addInterestMainSide() internal { + if (mainSideInfo.lastInterestUpdate + mainSideConfig.interestRate > block.timestamp) return; + uint timePassed = block.timestamp - mainSideInfo.lastInterestUpdate; + uint newRewards = mainSideInfo.totalStake * mainSideConfig.interest * timePassed / MILLION / mainSideConfig.interestRate; + + mainSideInfo.totalRewards += newRewards; + mainSideInfo.lastInterestUpdate = block.timestamp; + emit Interest(false, newRewards); + } + + + function _stakeMainSide(address user, uint amount) internal { + require(active, "Pool is not active"); + require(amount >= mainSideConfig.minStakeValue, "Pool: stake value is too low"); + if (msg.value != 0) { + require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); + require(msg.value == amount, "Pool: wrong amount of native coin"); + } else { + SafeERC20.safeTransferFrom(IERC20(mainSideConfig.token), msg.sender, address(this), amount); + } + + uint rewardsAmount = _calcRewards(false, amount); + + mainSideStakers[msg.sender].stake += amount; + mainSideStakers[msg.sender].stakedAt = block.timestamp; + mainSideInfo.totalStake += amount; + + mainSideInfo.totalRewards += rewardsAmount; + + _updateRewardsDebt(false, user, _calcRewards(false, mainSideStakers[user].stake)); + emit StakeChanged(false, msg.sender, amount); + } + + + function _unstakeMainSide(address user, uint amount) internal { + require(mainSideStakers[msg.sender].stake >= amount, "Not enough stake"); + + uint rewardsAmount = _calcRewards(false, amount); + + mainSideStakers[msg.sender].stake -= amount; + mainSideInfo.totalStake -= amount; + + mainSideInfo.totalRewards -= rewardsAmount; + _updateRewardsDebt(false, user, _calcRewards(false, mainSideStakers[user].stake)); + + // cancel previous lock (if exists). canceledAmount will be added to new lock + uint canceledAmount; + if (lockKeeper.getLock(mainSideStakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists + canceledAmount = lockKeeper.cancelLock(mainSideStakers[msg.sender].lockedWithdrawal); + + if (mainSideConfig.token == address(0)) { + payable(msg.sender).transfer(amount - canceledAmount); + } else { + IERC20(mainSideConfig.token).approve(address(lockKeeper), amount - canceledAmount); + } + + // lock funds + mainSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( + msg.sender, address(mainSideConfig.token), uint64(block.timestamp + mainSideConfig.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainSideConfig.token)))) + ); + + _claimRewards(false, msg.sender); + + emit UnstakeLocked(false, msg.sender, amount + canceledAmount, block.timestamp + mainSideConfig.lockPeriod, block.timestamp); + emit StakeChanged(false, msg.sender, mainSideStakers[msg.sender].stake); + } + + + function _unstakeFastMainSide(address user, uint amount) internal { + require(mainSideStakers[msg.sender].stake >= amount, "Not enough stake"); + + uint rewardsAmount = _calcRewards(false, amount); + + mainSideStakers[msg.sender].stake -= amount; + mainSideInfo.totalStake -= amount; + + mainSideInfo.totalRewards -= rewardsAmount; + _updateRewardsDebt(false, user, _calcRewards(false, mainSideStakers[user].stake)); + + uint penalty = amount * mainSideConfig.fastUnstakePenalty / MILLION; + if (mainSideConfig.token == address(0)) { + payable(msg.sender).transfer(amount - penalty); + } else { + SafeERC20.safeTransfer(IERC20(mainSideConfig.token), msg.sender, amount - penalty); + } + + _claimRewards(false, msg.sender); + + emit UnstakeFast(false, msg.sender, amount, penalty); + emit StakeChanged(false, msg.sender, amount); + } + + // DEPENDANT SIDE METHODS + + function _addInterestDependantSide() internal { + if (dependantSideInfo.lastInterestUpdate + dependantSideConfig.interestRate > block.timestamp) return; + uint timePassed = block.timestamp - dependantSideInfo.lastInterestUpdate; + uint newRewards = dependantSideInfo.totalStake * dependantSideConfig.interest * timePassed / MILLION / dependantSideConfig.interestRate; + + dependantSideInfo.totalRewards += newRewards; + dependantSideInfo.lastInterestUpdate = block.timestamp; + emit Interest(true, newRewards); + } + + function _stakeDependantSide(address user, uint amount) internal { + require(active, "Pool is not active"); + require(amount >= dependantSideConfig.minStakeValue, "Pool: stake value is too low"); + if (msg.value != 0) { + require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); + require(msg.value == amount, "Pool: wrong amount of native coin"); + } else { + SafeERC20.safeTransferFrom(IERC20(mainSideConfig.token), msg.sender, address(this), amount); + } + + uint rewardsAmount = _calcRewards(true, amount); + + dependantSideStakers[msg.sender].stake += amount; + dependantSideInfo.totalStake += amount; + + require(dependantSideInfo.totalStake <= dependantSideConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); + require(dependantSideStakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); + require(dependantSideStakers[msg.sender].stake <= dependantSideConfig.maxStakePerUserValue, "Pool: max stake per user exceeded"); + + dependantSideInfo.totalRewards += rewardsAmount; + + _updateRewardsDebt(true, user, _calcRewards(true, mainSideStakers[user].stake)); + emit StakeChanged(true, msg.sender, amount); + } + + function _unstakeDependantSide(address user, uint amount) internal { + require(dependantSideStakers[msg.sender].stake >= amount, "Not enough stake"); + require(block.timestamp - dependantSideStakers[msg.sender].stakedAt >= dependantSideConfig.stakeLockPeriod, "Stake is locked"); + + uint rewardsAmount = _calcRewards(true, amount); + + dependantSideStakers[msg.sender].stake -= amount; + dependantSideInfo.totalStake -= amount; + + dependantSideInfo.totalRewards -= rewardsAmount; + _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); + + // cancel previous lock (if exists). canceledAmount will be added to new lock + uint canceledAmount; + if (lockKeeper.getLock(dependantSideStakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists + canceledAmount = lockKeeper.cancelLock(dependantSideStakers[msg.sender].lockedWithdrawal); + + if (mainSideConfig.token == address(0)) { + payable(msg.sender).transfer(amount - canceledAmount); + } else { + IERC20(mainSideConfig.token).approve(address(lockKeeper), amount - canceledAmount); + } + + // lock funds + dependantSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( + msg.sender, address(mainSideConfig.token), uint64(block.timestamp + dependantSideConfig.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(dependantSideConfig.token)))) + ); + + _claimRewards(true, msg.sender); + + emit UnstakeLocked(true, msg.sender, amount + canceledAmount, block.timestamp + mainSideConfig.lockPeriod, block.timestamp); + emit StakeChanged(true, msg.sender, dependantSideStakers[msg.sender].stake); + + } + + function _unstakeFastDependantSide(address user, uint amount) internal { + require(dependantSideStakers[msg.sender].stake >= amount, "Not enough stake"); + require(block.timestamp - dependantSideStakers[msg.sender].stakedAt >= dependantSideConfig.stakeLockPeriod, "Stake is locked"); + + uint rewardsAmount = _calcRewards(true, amount); + + dependantSideStakers[msg.sender].stake -= amount; + dependantSideInfo.totalStake -= amount; + + dependantSideInfo.totalRewards -= rewardsAmount; + _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); + + uint penalty = amount * dependantSideConfig.fastUnstakePenalty / MILLION; + if (dependantSideConfig.token == address(0)) { + payable(msg.sender).transfer(amount - penalty); + } else { + SafeERC20.safeTransfer(IERC20(dependantSideConfig.token), msg.sender, amount - penalty); + } + + _claimRewards(true, msg.sender); + + emit UnstakeFast(true, msg.sender, amount, penalty); + emit StakeChanged(true, msg.sender, amount); + } + + function _maxUserStakeValue(address user) internal view returns (uint) { + return mainSideStakers[user].stake * dependantSideConfig.stakeLimitsMultiplier / MILLION; + } + + //COMMON METHODS + + // store claimable rewards + function _calcClaimableRewards(bool dependant, address user) internal { + if (dependant) { + uint rewardsAmount = _calcRewards(dependant, dependantSideStakers[user].stake); + uint rewardsWithoutDebt = rewardsAmount - dependantSideStakers[user].rewardsDebt; + dependantSideStakers[user].claimableRewards += rewardsWithoutDebt; + dependantSideInfo.totalRewardsDebt += rewardsWithoutDebt; + dependantSideStakers[user].rewardsDebt += rewardsWithoutDebt; + } else { + uint rewardsAmount = _calcRewards(dependant, mainSideStakers[user].stake); + uint rewardsWithoutDebt = rewardsAmount - mainSideStakers[user].rewardsDebt; + mainSideStakers[user].claimableRewards += rewardsWithoutDebt; + mainSideInfo.totalRewardsDebt += rewardsWithoutDebt; + mainSideStakers[user].rewardsDebt += rewardsWithoutDebt; + } + } + + function _claimRewards(bool dependant, address user) internal { + if (dependant) { + uint amount = dependantSideStakers[user].claimableRewards; + if (amount == 0) return; + + dependantSideStakers[user].claimableRewards = 0; + + uint rewardTokenAmount = amount * dependantSideConfig.rewardTokenPrice; + if (dependantSideConfig.rewardToken == address(0)) { + rewardsBank.withdrawAmb(payable(user), amount); + } else { + rewardsBank.withdrawErc20(dependantSideConfig.rewardToken, payable(user), rewardTokenAmount); + } + emit Claim(true, user, rewardTokenAmount); + } else { + uint amount = mainSideStakers[user].claimableRewards; + if (amount == 0) return; + + mainSideStakers[user].claimableRewards = 0; + + uint rewardTokenAmount = amount * mainSideConfig.rewardTokenPrice; + if (mainSideConfig.rewardToken == address(0)) { + rewardsBank.withdrawAmb(payable(user), amount); + } else { + rewardsBank.withdrawErc20(mainSideConfig.rewardToken, payable(user), rewardTokenAmount); + } + emit Claim(false, user, rewardTokenAmount); + } + } + + function _calcRewards(bool dependant, uint amount) internal view returns (uint) { + if (dependant) { + if (dependantSideInfo.totalStake == 0 && dependantSideInfo.totalRewards == 0) return amount; + amount * dependantSideInfo.totalRewards / dependantSideInfo.totalStake; + } else { + if (mainSideInfo.totalStake == 0 && mainSideInfo.totalRewards == 0) return amount; + amount * mainSideInfo.totalRewards / mainSideInfo.totalStake; + } + } + + + function _updateRewardsDebt(bool dependant, address user, uint newDebt) internal { + if (dependant) { + uint oldDebt = dependantSideStakers[user].rewardsDebt; + if (newDebt < oldDebt) dependantSideInfo.totalRewardsDebt -= oldDebt - newDebt; + else dependantSideInfo.totalRewardsDebt += newDebt - oldDebt; + dependantSideStakers[user].rewardsDebt = newDebt; + } else { + uint oldDebt = mainSideStakers[user].rewardsDebt; + if (newDebt < oldDebt) mainSideInfo.totalRewardsDebt -= oldDebt - newDebt; + else mainSideInfo.totalRewardsDebt += newDebt - oldDebt; + mainSideStakers[user].rewardsDebt = newDebt; + } + } + + function _addressToString(address x) internal pure returns (string memory) { + bytes memory s = new bytes(40); + for (uint i = 0; i < 20; i++) { + uint8 b = uint8(uint(uint160(x)) / (2 ** (8 * (19 - i)))); + uint8 hi = (b / 16); + uint8 lo = (b - 16 * hi); + s[2 * i] = _char(hi); + s[2 * i + 1] = _char(lo); + } + return string(s); + } + + function _char(uint8 b) internal pure returns (bytes1 c) { + return bytes1(b + (b < 10 ? 0x30 : 0x57)); + } + +} From ee40061b600ee426718ec86d3b82d7840e38fbd1 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 11 Sep 2024 20:22:41 +0300 Subject: [PATCH 28/60] Update token pools manager --- contracts/staking/token/DoubleSidePool.sol | 21 +- .../{TokenPool.sol => SingleSidePool.sol} | 188 +++++++++--------- contracts/staking/token/TokenPoolsManager.sol | 77 +++++-- 3 files changed, 163 insertions(+), 123 deletions(-) rename contracts/staking/token/{TokenPool.sol => SingleSidePool.sol} (50%) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index 773d8a15..ec92d3d1 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -84,10 +84,10 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { event InterestChanged(bool dependant, uint interest); event InterestRateChanged(bool dependant, uint interestRate); - event PoolMaxStakeValueChanged(uint poolMaxStakeValue); + event MaxTotalStakeValueChanged(uint poolMaxStakeValue); + event MaxStakePerUserValueChanged(uint maxStakePerUserValue); event StakeLockPeriodChanged(uint period); event StakeLimitsMultiplierChanged(uint value); - event ApyBonusMultiplierChanged(uint value); event StakeChanged(bool dependant, address indexed user, uint amount); event Claim(bool dependant, address indexed user, uint amount); @@ -95,8 +95,6 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { event UnstakeLocked(bool dependant, address indexed user, uint amount, uint unlockTime, uint creationTime); event UnstakeFast(bool dependant, address indexed user, uint amount, uint penalty); - // TODO: Add more dependant side specific events - function initialize( RewardsBank rewardsBank_, LockKeeper lockkeeper_, string memory name_, MainSideConfig memory mainSideConfig_ @@ -114,9 +112,10 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { // OWNER METHODS - function addDependantSide(DependantSideConfig memory dependantSideConfig_) public onlyRole(DEFAULT_ADMIN_ROLE) { + function addDependentSide(bytes calldata prams) public onlyRole(DEFAULT_ADMIN_ROLE) { require(!hasSecondSide, "Second side already exists"); hasSecondSide = true; + DependantSideConfig memory dependantSideConfig_ = abi.decode(prams, (DependantSideConfig)); dependantSideConfig = dependantSideConfig_; } @@ -184,12 +183,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { function setMaxTotalStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { dependantSideConfig.maxTotalStakeValue = value; - emit PoolMaxStakeValueChanged(value); - } - - function setStakeLimitsMultiplier(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - dependantSideConfig.stakeLimitsMultiplier = value; - emit StakeLimitsMultiplierChanged(value); + emit MaxTotalStakeValueChanged(value); } function setStakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { @@ -197,6 +191,11 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { emit StakeLockPeriodChanged(period); } + function setStakeLimitsMultiplier(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + dependantSideConfig.stakeLimitsMultiplier = value; + emit StakeLimitsMultiplierChanged(value); + } + //TODO: Add more setters // PUBLIC METHODS diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/SingleSidePool.sol similarity index 50% rename from contracts/staking/token/TokenPool.sol rename to contracts/staking/token/SingleSidePool.sol index 67503a16..e66c1cac 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/SingleSidePool.sol @@ -10,33 +10,43 @@ import "../../LockKeeper.sol"; import "hardhat/console.sol"; -contract TokenPool is Initializable, AccessControl, IOnBlockListener { +contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { uint constant public MILLION = 1_000_000; - - IERC20 public token; - RewardsBank public rewardsBank; - LockKeeper public lockKeeper; - - string public name; - uint public minStakeValue; - uint public fastUnstakePenalty; - uint public interest; - uint public interestRate; //Time in seconds to how often the stake is increased - uint public lockPeriod; - address public rewardToken; - uint public rewardTokenPrice; // The coefficient to calculate the reward token amount + + struct Config { + IERC20 token; + RewardsBank rewardsBank; + LockKeeper lockKeeper; + string name; + address rewardToken; + uint rewardTokenPrice; // The coefficient to calculate the reward token amount + uint minStakeValue; + uint fastUnstakePenalty; + uint interest; //Time in seconds to how often the stake is increased + uint interestRate; + uint lockPeriod; + } + + struct Info { + uint totalStake; + uint totalRewards; + uint lastInterestUpdate; + uint totalRewardsDebt; + } + + struct Staker { + uint stake; + uint rewardsDebt; + uint claimableRewards; + uint lockedWithdrawal; + } bool public active; - uint public totalStake; - uint public totalRewards; - uint private lastInterestUpdate; - uint private totalRewardsDebt; + Config public config; + Info public info; - mapping(address => uint) public stakes; - mapping(address => uint) private rewardsDebt; - mapping(address => uint) private claimableRewards; - mapping(address => uint) private lockedWithdrawals; + mapping(address => Staker) public stakers; //EVENTS @@ -53,23 +63,11 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); event UnstakeFast(address indexed user, uint amount, uint penalty); - function initialize( - address token_, RewardsBank rewardsBank_, LockKeeper lockkeeper_, string memory name_, uint minStakeValue_, - uint fastUnstakePenalty_, uint intereset_, uint interestRate_, uint lockPeriod_, address rewardToken_, uint rewardTokenPrice_ - ) public initializer { - token = IERC20(token_); - rewardsBank = rewardsBank_; - lockKeeper = lockkeeper_; - - name = name_; - minStakeValue = minStakeValue_; - fastUnstakePenalty = fastUnstakePenalty_; - interest = intereset_; - interestRate = interestRate_; - lockPeriod = lockPeriod_; - rewardToken = rewardToken_; - rewardTokenPrice = rewardTokenPrice_; - lastInterestUpdate = block.timestamp; + function initialize(bytes calldata params_) public initializer { + Config memory config_ = abi.decode(params_, (Config)); + config = config_; + + info.lastInterestUpdate = block.timestamp; active = true; @@ -91,29 +89,29 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { } function setMinStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - minStakeValue = value; + config.minStakeValue = value; emit MinStakeValueChanged(value); } function setInterest(uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { _addInterest(); - interest = _interest; - interestRate = _interestRate; - emit InterestRateChanged(interest, interestRate); + config.interest = _interest; + config.interestRate = _interestRate; + emit InterestRateChanged(config.interest, config.interestRate); } function setLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - lockPeriod = period; + config.lockPeriod = period; emit LockPeriodChanged(period); } function setRewardTokenPrice(uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { - rewardTokenPrice = price; + config.rewardTokenPrice = price; emit RewardTokenPriceChanged(price); } function setFastUnstakePenalty(uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { - fastUnstakePenalty = penalty; + config.fastUnstakePenalty = penalty; emit FastUnstakePenaltyChanged(penalty); } @@ -121,8 +119,8 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { function stake(uint amount) public { require(active, "Pool is not active"); - require(amount >= minStakeValue, "Pool: stake value is too low"); - require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + require(amount >= config.minStakeValue, "Pool: stake value is too low"); + require(config.token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); _stake(msg.sender, amount); @@ -130,41 +128,41 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { } function unstake(uint amount) public { - require(stakes[msg.sender] >= amount, "Not enough stake"); + require(stakers[msg.sender].stake >= amount, "Not enough stake"); _unstake(msg.sender, amount); // cancel previous lock (if exists). canceledAmount will be added to new lock uint canceledAmount; - if (lockKeeper.getLock(lockedWithdrawals[msg.sender]).totalClaims > 0) // prev lock exists - canceledAmount = lockKeeper.cancelLock(lockedWithdrawals[msg.sender]); + if (config.lockKeeper.getLock(stakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists + canceledAmount = config.lockKeeper.cancelLock(stakers[msg.sender].lockedWithdrawal); - token.approve(address(lockKeeper), amount + canceledAmount); + config.token.approve(address(config.lockKeeper), amount + canceledAmount); // lock funds - lockedWithdrawals[msg.sender] = lockKeeper.lockSingle( - msg.sender, address(token), uint64(block.timestamp + lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(token)))) + stakers[msg.sender].lockedWithdrawal = config.lockKeeper.lockSingle( + msg.sender, address(config.token), uint64(block.timestamp + config.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(config.token)))) ); _claimRewards(msg.sender); - emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + lockPeriod, block.timestamp); - emit StakeChanged(msg.sender, stakes[msg.sender]); + emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + config.lockPeriod, block.timestamp); + emit StakeChanged(msg.sender, stakers[msg.sender].stake); } function unstakeFast(uint amount) public { - require(stakes[msg.sender] >= amount, "Not enough stake"); + require(stakers[msg.sender].stake >= amount, "Not enough stake"); _unstake(msg.sender, amount); - uint penalty = amount * fastUnstakePenalty / MILLION; - SafeERC20.safeTransfer(token, msg.sender, amount - penalty); + uint penalty = amount * config.fastUnstakePenalty / MILLION; + SafeERC20.safeTransfer(config.token, msg.sender, amount - penalty); _claimRewards(msg.sender); emit UnstakeFast(msg.sender, amount, penalty); - emit StakeChanged(msg.sender, stakes[msg.sender]); + emit StakeChanged(msg.sender, stakers[msg.sender].stake); } function claim() public { @@ -180,88 +178,88 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { // VIEW METHODS function getStake(address user) public view returns (uint) { - return stakes[user]; + return stakers[user].stake; } function getInterest() public view returns (uint) { - return interest; + return config.interest; } function getInterestRate() public view returns (uint) { - return interestRate; + return config.interestRate; } function getUserRewards(address user) public view returns (uint) { - uint rewardsAmount = _calcRewards(stakes[user]); - if (rewardsAmount + claimableRewards[user] <= rewardsDebt[user]) + uint rewardsAmount = _calcRewards(stakers[user].stake); + if (rewardsAmount + stakers[user].claimableRewards <= stakers[user].rewardsDebt) return 0; - return rewardsAmount + claimableRewards[user] - rewardsDebt[user]; + return rewardsAmount + stakers[user].claimableRewards - stakers[user].rewardsDebt; } // INTERNAL METHODS // store claimable rewards function _calcClaimableRewards(address user) internal { - uint rewardsAmount = _calcRewards(stakes[user]); - uint rewardsWithoutDebt = rewardsAmount - rewardsDebt[user]; - claimableRewards[user] += rewardsWithoutDebt; - totalRewardsDebt += rewardsWithoutDebt; - rewardsDebt[user] += rewardsWithoutDebt; + uint rewardsAmount = _calcRewards(stakers[user].stake); + uint rewardsWithoutDebt = rewardsAmount - stakers[user].rewardsDebt; + stakers[user].claimableRewards += rewardsWithoutDebt; + info.totalRewardsDebt += rewardsWithoutDebt; + stakers[user].rewardsDebt += rewardsWithoutDebt; } function _addInterest() internal { - if (lastInterestUpdate + interestRate > block.timestamp) return; - uint timePassed = block.timestamp - lastInterestUpdate; - uint newRewards = totalStake * interest * timePassed / MILLION / interestRate; + if (info.lastInterestUpdate + config.interestRate > block.timestamp) return; + uint timePassed = block.timestamp - info.lastInterestUpdate; + uint newRewards = info.totalStake * config.interest * timePassed / MILLION / config.interestRate; - totalRewards += newRewards; - lastInterestUpdate = block.timestamp; + info.totalRewards += newRewards; + info.lastInterestUpdate = block.timestamp; emit Interest(newRewards); } function _stake(address user, uint amount) internal { uint rewardsAmount = _calcRewards(amount); - stakes[msg.sender] += amount; - totalStake += amount; + stakers[msg.sender].stake += amount; + info.totalStake += amount; - totalRewards += rewardsAmount; + info.totalRewards += rewardsAmount; - _updateRewardsDebt(user, _calcRewards(stakes[user])); + _updateRewardsDebt(user, _calcRewards(stakers[user].stake)); } function _unstake(address user, uint amount) internal { uint rewardsAmount = _calcRewards(amount); - stakes[msg.sender] -= amount; - totalStake -= amount; + stakers[msg.sender].stake -= amount; + info.totalStake -= amount; - totalRewards -= rewardsAmount; - _updateRewardsDebt(user, _calcRewards(stakes[user])); + info.totalRewards -= rewardsAmount; + _updateRewardsDebt(user, _calcRewards(stakers[user].stake)); } function _updateRewardsDebt(address user, uint newDebt) internal { - uint oldDebt = rewardsDebt[user]; - if (newDebt < oldDebt) totalRewardsDebt -= oldDebt - newDebt; - else totalRewardsDebt += newDebt - oldDebt; - rewardsDebt[user] = newDebt; + uint oldDebt = stakers[user].rewardsDebt; + if (newDebt < oldDebt) info.totalRewardsDebt -= oldDebt - newDebt; + else info.totalRewardsDebt += newDebt - oldDebt; + stakers[user].rewardsDebt = newDebt; } function _claimRewards(address user) internal { - uint amount = claimableRewards[user]; + uint amount = stakers[user].claimableRewards; if (amount == 0) return; - claimableRewards[user] = 0; + stakers[user].claimableRewards = 0; - uint rewardTokenAmount = amount * rewardTokenPrice; - rewardsBank.withdrawErc20(rewardToken, payable(user), rewardTokenAmount); + uint rewardTokenAmount = amount * config.rewardTokenPrice; + config.rewardsBank.withdrawErc20(config.rewardToken, payable(user), rewardTokenAmount); emit Claim(user, rewardTokenAmount); } function _calcRewards(uint amount) internal view returns (uint) { - if (totalStake == 0 && totalRewards == 0) return amount; - return amount * totalRewards / totalStake; + if (info.totalStake == 0 && info.totalRewards == 0) return amount; + return amount * info.totalRewards /info.totalStake; } function _addressToString(address x) internal pure returns (string memory) { diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 6469a59b..25910ec1 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -4,7 +4,8 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import "./TokenPool.sol"; +import "./SingleSidePool.sol"; +import "./DoubleSidePool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; @@ -13,57 +14,99 @@ import "hardhat/console.sol"; contract TokenPoolsManager is AccessControl{ LockKeeper lockKeeper; RewardsBank public bank; - UpgradeableBeacon public beacon; + UpgradeableBeacon public singleSideBeacon; + UpgradeableBeacon public doubleSideBeacon; mapping(string => address) public pools; + mapping(string => address) public doubleSidePools; - constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon beacon_) { + constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon singleSideBeacon_, UpgradeableBeacon doubleSideBeacon_) { lockKeeper = lockKeeper_; bank = bank_; - beacon = beacon_; + singleSideBeacon = singleSideBeacon_; + doubleSideBeacon = doubleSideBeacon_; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } event PoolCreated(string name, address pool); event PoolDeactivated(string name); event PoolActivated(string name); + event DoubleSidePoolCreate(string name, address pool); + event DoubleSidePoolDeactivated(string name); + event DoubleSidePoolActivated(string name); + event DependentSideAdded(string name, address pool); // OWNER METHODS - function createPool( - address token_, string memory name, uint minStakeValue_, - uint fastUnstakePenalty_, uint interest_, uint interestRate_, uint lockPeriod_, address rewardToken_, uint rewardTokenPrice_ - ) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createSingleSidePool(bytes calldata params_) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { console.log("Entered createPool"); + SingleSidePool.Config memory params = abi.decode(params_, (SingleSidePool.Config)); bytes memory data = abi.encodeWithSignature( - "initialize(address,address,address,string,uint256,uint256,uint256,uint256,uint256,address,uint256)", - token_, bank, lockKeeper, name, minStakeValue_, fastUnstakePenalty_, interest_, interestRate_, lockPeriod_, rewardToken_, rewardTokenPrice_); - address pool = address(new BeaconProxy(address(beacon), data)); + "initialize((address,address,address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", + params_); + address pool = address(new BeaconProxy(address(singleSideBeacon), data)); console.log("Pool created at address: %s", pool); - pools[name] = pool; + pools[params.name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit PoolCreated(name, pool); + emit PoolCreated(params.name, pool); return pool; } - function deactivatePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function deactivateSingleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); + SingleSidePool pool = SingleSidePool(pools[_pool]); pool.deactivate(); emit PoolDeactivated(_pool); } - function activatePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function activateSingleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); + + SingleSidePool pool = SingleSidePool(pools[_pool]); pool.activate(); emit PoolActivated(_pool); } + function createDoubleSidePool(string calldata name_, bytes calldata params_) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + bytes memory data = abi.encodeWithSignature( + "initialize(address,address,string,(address,address,uint,uint,uint,uint,uint,uint,uint))", + bank, lockKeeper, name_, params_); + address pool = address(new BeaconProxy(address(doubleSideBeacon), data)); + doubleSidePools[name_] = pool; + bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); + emit DoubleSidePoolCreate(name_, pool); + return pool; + } + + function addDependentSide(string calldata name_, bytes calldata params_) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(doubleSidePools[name_] != address(0), "Pool does not exist"); + DoubleSidePool pool = DoubleSidePool(doubleSidePools[name_]); + pool.addDependentSide(params_); + emit DependentSideAdded(name_, address(pool)); + } + + function deactivateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(doubleSidePools[_pool] != address(0), "Pool does not exist"); + DoubleSidePool pool = DoubleSidePool(doubleSidePools[_pool]); + pool.deactivate(); + emit DoubleSidePoolDeactivated(_pool); + } + + function activateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(doubleSidePools[_pool] != address(0), "Pool does not exist"); + DoubleSidePool pool = DoubleSidePool(doubleSidePools[_pool]); + pool.activate(); + emit DoubleSidePoolActivated(_pool); + } + // VIEW METHODS function getPoolAddress(string memory name) public view returns (address) { return pools[name]; } + function getDoubleSidePoolAddress(string memory name) public view returns (address) { + return doubleSidePools[name]; + } + } From 122995dc9a09cccd4a0127ac931210d222098028 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 11 Sep 2024 20:43:12 +0300 Subject: [PATCH 29/60] Update deploy script --- scripts/ecosystem/token_staking/deploy.ts | 27 ++++++--- scripts/staking/deploy_token_staking.ts | 70 ----------------------- src/contracts/names.ts | 11 ++-- 3 files changed, 27 insertions(+), 81 deletions(-) delete mode 100644 scripts/staking/deploy_token_staking.ts diff --git a/scripts/ecosystem/token_staking/deploy.ts b/scripts/ecosystem/token_staking/deploy.ts index 8fa7a4b4..8405bca5 100644 --- a/scripts/ecosystem/token_staking/deploy.ts +++ b/scripts/ecosystem/token_staking/deploy.ts @@ -1,11 +1,10 @@ import { ethers, upgrades } from "hardhat"; -import { deploy, loadDeployment } from "@airdao/deployments/deploying"; +import { deploy, } from "@airdao/deployments/deploying"; import { ContractNames } from "../../../src"; import { TokenPoolsManager__factory, - Multisig__factory, RewardsBank__factory, LockKeeper__factory } from "../../../typechain-types"; @@ -37,26 +36,40 @@ export async function main() { }); console.log("deploying TokenPool Beacon"); - const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); - console.log("TokenPool Beacon deployed to:", tokenPoolBeacon.address); + + const singleSidePoolFactory = await ethers.getContractFactory("SingleSidePool"); + const singleSideBeacon = await upgrades.deployBeacon(singleSidePoolFactory); + console.log("SingleSidePool Beacon deployed to:", singleSideBeacon.address); + + const doubleSidePoolFactory = await ethers.getContractFactory("DoubleSidePool"); + const doubleSideBeacon = await upgrades.deployBeacon(doubleSidePoolFactory); + console.log("DoubleSidePool Beacon deployed to:", doubleSideBeacon.address); const poolsManager = await deploy({ contractName: ContractNames.Ecosystem_TokenPoolsManager, artifactName: "TokenPoolsManager", - deployArgs: [rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address], + deployArgs: [rewardsBank.address, lockKeeper.address, singleSideBeacon.address, doubleSideBeacon.address], signer: deployer, loadIfAlreadyDeployed: true, }); + console.log("Grant poolsManager rewardsBank admin roles"); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); + + console.log("Grant multisig rewardsBank admin role"); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); + + console.log("Grant multisig poolsManager admin role"); await (await poolsManager.grantRole(await poolsManager.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); if (chainId != 16718) return; // continue only on prod - console.log("Reworking roles from deployer"); + console.log("Revoking roles from deployer"); + + console.log("Revoke rewardsBank admin role from deployer"); await (await rewardsBank.revokeRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), deployer.address)).wait(); + + console.log("Revoke poolsManager admin role from deployer"); await (await poolsManager.revokeRole(await poolsManager.DEFAULT_ADMIN_ROLE(), deployer.address)).wait(); } diff --git a/scripts/staking/deploy_token_staking.ts b/scripts/staking/deploy_token_staking.ts deleted file mode 100644 index 8a4f7d6d..00000000 --- a/scripts/staking/deploy_token_staking.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ethers, upgrades } from "hardhat"; -import { deploy, loadDeployment } from "@airdao/deployments/deploying"; - -import { ContractNames } from "../../src"; - -import { - TokenPoolsManager__factory, - Multisig__factory, - RewardsBank__factory, - LockKeeper__factory -} from "../../typechain-types"; -import {Roadmap2023MultisigSettings} from "../addresses"; -export async function main() { - const { chainId } = await ethers.provider.getNetwork(); - - const [deployer] = await ethers.getSigners(); - - const masterMultisig = loadDeployment(ContractNames.MasterMultisig, chainId).address; - - const multisig = await deploy({ - contractName: ContractNames.TokenPoolsManagerMultisig, - artifactName: "Multisig", - deployArgs: [...Roadmap2023MultisigSettings, masterMultisig], - signer: deployer, - loadIfAlreadyDeployed: true, - }); - - const rewardsBank = await deploy({ - contractName: ContractNames.TokenPoolsManagerRewardsBank, - artifactName: "RewardsBank", - deployArgs: [], - signer: deployer, - loadIfAlreadyDeployed: true, - }); - - const lockKeeper = await deploy({ - contractName: ContractNames.LockKeeper, - artifactName: "LockKeeper", - deployArgs: [], - signer: deployer, - loadIfAlreadyDeployed: true, - isUpgradeableProxy: true, - }); - - - - const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); - - const poolsManager = await deploy({ - contractName: ContractNames.TokenPoolsManager, - artifactName: "TokenPoolsManager", - deployArgs: [rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address], - signer: deployer, - loadIfAlreadyDeployed: true, - }); - - await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); - await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); - await (await poolsManager.grantRole(await poolsManager.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); - - //TODO: Rewoke roles from deployer -} - -if (require.main === module) { - main().catch((error) => { - console.error(error); - process.exitCode = 1; - }); -} diff --git a/src/contracts/names.ts b/src/contracts/names.ts index 1ee75c40..acd65476 100644 --- a/src/contracts/names.ts +++ b/src/contracts/names.ts @@ -84,13 +84,15 @@ export enum ContractNames { Ecosystem_LiquidNodesManagerTreasuryFees = "Ecosystem_LiquidNodesManager_TreasuryFees", Ecosystem_LiquidPoolStakingTiers = "Ecosystem_LiquidPool_StakingTiers", - Ecosystem_TokenPool = "Ecosystem_TokenPool", - Ecosystem_TokenPoolBeacon = "Ecosystem_TokenPoolBeacon", + Ecosystem_GovernmentMultisig = "Ecosystem_Government_Multisig", + + Ecosystem_SingleSidePool = "Ecosystem_SingleSidePool", + Ecosystem_SingleSidePoolBeacon = "Ecosystem_SingleSidePool_Beacon", + Ecosystem_DoubleSidePool = "Ecosystem_DoubleSidePool", + Ecosystem_DoubleSidePoolBeacon = "Ecosystem_DoubleSidePool_Beacon", Ecosystem_TokenPoolsManager = "Ecosystem_TokenPoolsManager", Ecosystem_TokenPoolsManagerMultisig = "Ecosystem_TokenPoolsManager_Multisig", Ecosystem_TokenPoolsManagerRewardsBank = "Ecosystem_TokenPoolsManager_RewardsBank", - - Ecosystem_GovernmentMultisig = "Ecosystem_Government_Multisig", } export const MULTISIGS_COMMON = { @@ -126,6 +128,7 @@ export const MULTISIGS_ECOSYSTEM = { [ContractNames.Ecosystem_LiquidNodesManagerTreasuryFees]: ContractNames.Ecosystem_LiquidPoolMultisig, [ContractNames.Ecosystem_LiquidPoolStAMB]: ContractNames.Ecosystem_LiquidPoolMultisig, [ContractNames.Ecosystem_LiquidPoolStakingTiers]: ContractNames.Ecosystem_LiquidPoolMultisig, + [ContractNames.Ecosystem_TokenPoolsManager]: ContractNames.Ecosystem_TokenPoolsManagerMultisig, }; export const MULTISIGS = {...MULTISIGS_COMMON, ...MULTISIGS_ECOSYSTEM}; From d6a5d7c6f76e9bb8808632577f4f1354d853da86 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 11 Sep 2024 21:55:31 +0300 Subject: [PATCH 30/60] Fix call params --- contracts/staking/token/DoubleSidePool.sol | 7 +- contracts/staking/token/SingleSidePool.sol | 15 +-- contracts/staking/token/TokenPoolsManager.sol | 13 +- scripts/ecosystem/token_staking/deploy.ts | 2 +- test/staking/token/DoubleSidePool.ts | 0 .../token/{TokenPool.ts => SingleSidePool.ts} | 122 +++++++++--------- test/staking/token/TokenPoolsManager.ts | 15 ++- 7 files changed, 90 insertions(+), 84 deletions(-) create mode 100644 test/staking/token/DoubleSidePool.ts rename test/staking/token/{TokenPool.ts => SingleSidePool.ts} (58%) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index ec92d3d1..5ef7dd95 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -13,7 +13,7 @@ import "hardhat/console.sol"; //The side defined by the address of the token. Zero address means native coin contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { - struct MainSideConfig { + struct MainSideConfig { address token; address rewardToken; uint rewardTokenPrice; @@ -112,11 +112,10 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { // OWNER METHODS - function addDependentSide(bytes calldata prams) public onlyRole(DEFAULT_ADMIN_ROLE) { + function addDependentSide(DependantSideConfig calldata config_) public onlyRole(DEFAULT_ADMIN_ROLE) { require(!hasSecondSide, "Second side already exists"); hasSecondSide = true; - DependantSideConfig memory dependantSideConfig_ = abi.decode(prams, (DependantSideConfig)); - dependantSideConfig = dependantSideConfig_; + dependantSideConfig = config_; } function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/contracts/staking/token/SingleSidePool.sol b/contracts/staking/token/SingleSidePool.sol index e66c1cac..fa34afbb 100644 --- a/contracts/staking/token/SingleSidePool.sol +++ b/contracts/staking/token/SingleSidePool.sol @@ -63,8 +63,7 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); event UnstakeFast(address indexed user, uint amount, uint penalty); - function initialize(bytes calldata params_) public initializer { - Config memory config_ = abi.decode(params_, (Config)); + function initialize(Config calldata config_) public initializer { config = config_; info.lastInterestUpdate = block.timestamp; @@ -177,16 +176,16 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { // VIEW METHODS - function getStake(address user) public view returns (uint) { - return stakers[user].stake; + function getConfig() public view returns (Config memory) { + return config; } - function getInterest() public view returns (uint) { - return config.interest; + function getInfo() public view returns (Info memory) { + return info; } - function getInterestRate() public view returns (uint) { - return config.interestRate; + function getStake(address user) public view returns (uint) { + return stakers[user].stake; } function getUserRewards(address user) public view returns (uint) { diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 25910ec1..4fcedbc8 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -38,12 +38,11 @@ contract TokenPoolsManager is AccessControl{ // OWNER METHODS - function createSingleSidePool(bytes calldata params_) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createSingleSidePool(SingleSidePool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { console.log("Entered createPool"); - SingleSidePool.Config memory params = abi.decode(params_, (SingleSidePool.Config)); bytes memory data = abi.encodeWithSignature( "initialize((address,address,address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", - params_); + params); address pool = address(new BeaconProxy(address(singleSideBeacon), data)); console.log("Pool created at address: %s", pool); pools[params.name] = pool; @@ -67,10 +66,10 @@ contract TokenPoolsManager is AccessControl{ emit PoolActivated(_pool); } - function createDoubleSidePool(string calldata name_, bytes calldata params_) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createDoubleSidePool(string calldata name_, DoubleSidePool.MainSideConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { bytes memory data = abi.encodeWithSignature( "initialize(address,address,string,(address,address,uint,uint,uint,uint,uint,uint,uint))", - bank, lockKeeper, name_, params_); + bank, lockKeeper, name_, params); address pool = address(new BeaconProxy(address(doubleSideBeacon), data)); doubleSidePools[name_] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); @@ -78,10 +77,10 @@ contract TokenPoolsManager is AccessControl{ return pool; } - function addDependentSide(string calldata name_, bytes calldata params_) public onlyRole(DEFAULT_ADMIN_ROLE) { + function addDependentSide(string calldata name_, DoubleSidePool.DependantSideConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { require(doubleSidePools[name_] != address(0), "Pool does not exist"); DoubleSidePool pool = DoubleSidePool(doubleSidePools[name_]); - pool.addDependentSide(params_); + pool.addDependentSide(params); emit DependentSideAdded(name_, address(pool)); } diff --git a/scripts/ecosystem/token_staking/deploy.ts b/scripts/ecosystem/token_staking/deploy.ts index 8405bca5..fc2557b7 100644 --- a/scripts/ecosystem/token_staking/deploy.ts +++ b/scripts/ecosystem/token_staking/deploy.ts @@ -1,5 +1,5 @@ import { ethers, upgrades } from "hardhat"; -import { deploy, } from "@airdao/deployments/deploying"; +import { deploy } from "@airdao/deployments/deploying"; import { ContractNames } from "../../../src"; diff --git a/test/staking/token/DoubleSidePool.ts b/test/staking/token/DoubleSidePool.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/SingleSidePool.ts similarity index 58% rename from test/staking/token/TokenPool.ts rename to test/staking/token/SingleSidePool.ts index bf67dd67..f623d2b2 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/SingleSidePool.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { RewardsBank, AirBond, - TokenPool, + SingleSidePool, RewardsBank__factory, AirBond__factory, LockKeeper__factory, @@ -13,20 +13,11 @@ import { } from "../../../typechain-types"; const D1 = 24 * 60 * 60; -const MILLION = 1000000; - -const name = "Test"; -const minStakeValue = 10; -const fastUnstakePenalty = 100000; // 10% -const interest = 100000; // 10% -const interestRate = D1; // 1 day -const lockPeriod = 24 * 60 * 60; // 1 day -const rewardTokenPrice = 1; import { expect } from "chai"; -describe("TokenPool", function () { +describe("SingleSidePool", function () { let owner: SignerWithAddress; - let tokenPool: TokenPool; + let singleSidePool: SingleSidePool; let rewardsBank: RewardsBank; let lockKeeper: LockKeeper; let token: AirBond; @@ -37,54 +28,66 @@ describe("TokenPool", function () { const token = await new AirBond__factory(owner).deploy(owner.address); const lockKeeper = await new LockKeeper__factory(owner).deploy(); - const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - - const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [ - token.address, rewardsBank.address, lockKeeper.address, name, minStakeValue, fastUnstakePenalty, - interest, interestRate, lockPeriod, token.address, rewardTokenPrice - ])) as TokenPool; - - await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), tokenPool.address)).wait(); + const singleSidePoolFactory = await ethers.getContractFactory("SingleSidePool"); + + const singleSidePoolParams: SingleSidePool.ConfigStruct = { + token: token.address, + rewardsBank: rewardsBank.address, + lockKeeper: lockKeeper.address, + name: "Test", + minStakeValue: 10, + fastUnstakePenalty: 100000, // 10% + interest: 100000, // 10% + interestRate: D1, // 1 day + lockPeriod: D1, // 1 day + rewardToken: token.address, + rewardTokenPrice: 1, + }; + + const singleSidePool = (await upgrades.deployProxy(singleSidePoolFactory, [singleSidePoolParams])) as SingleSidePool; + + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), singleSidePool.address)).wait(); await (await token.grantRole(await token.MINTER_ROLE(), owner.address)).wait(); await token.mint(owner.address, 100000000000); - return { owner, tokenPool, rewardsBank, lockKeeper, token }; + return { owner, singleSidePool, rewardsBank, lockKeeper, token }; } beforeEach(async function () { - ({ owner, tokenPool, rewardsBank, lockKeeper, token } = await loadFixture(deploy)); + ({ owner, singleSidePool, rewardsBank, lockKeeper, token } = await loadFixture(deploy)); }); describe("Owner Methods", function () { it("Should deactivate and activate the pool", async function () { - await tokenPool.deactivate(); - expect(await tokenPool.active()).to.equal(false); + await singleSidePool.deactivate(); + expect(await singleSidePool.active()).to.equal(false); - await tokenPool.activate(); - expect(await tokenPool.active()).to.equal(true); + await singleSidePool.activate(); + expect(await singleSidePool.active()).to.equal(true); }); }); describe("Staking", function () { beforeEach(async function () { - await token.approve(tokenPool.address, 1000000); + await token.approve(singleSidePool.address, 1000000); }); it("Should allow staking", async function () { const stake = 1000; - await tokenPool.stake(stake); - expect(await tokenPool.totalStake()).to.equal(stake); - expect(await tokenPool.getStake(owner.address)).to.equal(stake); + await singleSidePool.stake(stake); + const info = await singleSidePool.info(); + expect(info.totalStake).to.equal(stake); + expect(await singleSidePool.getStake(owner.address)).to.equal(stake); }); it("Should not allow staking when pool is deactivated", async function () { - await tokenPool.deactivate(); - await expect(tokenPool.stake(1000)).to.be.revertedWith("Pool is not active"); + await singleSidePool.deactivate(); + await expect(singleSidePool.stake(1000)).to.be.revertedWith("Pool is not active"); }); it("Should not allow staking below minimum stake value", async function () { - await expect(tokenPool.stake(1)).to.be.revertedWith("Pool: stake value is too low"); + await expect(singleSidePool.stake(1)).to.be.revertedWith("Pool: stake value is too low"); }); }); @@ -93,61 +96,62 @@ describe("TokenPool", function () { const stake = 1000; beforeEach(async function () { - await token.approve(tokenPool.address, 1000000000); - await tokenPool.stake(stake); + await token.approve(singleSidePool.address, 1000000000); + await singleSidePool.stake(stake); }); it("Should allow unstaking with rewards", async function () { await time.increase(D1); - await tokenPool.onBlock(); + await singleSidePool.onBlock(); - await expect(await tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); - expect(await tokenPool.totalStake()).to.equal(0); - expect(await tokenPool.getStake(owner.address)).to.equal(0); + await expect(await singleSidePool.unstake(stake)).to.emit(lockKeeper, "Locked"); + const info = await singleSidePool.info(); + expect(info.totalStake).to.equal(0); + expect(await singleSidePool.getStake(owner.address)).to.equal(0); }); it("Should allow fast unstaking with rewards", async function () { await time.increase(D1); - await tokenPool.onBlock(); + await singleSidePool.onBlock(); const balanceBefore = await token.balanceOf(owner.address); - await tokenPool.unstakeFast(stake); + await singleSidePool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); it("Should allow unstaking without rewards", async function () { - await expect(tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); + await expect(singleSidePool.unstake(stake)).to.emit(lockKeeper, "Locked"); }); it("Should allow fast unstaking without rewards", async function () { const balanceBefore = await token.balanceOf(owner.address); - await tokenPool.unstakeFast(stake); + await singleSidePool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); it("Should not allow unstaking more than staked", async function () { - await expect(tokenPool.unstake(stake * 2)).to.be.revertedWith("Not enough stake"); + await expect(singleSidePool.unstake(stake * 2)).to.be.revertedWith("Not enough stake"); }); it("Should not allow fast unstaking more than staked", async function () { const balanceBefore = await token.balanceOf(owner.address); - await tokenPool.unstakeFast(stake); + await singleSidePool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); it("Should allow unstaking when pool is deactivated", async function () { - await tokenPool.deactivate(); - await expect(tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); + await singleSidePool.deactivate(); + await expect(singleSidePool.unstake(stake)).to.emit(lockKeeper, "Locked"); }); it("Should allow fast unstaking when pool is deactivated", async function () { - await tokenPool.deactivate(); + await singleSidePool.deactivate(); const balanceBefore = await token.balanceOf(owner.address); - await tokenPool.unstakeFast(stake); + await singleSidePool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); @@ -156,42 +160,42 @@ describe("TokenPool", function () { describe("Rewards", function () { beforeEach(async function () { await token.mint(owner.address, 100000000000); - await token.approve(tokenPool.address, 1000000); + await token.approve(singleSidePool.address, 1000000); await token.transfer(rewardsBank.address, 10000); }); it("Should allow claiming rewards", async function () { - await tokenPool.stake(1000); + await singleSidePool.stake(1000); // Wait for 1 day await time.increase(D1); - await tokenPool.onBlock(); + await singleSidePool.onBlock(); const expectedReward = 100; - const rewards = await tokenPool.getUserRewards(owner.address); + const rewards = await singleSidePool.getUserRewards(owner.address); expect (rewards).to.equal(expectedReward); const balanceBefore = await token.balanceOf(owner.address); - await tokenPool.claim(); + await singleSidePool.claim(); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(100); }); it("Should allow claiming rewards when pool is deactivated", async function () { - await tokenPool.stake(1000); + await singleSidePool.stake(1000); // Wait for 1 day await time.increase(D1); - await tokenPool.onBlock(); + await singleSidePool.onBlock(); - await tokenPool.deactivate(); + await singleSidePool.deactivate(); const expectedReward = 100; - const rewards = await tokenPool.getUserRewards(owner.address); + const rewards = await singleSidePool.getUserRewards(owner.address); expect (rewards).to.equal(expectedReward); const balanceBefore = await token.balanceOf(owner.address); - await tokenPool.claim(); + await singleSidePool.claim(); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(100); }); diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index 4083a0c3..0d8f7ce0 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -11,7 +11,7 @@ import { LockKeeper__factory, } from "../../../typechain-types"; -import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; +import SignleSidePoolJson from "../../../artifacts/contracts/staking/token/SingleSidePool.sol/SingleSidePool.json"; import { expect } from "chai"; @@ -26,12 +26,17 @@ describe("PoolsManager", function () { const rewardsBank = await new RewardsBank__factory(owner).deploy(); const airBond = await new AirBond__factory(owner).deploy(owner.address); - const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); + + const singleSidePoolFactory = await ethers.getContractFactory("SingleSidePool"); + const singleSidePoolBeacon = await upgrades.deployBeacon(singleSidePoolFactory); + + const doubleSidePoolFactory = await ethers.getContractFactory("DoubleSidePool"); + const doubleSidePoolBeacon = await upgrades.deployBeacon(doubleSidePoolFactory); const lockKeeper = await new LockKeeper__factory(owner).deploy(); - const poolsManager = await new TokenPoolsManager__factory(owner).deploy(rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address); + const poolsManager = await new TokenPoolsManager__factory(owner) + .deploy(rewardsBank.address, lockKeeper.address, singleSidePoolBeacon.address, doubleSidePoolBeacon.address); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); const tokenAddr = airBond.address; @@ -74,7 +79,7 @@ describe("PoolsManager", function () { console.log("Pool Address: ", poolAddress); //await poolsManager.deactivatePool("TestProxy"); - const proxyPool = new ethers.Contract(poolAddress, TokenPoolJson.abi, owner); + const proxyPool = new ethers.Contract(poolAddress, SignleSidePoolJson.abi, owner); const active = await proxyPool.active(); console.log("Active: ", active); }); From f461def07b1c1073a343cf74bb3584d452a7e371 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 12 Sep 2024 12:56:05 +0300 Subject: [PATCH 31/60] Tests WIP --- contracts/staking/token/SingleSidePool.sol | 18 ++++--- contracts/staking/token/TokenPoolsManager.sol | 4 +- test/staking/token/TokenPoolsManager.ts | 51 ++++++++++++------- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/contracts/staking/token/SingleSidePool.sol b/contracts/staking/token/SingleSidePool.sol index fa34afbb..1f2ffea5 100644 --- a/contracts/staking/token/SingleSidePool.sol +++ b/contracts/staking/token/SingleSidePool.sol @@ -15,8 +15,6 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { struct Config { IERC20 token; - RewardsBank rewardsBank; - LockKeeper lockKeeper; string name; address rewardToken; uint rewardTokenPrice; // The coefficient to calculate the reward token amount @@ -42,6 +40,8 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { } bool public active; + RewardsBank rewardsBank; + LockKeeper lockKeeper; Config public config; Info public info; @@ -63,7 +63,9 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); event UnstakeFast(address indexed user, uint amount, uint penalty); - function initialize(Config calldata config_) public initializer { + function initialize(RewardsBank bank_, LockKeeper keeper_, Config calldata config_) public initializer { + rewardsBank = bank_; + lockKeeper = keeper_; config = config_; info.lastInterestUpdate = block.timestamp; @@ -133,13 +135,13 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { // cancel previous lock (if exists). canceledAmount will be added to new lock uint canceledAmount; - if (config.lockKeeper.getLock(stakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists - canceledAmount = config.lockKeeper.cancelLock(stakers[msg.sender].lockedWithdrawal); + if (lockKeeper.getLock(stakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists + canceledAmount = lockKeeper.cancelLock(stakers[msg.sender].lockedWithdrawal); - config.token.approve(address(config.lockKeeper), amount + canceledAmount); + config.token.approve(address(lockKeeper), amount + canceledAmount); // lock funds - stakers[msg.sender].lockedWithdrawal = config.lockKeeper.lockSingle( + stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( msg.sender, address(config.token), uint64(block.timestamp + config.lockPeriod), amount + canceledAmount, string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(config.token)))) ); @@ -252,7 +254,7 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { stakers[user].claimableRewards = 0; uint rewardTokenAmount = amount * config.rewardTokenPrice; - config.rewardsBank.withdrawErc20(config.rewardToken, payable(user), rewardTokenAmount); + rewardsBank.withdrawErc20(config.rewardToken, payable(user), rewardTokenAmount); emit Claim(user, rewardTokenAmount); } diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 4fcedbc8..e3caf600 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -41,8 +41,8 @@ contract TokenPoolsManager is AccessControl{ function createSingleSidePool(SingleSidePool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { console.log("Entered createPool"); bytes memory data = abi.encodeWithSignature( - "initialize((address,address,address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", - params); + "initialize(address,address,(address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", + bank, lockKeeper, params); address pool = address(new BeaconProxy(address(singleSideBeacon), data)); console.log("Pool created at address: %s", pool); pools[params.name] = pool; diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index 0d8f7ce0..8aede7fa 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -6,9 +6,11 @@ import { TokenPoolsManager, RewardsBank, AirBond__factory, + SingleSidePool, RewardsBank__factory, TokenPoolsManager__factory, LockKeeper__factory, + LockKeeper, } from "../../../typechain-types"; import SignleSidePoolJson from "../../../artifacts/contracts/staking/token/SingleSidePool.sol/SingleSidePool.json"; @@ -20,6 +22,7 @@ describe("PoolsManager", function () { let rewardsBank: RewardsBank; let tokenAddr: string; let owner: SignerWithAddress; + let lockKeeper: LockKeeper; async function deploy() { const [owner] = await ethers.getSigners(); @@ -41,24 +44,29 @@ describe("PoolsManager", function () { await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); const tokenAddr = airBond.address; - return { poolsManager, rewardsBank, tokenAddr, owner }; + return { poolsManager, rewardsBank, lockKeeper, tokenAddr, owner }; } beforeEach(async function () { - ({ poolsManager, rewardsBank, tokenAddr, owner } = await loadFixture(deploy)); + ({ poolsManager, rewardsBank, lockKeeper, tokenAddr, owner } = await loadFixture(deploy)); }); - describe("Pool Management", function () { - it("Should allow the owner to create a pool", async function () { - const minStakeValue = 10; - const fastUnstakePenalty = 100000; // 10% - const interest = 100000; // 10% - const interestRate = 24 * 60 * 60; // 24 hours - const lockPeriod = 24 * 60 * 60; // 24 hours - const rewardsTokenPrice = 1; + describe("SingleSidePool Management", function () { + it("Should allow the owner to create a single side pool", async function () { + const singleSidePoolConfig: SingleSidePool.ConfigStruct = { + token: tokenAddr, + name: "TestPool", + minStakeValue: 10, + rewardToken: tokenAddr, + rewardTokenPrice: 1, + fastUnstakePenalty: 100000, // 10% + interest: 100000, // 10% + interestRate: 24 * 60 * 60, // 24 hours + lockPeriod: 24 * 60 * 60, // 24 hours + }; console.log("before createPool"); - const tx = await poolsManager.createPool(tokenAddr, "TestProxy", minStakeValue, fastUnstakePenalty, interest, interestRate, lockPeriod, tokenAddr, rewardsTokenPrice); + const tx = await poolsManager.createSingleSidePool(singleSidePoolConfig); const receipt = await tx.wait(); console.log("Receipt: ", receipt); const poolAddress = receipt.events![4].args![1]; @@ -67,14 +75,19 @@ describe("PoolsManager", function () { }); it("Should activate and deactivate a pool", async function () { - const minStakeValue = 10; - const fastUnstakePenalty = 100000; // 10% - const interest = 100000; // 10% - const interestRate = 24 * 60 * 60; // 24 hours - const lockPeriod = 24 * 60 * 60; // 24 hours - const rewardsTokenPrice = 1; - - await poolsManager.createPool(tokenAddr, "TestProxy", minStakeValue, fastUnstakePenalty, interest, interestRate, lockPeriod, tokenAddr, rewardsTokenPrice); + const singleSidePoolConfig: SingleSidePool.ConfigStruct = { + token: tokenAddr, + name: "TestPool", + minStakeValue: 10, + rewardToken: tokenAddr, + rewardTokenPrice: 1, + fastUnstakePenalty: 100000, // 10% + interest: 100000, // 10% + interestRate: 24 * 60 * 60, // 24 hours + lockPeriod: 24 * 60 * 60, // 24 hours + }; + + await poolsManager.createSingleSidePool(tokenAddr, "TestProxy", minStakeValue, fastUnstakePenalty, interest, interestRate, lockPeriod, tokenAddr, rewardsTokenPrice); const poolAddress = await poolsManager.getPoolAddress("TestProxy"); console.log("Pool Address: ", poolAddress); From 15be59554498a4d924aa51fae4783f15f5cc51aa Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 12 Sep 2024 18:32:43 +0300 Subject: [PATCH 32/60] Add test for TokenPoolsManager --- contracts/staking/token/DoubleSidePool.sol | 2 +- contracts/staking/token/TokenPoolsManager.sol | 8 +- test/staking/token/TokenPoolsManager.ts | 83 +++++++++++++++++-- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index 5ef7dd95..cb1cdf9e 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -112,7 +112,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { // OWNER METHODS - function addDependentSide(DependantSideConfig calldata config_) public onlyRole(DEFAULT_ADMIN_ROLE) { + function addDependantSide(DependantSideConfig calldata config_) public onlyRole(DEFAULT_ADMIN_ROLE) { require(!hasSecondSide, "Second side already exists"); hasSecondSide = true; dependantSideConfig = config_; diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index e3caf600..c5ea41da 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -67,20 +67,22 @@ contract TokenPoolsManager is AccessControl{ } function createDoubleSidePool(string calldata name_, DoubleSidePool.MainSideConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + console.log("Entered createDoubleSidePool"); bytes memory data = abi.encodeWithSignature( - "initialize(address,address,string,(address,address,uint,uint,uint,uint,uint,uint,uint))", + "initialize(address,address,string,(address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256))", bank, lockKeeper, name_, params); address pool = address(new BeaconProxy(address(doubleSideBeacon), data)); + console.log("DoubleSidePool created at address: %s", pool); doubleSidePools[name_] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); emit DoubleSidePoolCreate(name_, pool); return pool; } - function addDependentSide(string calldata name_, DoubleSidePool.DependantSideConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { + function addDependantSide(string calldata name_, DoubleSidePool.DependantSideConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { require(doubleSidePools[name_] != address(0), "Pool does not exist"); DoubleSidePool pool = DoubleSidePool(doubleSidePools[name_]); - pool.addDependentSide(params); + pool.addDependantSide(params); emit DependentSideAdded(name_, address(pool)); } diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index 8aede7fa..f17b1961 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -11,9 +11,11 @@ import { TokenPoolsManager__factory, LockKeeper__factory, LockKeeper, + DoubleSidePool, } from "../../../typechain-types"; import SignleSidePoolJson from "../../../artifacts/contracts/staking/token/SingleSidePool.sol/SingleSidePool.json"; +import DoubleSidePoolJson from "../../../artifacts/contracts/staking/token/DoubleSidePool.sol/DoubleSidePool.json"; import { expect } from "chai"; @@ -65,13 +67,11 @@ describe("PoolsManager", function () { lockPeriod: 24 * 60 * 60, // 24 hours }; - console.log("before createPool"); const tx = await poolsManager.createSingleSidePool(singleSidePoolConfig); const receipt = await tx.wait(); - console.log("Receipt: ", receipt); const poolAddress = receipt.events![4].args![1]; - expect(await poolsManager.getPoolAddress("TestProxy")).to.equal(poolAddress); + expect(await poolsManager.getPoolAddress("TestPool")).to.equal(poolAddress); }); it("Should activate and deactivate a pool", async function () { @@ -87,17 +87,82 @@ describe("PoolsManager", function () { lockPeriod: 24 * 60 * 60, // 24 hours }; - await poolsManager.createSingleSidePool(tokenAddr, "TestProxy", minStakeValue, fastUnstakePenalty, interest, interestRate, lockPeriod, tokenAddr, rewardsTokenPrice); - const poolAddress = await poolsManager.getPoolAddress("TestProxy"); - console.log("Pool Address: ", poolAddress); + await poolsManager.createSingleSidePool(singleSidePoolConfig); + const poolAddress = await poolsManager.getPoolAddress("TestPool"); - //await poolsManager.deactivatePool("TestProxy"); const proxyPool = new ethers.Contract(poolAddress, SignleSidePoolJson.abi, owner); - const active = await proxyPool.active(); - console.log("Active: ", active); + expect(await proxyPool.active()).to.equal(true); + await poolsManager.deactivateSingleSidePool("TestPool"); + expect(await proxyPool.active()).to.equal(false); }); }); + describe("DoubleSidePool Management", function () { + it("Should allow the owner to create a double side pool", async function () { + const doubleSidePoolConfig: DoubleSidePool.MainSideConfigStruct = { + token: tokenAddr, + rewardToken: tokenAddr, + rewardTokenPrice: 1, + minStakeValue: 10, + unstakeLockPeriod: 24 * 60 * 60, // 24 hours + fastUnstakePenalty: 100000, // 10% + interest: 100000, // 10% + interestRate: 24 * 60 * 60, // 24 hours + lockPeriod: 24 * 60 * 60, // 24 hours + }; + + const tx = await poolsManager.createDoubleSidePool("TestPool", doubleSidePoolConfig); + const receipt = await tx.wait(); + const poolAddress = receipt.events![4].args![1]; + + expect(await poolsManager.getDoubleSidePoolAddress("TestPool")).to.equal(poolAddress); + }); + + // add dependant side + it("Should allow the owner to add a dependant side to a double side pool", async function () { + const doubleSidePoolConfig: DoubleSidePool.MainSideConfigStruct = { + token: tokenAddr, + rewardToken: tokenAddr, + rewardTokenPrice: 1, + minStakeValue: 10, + unstakeLockPeriod: 24 * 60 * 60, // 24 hours + fastUnstakePenalty: 100000, // 10% + interest: 100000, // 10% + interestRate: 24 * 60 * 60, // 24 hours + lockPeriod: 24 * 60 * 60, // 24 hours + }; + + await poolsManager.createDoubleSidePool("TestPool", doubleSidePoolConfig); + + const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { + token: tokenAddr, + rewardToken: tokenAddr, + rewardTokenPrice: 1, + minStakeValue: 10, + unstakeLockPeriod: 24 * 60 * 60, // 24 hours + fastUnstakePenalty: 100000, // 10% + interest: 100000, // 10% + interestRate: 24 * 60 * 60, // 24 hours + lockPeriod: 24 * 60 * 60, // 24 hours + maxTotalStakeValue: ethers.utils.parseEther("1000"), + maxStakePerUserValue: ethers.utils.parseEther("1000"), + stakeLockPeriod: 24 * 60 * 60, + stakeLimitsMultiplier: 24 * 60 * 60, + }; + + const tx = await poolsManager.addDependantSide("TestPool", dependantSideConfig); + const receipt = await tx.wait(); + const poolAddress = receipt.events![0].args![1]; + expect(await poolsManager.getDoubleSidePoolAddress("TestPool")).to.equal(poolAddress); + + const contract = new ethers.Contract(poolAddress, DoubleSidePoolJson.abi, owner); + const dependantSideConfigStruct: DoubleSidePool.DependantSideConfigStruct = await contract.getDependantSideConfig(); + expect(dependantSideConfigStruct.token).to.equal(tokenAddr); + }); + // activate deactivate + }); + + }); From d75ec998344871ce09388337699acd80ebf18e45 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Fri, 13 Sep 2024 14:31:43 +0300 Subject: [PATCH 33/60] Fix tests --- contracts/staking/token/DoubleSidePool.sol | 12 ++++++------ contracts/staking/token/SingleSidePool.sol | 7 ++++--- test/staking/token/SingleSidePool.ts | 9 ++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index cb1cdf9e..ea5d98f6 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -56,7 +56,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { uint stakedAt; } - uint constant public MILLION = 1_000_000; + uint constant public BILLION = 1_000_000_000; LockKeeper public lockKeeper; RewardsBank public rewardsBank; @@ -287,7 +287,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { function _addInterestMainSide() internal { if (mainSideInfo.lastInterestUpdate + mainSideConfig.interestRate > block.timestamp) return; uint timePassed = block.timestamp - mainSideInfo.lastInterestUpdate; - uint newRewards = mainSideInfo.totalStake * mainSideConfig.interest * timePassed / MILLION / mainSideConfig.interestRate; + uint newRewards = mainSideInfo.totalStake * mainSideConfig.interest * timePassed / BILLION / mainSideConfig.interestRate; mainSideInfo.totalRewards += newRewards; mainSideInfo.lastInterestUpdate = block.timestamp; @@ -364,7 +364,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { mainSideInfo.totalRewards -= rewardsAmount; _updateRewardsDebt(false, user, _calcRewards(false, mainSideStakers[user].stake)); - uint penalty = amount * mainSideConfig.fastUnstakePenalty / MILLION; + uint penalty = amount * mainSideConfig.fastUnstakePenalty / BILLION; if (mainSideConfig.token == address(0)) { payable(msg.sender).transfer(amount - penalty); } else { @@ -382,7 +382,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { function _addInterestDependantSide() internal { if (dependantSideInfo.lastInterestUpdate + dependantSideConfig.interestRate > block.timestamp) return; uint timePassed = block.timestamp - dependantSideInfo.lastInterestUpdate; - uint newRewards = dependantSideInfo.totalStake * dependantSideConfig.interest * timePassed / MILLION / dependantSideConfig.interestRate; + uint newRewards = dependantSideInfo.totalStake * dependantSideConfig.interest * timePassed / BILLION / dependantSideConfig.interestRate; dependantSideInfo.totalRewards += newRewards; dependantSideInfo.lastInterestUpdate = block.timestamp; @@ -462,7 +462,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { dependantSideInfo.totalRewards -= rewardsAmount; _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); - uint penalty = amount * dependantSideConfig.fastUnstakePenalty / MILLION; + uint penalty = amount * dependantSideConfig.fastUnstakePenalty / BILLION; if (dependantSideConfig.token == address(0)) { payable(msg.sender).transfer(amount - penalty); } else { @@ -476,7 +476,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { } function _maxUserStakeValue(address user) internal view returns (uint) { - return mainSideStakers[user].stake * dependantSideConfig.stakeLimitsMultiplier / MILLION; + return mainSideStakers[user].stake * dependantSideConfig.stakeLimitsMultiplier / BILLION; } //COMMON METHODS diff --git a/contracts/staking/token/SingleSidePool.sol b/contracts/staking/token/SingleSidePool.sol index 1f2ffea5..d9e26f7f 100644 --- a/contracts/staking/token/SingleSidePool.sol +++ b/contracts/staking/token/SingleSidePool.sol @@ -11,7 +11,6 @@ import "../../LockKeeper.sol"; import "hardhat/console.sol"; contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { - uint constant public MILLION = 1_000_000; struct Config { IERC20 token; @@ -39,6 +38,8 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { uint lockedWithdrawal; } + uint constant public BILLION = 1_000_000_000; + bool public active; RewardsBank rewardsBank; LockKeeper lockKeeper; @@ -157,7 +158,7 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { _unstake(msg.sender, amount); - uint penalty = amount * config.fastUnstakePenalty / MILLION; + uint penalty = amount * config.fastUnstakePenalty / BILLION; SafeERC20.safeTransfer(config.token, msg.sender, amount - penalty); _claimRewards(msg.sender); @@ -212,7 +213,7 @@ contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { function _addInterest() internal { if (info.lastInterestUpdate + config.interestRate > block.timestamp) return; uint timePassed = block.timestamp - info.lastInterestUpdate; - uint newRewards = info.totalStake * config.interest * timePassed / MILLION / config.interestRate; + uint newRewards = info.totalStake * config.interest * timePassed / BILLION / config.interestRate; info.totalRewards += newRewards; info.lastInterestUpdate = block.timestamp; diff --git a/test/staking/token/SingleSidePool.ts b/test/staking/token/SingleSidePool.ts index f623d2b2..09e6da62 100644 --- a/test/staking/token/SingleSidePool.ts +++ b/test/staking/token/SingleSidePool.ts @@ -13,6 +13,7 @@ import { } from "../../../typechain-types"; const D1 = 24 * 60 * 60; +const BILLION = 1000000000; import { expect } from "chai"; describe("SingleSidePool", function () { @@ -32,19 +33,17 @@ describe("SingleSidePool", function () { const singleSidePoolParams: SingleSidePool.ConfigStruct = { token: token.address, - rewardsBank: rewardsBank.address, - lockKeeper: lockKeeper.address, name: "Test", minStakeValue: 10, - fastUnstakePenalty: 100000, // 10% - interest: 100000, // 10% + fastUnstakePenalty: 0.10 * BILLION, // 10% + interest: 0.10 * BILLION, // 10% interestRate: D1, // 1 day lockPeriod: D1, // 1 day rewardToken: token.address, rewardTokenPrice: 1, }; - const singleSidePool = (await upgrades.deployProxy(singleSidePoolFactory, [singleSidePoolParams])) as SingleSidePool; + const singleSidePool = (await upgrades.deployProxy(singleSidePoolFactory, [rewardsBank.address, lockKeeper.address, singleSidePoolParams])) as SingleSidePool; await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), singleSidePool.address)).wait(); await (await token.grantRole(await token.MINTER_ROLE(), owner.address)).wait(); From a94dd9f1b8b4b1c65bf3520ea2a7352e386fc5c3 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Fri, 13 Sep 2024 16:17:08 +0300 Subject: [PATCH 34/60] Tests for DoubleSidePool --- contracts/staking/token/DoubleSidePool.sol | 62 ++- test/staking/token/DoubleSidePool.ts | 551 +++++++++++++++++++++ 2 files changed, 590 insertions(+), 23 deletions(-) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index ea5d98f6..1cb32792 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -106,6 +106,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { hasSecondSide = false; mainSideConfig = mainSideConfig_; + mainSideInfo.lastInterestUpdate = block.timestamp; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -116,6 +117,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { require(!hasSecondSide, "Second side already exists"); hasSecondSide = true; dependantSideConfig = config_; + dependantSideInfo.lastInterestUpdate = block.timestamp; } function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { @@ -273,7 +275,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { } function getUserDependantSideRewards(address user) public view returns (uint) { - uint rewardsAmount = _calcRewards(false, dependantSideStakers[user].stake); + uint rewardsAmount = _calcRewards(true, dependantSideStakers[user].stake); if (rewardsAmount + dependantSideStakers[user].claimableRewards <= dependantSideStakers[user].rewardsDebt) return 0; @@ -285,7 +287,6 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { // MAIN SIDE METHODS function _addInterestMainSide() internal { - if (mainSideInfo.lastInterestUpdate + mainSideConfig.interestRate > block.timestamp) return; uint timePassed = block.timestamp - mainSideInfo.lastInterestUpdate; uint newRewards = mainSideInfo.totalStake * mainSideConfig.interest * timePassed / BILLION / mainSideConfig.interestRate; @@ -308,7 +309,6 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { uint rewardsAmount = _calcRewards(false, amount); mainSideStakers[msg.sender].stake += amount; - mainSideStakers[msg.sender].stakedAt = block.timestamp; mainSideInfo.totalStake += amount; mainSideInfo.totalRewards += rewardsAmount; @@ -335,16 +335,20 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { canceledAmount = lockKeeper.cancelLock(mainSideStakers[msg.sender].lockedWithdrawal); if (mainSideConfig.token == address(0)) { - payable(msg.sender).transfer(amount - canceledAmount); + // lock funds + mainSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle{value: amount + canceledAmount}( + msg.sender, address(mainSideConfig.token), uint64(block.timestamp + mainSideConfig.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainSideConfig.token)))) + ); } else { - IERC20(mainSideConfig.token).approve(address(lockKeeper), amount - canceledAmount); + IERC20(mainSideConfig.token).approve(address(lockKeeper), amount + canceledAmount); + // lock funds + mainSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( + msg.sender, address(mainSideConfig.token), uint64(block.timestamp + mainSideConfig.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainSideConfig.token)))) + ); } - // lock funds - mainSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( - msg.sender, address(mainSideConfig.token), uint64(block.timestamp + mainSideConfig.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainSideConfig.token)))) - ); _claimRewards(false, msg.sender); @@ -380,6 +384,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { // DEPENDANT SIDE METHODS function _addInterestDependantSide() internal { + if (!hasSecondSide) return; if (dependantSideInfo.lastInterestUpdate + dependantSideConfig.interestRate > block.timestamp) return; uint timePassed = block.timestamp - dependantSideInfo.lastInterestUpdate; uint newRewards = dependantSideInfo.totalStake * dependantSideConfig.interest * timePassed / BILLION / dependantSideConfig.interestRate; @@ -396,21 +401,26 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(mainSideConfig.token), msg.sender, address(this), amount); + SafeERC20.safeTransferFrom(IERC20(dependantSideConfig.token), msg.sender, address(this), amount); } + console.log("Staking dependant side"); + console.log("max user stake: ", _maxUserStakeValue(msg.sender)); + console.log("amount: ", amount); + uint rewardsAmount = _calcRewards(true, amount); dependantSideStakers[msg.sender].stake += amount; dependantSideInfo.totalStake += amount; + dependantSideStakers[msg.sender].stakedAt = block.timestamp; - require(dependantSideInfo.totalStake <= dependantSideConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); require(dependantSideStakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); + require(dependantSideInfo.totalStake <= dependantSideConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); require(dependantSideStakers[msg.sender].stake <= dependantSideConfig.maxStakePerUserValue, "Pool: max stake per user exceeded"); dependantSideInfo.totalRewards += rewardsAmount; - _updateRewardsDebt(true, user, _calcRewards(true, mainSideStakers[user].stake)); + _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); emit StakeChanged(true, msg.sender, amount); } @@ -432,16 +442,20 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { canceledAmount = lockKeeper.cancelLock(dependantSideStakers[msg.sender].lockedWithdrawal); if (mainSideConfig.token == address(0)) { - payable(msg.sender).transfer(amount - canceledAmount); + // lock funds + dependantSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle{value: amount + canceledAmount}( + msg.sender, address(dependantSideConfig.token), uint64(block.timestamp + dependantSideConfig.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(dependantSideConfig.token)))) + ); } else { - IERC20(mainSideConfig.token).approve(address(lockKeeper), amount - canceledAmount); + IERC20(dependantSideConfig.token).approve(address(lockKeeper), amount + canceledAmount); + // lock funds + dependantSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( + msg.sender, address(dependantSideConfig.token), uint64(block.timestamp + dependantSideConfig.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(dependantSideConfig.token)))) + ); } - // lock funds - dependantSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( - msg.sender, address(mainSideConfig.token), uint64(block.timestamp + dependantSideConfig.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(dependantSideConfig.token)))) - ); _claimRewards(true, msg.sender); @@ -476,7 +490,9 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { } function _maxUserStakeValue(address user) internal view returns (uint) { - return mainSideStakers[user].stake * dependantSideConfig.stakeLimitsMultiplier / BILLION; + console.log("main side stake: ", mainSideStakers[user].stake); + console.log("stake limits multiplier: ", dependantSideConfig.stakeLimitsMultiplier); + return mainSideStakers[user].stake * dependantSideConfig.stakeLimitsMultiplier; } //COMMON METHODS @@ -531,10 +547,10 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { function _calcRewards(bool dependant, uint amount) internal view returns (uint) { if (dependant) { if (dependantSideInfo.totalStake == 0 && dependantSideInfo.totalRewards == 0) return amount; - amount * dependantSideInfo.totalRewards / dependantSideInfo.totalStake; + return amount * dependantSideInfo.totalRewards / dependantSideInfo.totalStake; } else { if (mainSideInfo.totalStake == 0 && mainSideInfo.totalRewards == 0) return amount; - amount * mainSideInfo.totalRewards / mainSideInfo.totalStake; + return amount * mainSideInfo.totalRewards / mainSideInfo.totalStake; } } diff --git a/test/staking/token/DoubleSidePool.ts b/test/staking/token/DoubleSidePool.ts index e69de29b..2d6713a3 100644 --- a/test/staking/token/DoubleSidePool.ts +++ b/test/staking/token/DoubleSidePool.ts @@ -0,0 +1,551 @@ +import { ethers, upgrades } from "hardhat"; +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import { + RewardsBank, + AirBond, + DoubleSidePool, + RewardsBank__factory, + AirBond__factory, + LockKeeper__factory, + LockKeeper, +} from "../../../typechain-types"; + +const D1 = 24 * 60 * 60; +const BILLION = 1_000_000_000; + +import { expect } from "chai"; + +describe("DoubleSidePool", function () { + let owner: SignerWithAddress; + let doubleSidePool: DoubleSidePool; + let rewardsBank: RewardsBank; + let lockKeeper: LockKeeper; + let mainToken: AirBond; + let dependantToken: AirBond; + + async function deploy() { + const [owner] = await ethers.getSigners(); + const rewardsBank = await new RewardsBank__factory(owner).deploy(); + const mainToken = await new AirBond__factory(owner).deploy(owner.address); + const dependantToken = await new AirBond__factory(owner).deploy(owner.address); + const lockKeeper = await new LockKeeper__factory(owner).deploy(); + + const doubleSidePoolFactory = await ethers.getContractFactory("DoubleSidePool"); + + const mainSideConfig: DoubleSidePool.MainSideConfigStruct = { + token: mainToken.address, + rewardToken: mainToken.address, + rewardTokenPrice: 1, // 1:1 ratio + minStakeValue: 10, + unstakeLockPeriod: D1, // 1 day + fastUnstakePenalty: 0.10 * BILLION, // 10% + lockPeriod: D1, // 1 day + interest: 0.10 * BILLION, // 10% + interestRate: D1, // 1 day + }; + + const doubleSidePool = (await upgrades.deployProxy(doubleSidePoolFactory, [ + rewardsBank.address, + lockKeeper.address, + "Test Double Side Pool", + mainSideConfig + ])) as DoubleSidePool; + + await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), doubleSidePool.address); + await mainToken.grantRole(await mainToken.MINTER_ROLE(), owner.address); + await dependantToken.grantRole(await dependantToken.MINTER_ROLE(), owner.address); + + await mainToken.mint(owner.address, ethers.utils.parseEther("1000000")); + await dependantToken.mint(owner.address, ethers.utils.parseEther("1000000")); + + return { owner, doubleSidePool, rewardsBank, lockKeeper, mainToken, dependantToken }; + } + + beforeEach(async function () { + ({ owner, doubleSidePool, rewardsBank, lockKeeper, mainToken, dependantToken } = await loadFixture(deploy)); + }); + + describe("Initialization", function () { + it("Should initialize with correct main side config", async function () { + const config = await doubleSidePool.mainSideConfig(); + expect(config.token).to.equal(mainToken.address); + expect(config.rewardToken).to.equal(mainToken.address); + expect(config.rewardTokenPrice).to.equal(1); + expect(config.minStakeValue).to.equal(10); + expect(config.unstakeLockPeriod).to.equal(D1); + expect(config.fastUnstakePenalty).to.equal(0.10 * BILLION); + expect(config.lockPeriod).to.equal(D1); + expect(config.interest).to.equal(0.10 * BILLION); + expect(config.interestRate).to.equal(D1); + }); + + it("Should not have a dependant side initially", async function () { + expect(await doubleSidePool.hasSecondSide()).to.be.false; + }); + }); + + describe("Owner Methods", function () { + it("Should deactivate and activate the pool", async function () { + await doubleSidePool.deactivate(); + expect(await doubleSidePool.active()).to.be.false; + + await doubleSidePool.activate(); + expect(await doubleSidePool.active()).to.be.true; + }); + + it("Should add a dependant side", async function () { + const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { + token: dependantToken.address, + rewardToken: dependantToken.address, + rewardTokenPrice: BILLION, + minStakeValue: 5, + unstakeLockPeriod: D1, + fastUnstakePenalty: 0.05 * BILLION, + lockPeriod: D1, + interest: 0.15 * BILLION, + interestRate: D1, + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLockPeriod: D1, + stakeLimitsMultiplier: 2 * BILLION, + }; + + await doubleSidePool.addDependantSide(dependantSideConfig); + expect(await doubleSidePool.hasSecondSide()).to.be.true; + + const config = await doubleSidePool.dependantSideConfig(); + expect(config.token).to.equal(dependantToken.address); + expect(config.rewardToken).to.equal(dependantToken.address); + expect(config.rewardTokenPrice).to.equal(BILLION); + expect(config.minStakeValue).to.equal(5); + expect(config.unstakeLockPeriod).to.equal(D1); + expect(config.fastUnstakePenalty).to.equal(0.05 * BILLION); + expect(config.lockPeriod).to.equal(D1); + expect(config.interest).to.equal(0.15 * BILLION); + expect(config.interestRate).to.equal(D1); + expect(config.maxTotalStakeValue).to.equal(ethers.utils.parseEther("1000000")); + expect(config.maxStakePerUserValue).to.equal(ethers.utils.parseEther("100000")); + expect(config.stakeLockPeriod).to.equal(D1); + expect(config.stakeLimitsMultiplier).to.equal(2 * BILLION); + }); + }); + + describe("Main Side Staking", function () { + beforeEach(async function () { + await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + }); + + it("Should allow staking on the main side", async function () { + const stake = ethers.utils.parseEther("1000"); + await doubleSidePool.stake(false, stake); + const info = await doubleSidePool.mainSideInfo(); + expect(info.totalStake).to.equal(stake); + const staker = await doubleSidePool.getMainSideStaker(owner.address); + expect(staker.stake).to.equal(stake); + }); + + it("Should not allow staking when pool is deactivated", async function () { + await doubleSidePool.deactivate(); + await expect(doubleSidePool.stake(false, 1000)).to.be.revertedWith("Pool is not active"); + }); + + it("Should not allow staking below minimum stake value", async function () { + await expect(doubleSidePool.stake(false, 1)).to.be.revertedWith("Pool: stake value is too low"); + }); + }); + + describe("Main Side Unstaking", function () { + const stake = ethers.utils.parseEther("1000"); + + beforeEach(async function () { + await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + await doubleSidePool.stake(false, stake); + }); + + it("Should allow unstaking with rewards", async function () { + await time.increase(D1); + await doubleSidePool.onBlock(); + + await expect(doubleSidePool.unstake(false, stake)).to.emit(lockKeeper, "Locked"); + const info = await doubleSidePool.mainSideInfo(); + expect(info.totalStake).to.equal(0); + const staker = await doubleSidePool.getMainSideStaker(owner.address); + expect(staker.stake).to.equal(0); + }); + + it("Should allow fast unstaking with rewards", async function () { + await time.increase(D1); + await doubleSidePool.onBlock(); + + const balanceBefore = await mainToken.balanceOf(owner.address); + await doubleSidePool.unstakeFast(false, stake); + const balanceAfter = await mainToken.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(stake.mul(90).div(100)); // 90% due to 10% penalty + }); + + it("Should not allow unstaking more than staked", async function () { + await expect(doubleSidePool.unstake(false, stake.mul(2))).to.be.revertedWith("Not enough stake"); + }); + + it("Should allow unstaking when pool is deactivated", async function () { + await doubleSidePool.deactivate(); + await expect(doubleSidePool.unstake(false, stake)).to.emit(lockKeeper, "Locked"); + }); + }); + + describe("Main Side Rewards", function () { + beforeEach(async function () { + await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + await mainToken.transfer(rewardsBank.address, ethers.utils.parseEther("10000")); + }); + + it("Should allow claiming rewards", async function () { + await doubleSidePool.stake(false, 1000); + + await time.increase(D1); + await doubleSidePool.onBlock(); + + const expectedReward = 100; // 10% interest + const rewards = await doubleSidePool.getUserMainSideRewards(owner.address); + console.log("Main side rewards: ", rewards.toString()); + expect(rewards).to.equal(expectedReward); + + const balanceBefore = await mainToken.balanceOf(owner.address); + await doubleSidePool.claim(false); + const balanceAfter = await mainToken.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); + }); + + it("Should allow claiming rewards when pool is deactivated", async function () { + await doubleSidePool.stake(false, 1000); + + await time.increase(D1); + await doubleSidePool.onBlock(); + + await doubleSidePool.deactivate(); + + const expectedReward = 100; // 10% interest + const rewards = await doubleSidePool.getUserMainSideRewards(owner.address); + expect(rewards).to.equal(expectedReward); + + const balanceBefore = await mainToken.balanceOf(owner.address); + await doubleSidePool.claim(false); + const balanceAfter = await mainToken.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); + }); + }); + + describe("Dependant Side", function () { + beforeEach(async function () { + const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { + token: dependantToken.address, + rewardToken: dependantToken.address, + rewardTokenPrice: 1, + minStakeValue: 10, + unstakeLockPeriod: D1, + fastUnstakePenalty: 0.05 * BILLION, + lockPeriod: D1, + interest: 0.15 * BILLION, + interestRate: D1, + maxTotalStakeValue: BILLION * 1000, + maxStakePerUserValue: BILLION, + stakeLockPeriod: D1 * 30, + stakeLimitsMultiplier: 2, + }; + + await doubleSidePool.addDependantSide(dependantSideConfig); + await dependantToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + await mainToken.transfer(rewardsBank.address, ethers.utils.parseEther("10000")); + await dependantToken.transfer(rewardsBank.address, ethers.utils.parseEther("10000")); + }); + + it("Should allow staking on the dependant side", async function () { + const mainStake = 100000; + await doubleSidePool.stake(false, mainStake); + + const dependantStake = 100; + await doubleSidePool.stake(true, dependantStake); + + const info = await doubleSidePool.dependantSideInfo(); + expect(info.totalStake).to.equal(dependantStake); + const staker = await doubleSidePool.getDependantSideStaker(owner.address); + expect(staker.stake).to.equal(dependantStake); + }); + + it("Should not allow staking on dependant side beyond the limit", async function () { + const mainStake = 1000; + await doubleSidePool.stake(false, mainStake); + + const dependantStake = ethers.utils.parseEther("2001"); // Exceeds 2x main stake + await expect(doubleSidePool.stake(true, dependantStake)).to.be.revertedWith("Pool: user max stake value exceeded"); + }); + + it("Should allow unstaking from the dependant side", async function () { + await doubleSidePool.setStakeLockPeriod(0); + const mainStake = 1000; + await doubleSidePool.stake(false, mainStake); + + const dependantStake = 100; + await doubleSidePool.stake(true, dependantStake); + + await time.increase(D1); + await doubleSidePool.onBlock(); + + await expect(doubleSidePool.unstake(true, dependantStake)).to.emit(lockKeeper, "Locked"); + const info = await doubleSidePool.dependantSideInfo(); + expect(info.totalStake).to.equal(0); + const staker = await doubleSidePool.getDependantSideStaker(owner.address); + expect(staker.stake).to.equal(0); + }); + + it("Should allow claiming rewards from the dependant side", async function () { + const mainStake = 1000; + await doubleSidePool.stake(false, mainStake); + + const dependantStake = 100; + await doubleSidePool.stake(true, dependantStake); + + await time.increase(D1); + await doubleSidePool.onBlock(); + + const expectedReward = 15; // 15% interest + const rewards = await doubleSidePool.getUserDependantSideRewards(owner.address); + expect(rewards).to.equal(expectedReward); + + const balanceBefore = await dependantToken.balanceOf(owner.address); + await doubleSidePool.claim(true); + const balanceAfter = await dependantToken.balanceOf(owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); + }); + + it("Should not allow staking on dependant side when total stake limit is reached", async function () { + await doubleSidePool.setMaxTotalStakeValue(1000); + const mainStake = 100000; // Set a high main stake to avoid individual limit + await doubleSidePool.stake(false, mainStake); + + const maxStake = 1000; + await doubleSidePool.stake(true, maxStake); + + await expect(doubleSidePool.stake(true, 10)).to.be.revertedWith("Pool: max stake value exceeded"); + }); + + it("Should not allow unstaking from dependant side before stake lock period", async function () { + const mainStake = 1000; + await doubleSidePool.stake(false, mainStake); + + const dependantStake = 100; + await doubleSidePool.stake(true, dependantStake); + + await expect(doubleSidePool.unstake(true, dependantStake)).to.be.revertedWith("Stake is locked"); + }); + + it("Should allow fast unstaking from dependant side with penalty", async function () { + await doubleSidePool.setStakeLockPeriod(0); + const mainStake = 1000; + await doubleSidePool.stake(false, mainStake); + + const dependantStake = 100; + await doubleSidePool.stake(true, dependantStake); + + //await time.increase(D1); + //await doubleSidePool.onBlock(); + + const balanceBefore = await dependantToken.balanceOf(owner.address); + await doubleSidePool.unstakeFast(true, dependantStake); + const balanceAfter = await dependantToken.balanceOf(owner.address); + + const expectedReturn = 95; // 95% due to 5% penalty + expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReturn); + }); + + it("Should update stake limits when main side stake changes", async function () { + const initialMainStake = 1000; + await doubleSidePool.stake(false, initialMainStake); + + const maxDependantStake = 2000; // 2x multiplier + await doubleSidePool.stake(true, maxDependantStake); + + // Increase main stake + const additionalMainStake = 100; + await doubleSidePool.stake(false, additionalMainStake); + + // Now we should be able to stake more on the dependant side + const additionalDependantStake = 200; + await expect(doubleSidePool.stake(true, additionalDependantStake)).to.not.be.reverted; + }); + + it("Should not allow adding dependant side twice", async function () { + const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { + token: dependantToken.address, + rewardToken: dependantToken.address, + rewardTokenPrice: BILLION, + minStakeValue: 5, + unstakeLockPeriod: D1, + fastUnstakePenalty: 0.05 * BILLION, + lockPeriod: D1, + interest: 0.15 * BILLION, + interestRate: D1, + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLockPeriod: D1, + stakeLimitsMultiplier: 2 * BILLION, + }; + + await expect(doubleSidePool.addDependantSide(dependantSideConfig)).to.be.revertedWith("Second side already exists"); + }); + + }); + + describe("Interaction between Main and Dependant Sides", function () { + beforeEach(async function () { + const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { + token: dependantToken.address, + rewardToken: dependantToken.address, + rewardTokenPrice: BILLION, + minStakeValue: 5, + unstakeLockPeriod: D1, + fastUnstakePenalty: 0.05 * BILLION, + lockPeriod: D1, + interest: 0.15 * BILLION, + interestRate: D1, + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLockPeriod: D1, + stakeLimitsMultiplier: 2, + }; + + await doubleSidePool.addDependantSide(dependantSideConfig); + await dependantToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + }); + + it("Should correctly calculate rewards for both sides", async function () { + const mainStake = 1000; + await doubleSidePool.stake(false, mainStake); + + const dependantStake = 500; + await doubleSidePool.stake(true, dependantStake); + + await time.increase(D1); + await doubleSidePool.onBlock(); + + const mainRewards = await doubleSidePool.getUserMainSideRewards(owner.address); + const dependantRewards = await doubleSidePool.getUserDependantSideRewards(owner.address); + + expect(mainRewards).to.equal(mainStake * 10 / 100 ); // 10% interest + expect(dependantRewards).to.equal(dependantStake * 15 / 100); // 15% interest + }); + + it("Should allow staking and unstaking on both sides independently", async function () { + await doubleSidePool.setStakeLockPeriod(0); + const mainStake = ethers.utils.parseEther("1000"); + await doubleSidePool.stake(false, mainStake); + + const dependantStake = ethers.utils.parseEther("500"); + await doubleSidePool.stake(true, dependantStake); + + await doubleSidePool.unstake(false, mainStake.div(2)); + await doubleSidePool.unstake(true, dependantStake.div(2)); + + const mainStaker = await doubleSidePool.getMainSideStaker(owner.address); + const dependantStaker = await doubleSidePool.getDependantSideStaker(owner.address); + + expect(mainStaker.stake).to.equal(mainStake.div(2)); + expect(dependantStaker.stake).to.equal(dependantStake.div(2)); + }); + + it("Should enforce dependant side limits based on main side stake", async function () { + const mainStake = 1000; + await doubleSidePool.stake(false, mainStake); + + const maxDependantStake = 2000; // 2x multiplier + await doubleSidePool.stake(true, maxDependantStake); + + // Trying to stake more should fail + await expect(doubleSidePool.stake(true, 100)).to.be.revertedWith("Pool: user max stake value exceeded"); + + // Unstake half of main side + await time.increase(D1); + await doubleSidePool.unstake(false, 500); + + // Trying to stake on dependant side should still fail due to existing stake + await expect(doubleSidePool.stake(true, 100)).to.be.revertedWith("Pool: user max stake value exceeded"); + }); + }); + + describe("Edge cases and error handling", function () { + beforeEach(async function () { + const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { + token: dependantToken.address, + rewardToken: dependantToken.address, + rewardTokenPrice: BILLION, + minStakeValue: 5, + unstakeLockPeriod: D1, + fastUnstakePenalty: 0.05 * BILLION, + lockPeriod: D1, + interest: 0.15 * BILLION, + interestRate: D1, + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLockPeriod: D1, + stakeLimitsMultiplier: 2, + }; + + await doubleSidePool.addDependantSide(dependantSideConfig); + await dependantToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); + }); + + it("Should handle zero stakes correctly", async function () { + await expect(doubleSidePool.stake(false, 0)).to.be.revertedWith("Pool: stake value is too low"); + await expect(doubleSidePool.stake(true, 0)).to.be.revertedWith("Pool: stake value is too low"); + }); + + it("Should handle unstaking more than staked amount", async function () { + const stake = ethers.utils.parseEther("100"); + await doubleSidePool.stake(false, stake); + await doubleSidePool.stake(true, stake); + + await time.increase(D1); + + await expect(doubleSidePool.unstake(false, stake.mul(2))).to.be.revertedWith("Not enough stake"); + await expect(doubleSidePool.unstake(true, stake.mul(2))).to.be.revertedWith("Not enough stake"); + }); + + it("Should handle claiming rewards when there are no rewards", async function () { + await doubleSidePool.claim(false); + await doubleSidePool.claim(true); + // These should not revert, but also should not transfer any tokens + }); + + it("Should handle multiple stake and unstake operations correctly", async function () { + await doubleSidePool.setStakeLockPeriod(0); + const stake1 = 100; + const stake2 = 200; + const stake3 = 300; + + await doubleSidePool.stake(false, stake1); + await doubleSidePool.stake(true, stake1); + + await time.increase(D1 / 2); + + await doubleSidePool.stake(false, stake2); + await doubleSidePool.stake(true, stake2); + + await time.increase(D1 / 2); + + await doubleSidePool.unstake(false, stake3); + await doubleSidePool.unstake(true, stake3); + + const mainStaker = await doubleSidePool.getMainSideStaker(owner.address); + const dependantStaker = await doubleSidePool.getDependantSideStaker(owner.address); + + expect(mainStaker.stake).to.equal(stake1 + stake2 - stake3); + expect(dependantStaker.stake).to.equal(stake1 + stake2 - stake3); + }); + }); + +}); From e949009f5305a06a3ea9e64e750191435713d4c3 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 16 Sep 2024 14:32:36 +0300 Subject: [PATCH 35/60] Fix stake lock time --- contracts/staking/token/DoubleSidePool.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index 1cb32792..cb6eff81 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -197,8 +197,6 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { emit StakeLimitsMultiplierChanged(value); } - //TODO: Add more setters - // PUBLIC METHODS function stake(bool dependant, uint amount) public { @@ -412,7 +410,8 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { dependantSideStakers[msg.sender].stake += amount; dependantSideInfo.totalStake += amount; - dependantSideStakers[msg.sender].stakedAt = block.timestamp; + if (dependantSideStakers[msg.sender].stakedAt == 0) + dependantSideStakers[msg.sender].stakedAt = block.timestamp; require(dependantSideStakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); require(dependantSideInfo.totalStake <= dependantSideConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); @@ -433,6 +432,8 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { dependantSideStakers[msg.sender].stake -= amount; dependantSideInfo.totalStake -= amount; + if (dependantSideStakers[msg.sender].stake == 0) dependantSideStakers[msg.sender].stakedAt = 0; + dependantSideInfo.totalRewards -= rewardsAmount; _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); @@ -473,6 +474,8 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { dependantSideStakers[msg.sender].stake -= amount; dependantSideInfo.totalStake -= amount; + if (dependantSideStakers[msg.sender].stake == 0) dependantSideStakers[msg.sender].stakedAt = 0; + dependantSideInfo.totalRewards -= rewardsAmount; _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); From 043bd7b5f40196ad07fdbe839292cd63738fb33f Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 16 Sep 2024 15:37:21 +0300 Subject: [PATCH 36/60] Update contract interface --- contracts/staking/token/DoubleSidePool.sol | 449 ++++++++++----------- test/staking/token/DoubleSidePool.ts | 120 +++--- 2 files changed, 263 insertions(+), 306 deletions(-) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index cb6eff81..302c2493 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -8,8 +8,6 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -import "hardhat/console.sol"; - //The side defined by the address of the token. Zero address means native coin contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { @@ -199,133 +197,64 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { // PUBLIC METHODS - function stake(bool dependant, uint amount) public { - if (dependant) { - _stakeDependantSide(msg.sender, amount); - } else { - _stakeMainSide(msg.sender, amount); - } - } - - function unstake(bool dependant, uint amount) public { - if (dependant) { - _unstakeDependantSide(msg.sender, amount); - } else { - _unstakeMainSide(msg.sender, amount); - } - } - - function unstakeFast(bool dependant, uint amount) public { - if (dependant) { - _unstakeFastDependantSide(msg.sender, amount); - } else { - _unstakeFastMainSide(msg.sender, amount); - } - } - - function claim(bool dependant) public { - if (dependant) { - _calcClaimableRewards(true, msg.sender); - _claimRewards(true, msg.sender); + function stakeMainSide(uint amount) public payable { + require(active, "Pool is not active"); + require(amount >= mainSideConfig.minStakeValue, "Pool: stake value is too low"); + if (msg.value != 0) { + require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); + require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - _calcClaimableRewards(false, msg.sender); - _claimRewards(false, msg.sender); + SafeERC20.safeTransferFrom(IERC20(mainSideConfig.token), msg.sender, address(this), amount); } - } - function onBlock() external { - _addInterestMainSide(); - _addInterestDependantSide(); - } - - // VIEW METHODS - - function getMainSideConfig() public view returns (MainSideConfig memory) { - return mainSideConfig; - } + uint rewardsAmount = _calcRewardsMainSide(amount); - function getMainSideInfo() public view returns (SideInfo memory) { - return mainSideInfo; - } - - function getMainSideStaker(address user) public view returns (SideStaker memory) { - return mainSideStakers[user]; - } - - function getDependantSideConfig() public view returns (DependantSideConfig memory) { - return dependantSideConfig; - } - - function getDependantSideInfo() public view returns (SideInfo memory) { - return dependantSideInfo; - } - - function getDependantSideStaker(address user) public view returns (SideStaker memory) { - return dependantSideStakers[user]; - } - - function getUserMainSideRewards(address user) public view returns (uint) { - uint rewardsAmount = _calcRewards(false, mainSideStakers[user].stake); - if (rewardsAmount + mainSideStakers[user].claimableRewards <= mainSideStakers[user].rewardsDebt) - return 0; - - return rewardsAmount + mainSideStakers[user].claimableRewards - mainSideStakers[user].rewardsDebt; - } - - function getUserDependantSideRewards(address user) public view returns (uint) { - uint rewardsAmount = _calcRewards(true, dependantSideStakers[user].stake); - if (rewardsAmount + dependantSideStakers[user].claimableRewards <= dependantSideStakers[user].rewardsDebt) - return 0; - - return rewardsAmount + dependantSideStakers[user].claimableRewards - dependantSideStakers[user].rewardsDebt; - } - - // INTERNAL METHODS - - // MAIN SIDE METHODS + mainSideStakers[msg.sender].stake += amount; + mainSideInfo.totalStake += amount; - function _addInterestMainSide() internal { - uint timePassed = block.timestamp - mainSideInfo.lastInterestUpdate; - uint newRewards = mainSideInfo.totalStake * mainSideConfig.interest * timePassed / BILLION / mainSideConfig.interestRate; + mainSideInfo.totalRewards += rewardsAmount; - mainSideInfo.totalRewards += newRewards; - mainSideInfo.lastInterestUpdate = block.timestamp; - emit Interest(false, newRewards); + _updateRewardsDebtMainSide(msg.sender, _calcRewardsMainSide(mainSideStakers[msg.sender].stake)); + emit StakeChanged(false, msg.sender, amount); } - - function _stakeMainSide(address user, uint amount) internal { + function stakeDependantSide(uint amount) public payable { require(active, "Pool is not active"); - require(amount >= mainSideConfig.minStakeValue, "Pool: stake value is too low"); + require(amount >= dependantSideConfig.minStakeValue, "Pool: stake value is too low"); if (msg.value != 0) { require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(mainSideConfig.token), msg.sender, address(this), amount); + SafeERC20.safeTransferFrom(IERC20(dependantSideConfig.token), msg.sender, address(this), amount); } - uint rewardsAmount = _calcRewards(false, amount); + uint rewardsAmount = _calcRewardsDependantSide(amount); - mainSideStakers[msg.sender].stake += amount; - mainSideInfo.totalStake += amount; + dependantSideStakers[msg.sender].stake += amount; + dependantSideInfo.totalStake += amount; + if (dependantSideStakers[msg.sender].stakedAt == 0) + dependantSideStakers[msg.sender].stakedAt = block.timestamp; - mainSideInfo.totalRewards += rewardsAmount; + require(dependantSideStakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); + require(dependantSideInfo.totalStake <= dependantSideConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); + require(dependantSideStakers[msg.sender].stake <= dependantSideConfig.maxStakePerUserValue, "Pool: max stake per user exceeded"); - _updateRewardsDebt(false, user, _calcRewards(false, mainSideStakers[user].stake)); - emit StakeChanged(false, msg.sender, amount); - } + dependantSideInfo.totalRewards += rewardsAmount; + _updateRewardsDebtDependantSide(msg.sender, _calcRewardsDependantSide(dependantSideStakers[msg.sender].stake)); + emit StakeChanged(true, msg.sender, amount); + } - function _unstakeMainSide(address user, uint amount) internal { + function unstakeMainSide(uint amount) public { require(mainSideStakers[msg.sender].stake >= amount, "Not enough stake"); - uint rewardsAmount = _calcRewards(false, amount); + uint rewardsAmount = _calcRewardsMainSide(amount); mainSideStakers[msg.sender].stake -= amount; mainSideInfo.totalStake -= amount; mainSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebt(false, user, _calcRewards(false, mainSideStakers[user].stake)); + _updateRewardsDebtMainSide(msg.sender, _calcRewardsMainSide(mainSideStakers[msg.sender].stake)); // cancel previous lock (if exists). canceledAmount will be added to new lock uint canceledAmount; @@ -347,87 +276,17 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { ); } - - _claimRewards(false, msg.sender); + _claimRewardsMainSide(msg.sender); emit UnstakeLocked(false, msg.sender, amount + canceledAmount, block.timestamp + mainSideConfig.lockPeriod, block.timestamp); emit StakeChanged(false, msg.sender, mainSideStakers[msg.sender].stake); } - - function _unstakeFastMainSide(address user, uint amount) internal { - require(mainSideStakers[msg.sender].stake >= amount, "Not enough stake"); - - uint rewardsAmount = _calcRewards(false, amount); - - mainSideStakers[msg.sender].stake -= amount; - mainSideInfo.totalStake -= amount; - - mainSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebt(false, user, _calcRewards(false, mainSideStakers[user].stake)); - - uint penalty = amount * mainSideConfig.fastUnstakePenalty / BILLION; - if (mainSideConfig.token == address(0)) { - payable(msg.sender).transfer(amount - penalty); - } else { - SafeERC20.safeTransfer(IERC20(mainSideConfig.token), msg.sender, amount - penalty); - } - - _claimRewards(false, msg.sender); - - emit UnstakeFast(false, msg.sender, amount, penalty); - emit StakeChanged(false, msg.sender, amount); - } - - // DEPENDANT SIDE METHODS - - function _addInterestDependantSide() internal { - if (!hasSecondSide) return; - if (dependantSideInfo.lastInterestUpdate + dependantSideConfig.interestRate > block.timestamp) return; - uint timePassed = block.timestamp - dependantSideInfo.lastInterestUpdate; - uint newRewards = dependantSideInfo.totalStake * dependantSideConfig.interest * timePassed / BILLION / dependantSideConfig.interestRate; - - dependantSideInfo.totalRewards += newRewards; - dependantSideInfo.lastInterestUpdate = block.timestamp; - emit Interest(true, newRewards); - } - - function _stakeDependantSide(address user, uint amount) internal { - require(active, "Pool is not active"); - require(amount >= dependantSideConfig.minStakeValue, "Pool: stake value is too low"); - if (msg.value != 0) { - require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); - require(msg.value == amount, "Pool: wrong amount of native coin"); - } else { - SafeERC20.safeTransferFrom(IERC20(dependantSideConfig.token), msg.sender, address(this), amount); - } - - console.log("Staking dependant side"); - console.log("max user stake: ", _maxUserStakeValue(msg.sender)); - console.log("amount: ", amount); - - uint rewardsAmount = _calcRewards(true, amount); - - dependantSideStakers[msg.sender].stake += amount; - dependantSideInfo.totalStake += amount; - if (dependantSideStakers[msg.sender].stakedAt == 0) - dependantSideStakers[msg.sender].stakedAt = block.timestamp; - - require(dependantSideStakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); - require(dependantSideInfo.totalStake <= dependantSideConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); - require(dependantSideStakers[msg.sender].stake <= dependantSideConfig.maxStakePerUserValue, "Pool: max stake per user exceeded"); - - dependantSideInfo.totalRewards += rewardsAmount; - - _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); - emit StakeChanged(true, msg.sender, amount); - } - - function _unstakeDependantSide(address user, uint amount) internal { + function unstakeDependantSide(uint amount) public { require(dependantSideStakers[msg.sender].stake >= amount, "Not enough stake"); require(block.timestamp - dependantSideStakers[msg.sender].stakedAt >= dependantSideConfig.stakeLockPeriod, "Stake is locked"); - uint rewardsAmount = _calcRewards(true, amount); + uint rewardsAmount = _calcRewardsDependantSide(amount); dependantSideStakers[msg.sender].stake -= amount; dependantSideInfo.totalStake -= amount; @@ -435,7 +294,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { if (dependantSideStakers[msg.sender].stake == 0) dependantSideStakers[msg.sender].stakedAt = 0; dependantSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); + _updateRewardsDebtDependantSide(msg.sender, _calcRewardsDependantSide(dependantSideStakers[msg.sender].stake)); // cancel previous lock (if exists). canceledAmount will be added to new lock uint canceledAmount; @@ -458,18 +317,42 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { } - _claimRewards(true, msg.sender); + _claimRewardsDependantSide(msg.sender); emit UnstakeLocked(true, msg.sender, amount + canceledAmount, block.timestamp + mainSideConfig.lockPeriod, block.timestamp); emit StakeChanged(true, msg.sender, dependantSideStakers[msg.sender].stake); } - - function _unstakeFastDependantSide(address user, uint amount) internal { + + function unstakeFastMainSide(uint amount) public { + require(mainSideStakers[msg.sender].stake >= amount, "Not enough stake"); + + uint rewardsAmount = _calcRewardsMainSide(amount); + + mainSideStakers[msg.sender].stake -= amount; + mainSideInfo.totalStake -= amount; + + mainSideInfo.totalRewards -= rewardsAmount; + _updateRewardsDebtMainSide(msg.sender, _calcRewardsMainSide(mainSideStakers[msg.sender].stake)); + + uint penalty = amount * mainSideConfig.fastUnstakePenalty / BILLION; + if (mainSideConfig.token == address(0)) { + payable(msg.sender).transfer(amount - penalty); + } else { + SafeERC20.safeTransfer(IERC20(mainSideConfig.token), msg.sender, amount - penalty); + } + + _claimRewardsMainSide(msg.sender); + + emit UnstakeFast(false, msg.sender, amount, penalty); + emit StakeChanged(false, msg.sender, amount); + } + + function unstakeFastDependantSide(uint amount) public { require(dependantSideStakers[msg.sender].stake >= amount, "Not enough stake"); require(block.timestamp - dependantSideStakers[msg.sender].stakedAt >= dependantSideConfig.stakeLockPeriod, "Stake is locked"); - uint rewardsAmount = _calcRewards(true, amount); + uint rewardsAmount = _calcRewardsDependantSide(amount); dependantSideStakers[msg.sender].stake -= amount; dependantSideInfo.totalStake -= amount; @@ -477,7 +360,7 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { if (dependantSideStakers[msg.sender].stake == 0) dependantSideStakers[msg.sender].stakedAt = 0; dependantSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebt(true, user, _calcRewards(true, dependantSideStakers[user].stake)); + _updateRewardsDebtDependantSide(msg.sender, _calcRewardsDependantSide(dependantSideStakers[msg.sender].stake)); uint penalty = amount * dependantSideConfig.fastUnstakePenalty / BILLION; if (dependantSideConfig.token == address(0)) { @@ -486,90 +369,164 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { SafeERC20.safeTransfer(IERC20(dependantSideConfig.token), msg.sender, amount - penalty); } - _claimRewards(true, msg.sender); + _claimRewardsDependantSide(msg.sender); emit UnstakeFast(true, msg.sender, amount, penalty); emit StakeChanged(true, msg.sender, amount); } + function claimMainSide() public { + _calcClaimableRewardsMainSide(msg.sender); + _claimRewardsMainSide(msg.sender); + } + + function claimDependantSide() public { + _calcClaimableRewardsDependantSide(msg.sender); + _claimRewardsDependantSide(msg.sender); + } + + function onBlock() external { + _addInterestMainSide(); + _addInterestDependantSide(); + } + + // VIEW METHODS + + function getMainSideConfig() public view returns (MainSideConfig memory) { + return mainSideConfig; + } + + function getMainSideInfo() public view returns (SideInfo memory) { + return mainSideInfo; + } + + function getMainSideStaker(address user) public view returns (SideStaker memory) { + return mainSideStakers[user]; + } + + function getDependantSideConfig() public view returns (DependantSideConfig memory) { + return dependantSideConfig; + } + + function getDependantSideInfo() public view returns (SideInfo memory) { + return dependantSideInfo; + } + + function getDependantSideStaker(address user) public view returns (SideStaker memory) { + return dependantSideStakers[user]; + } + + function getUserMainSideRewards(address user) public view returns (uint) { + uint rewardsAmount = _calcRewardsMainSide(mainSideStakers[user].stake); + if (rewardsAmount + mainSideStakers[user].claimableRewards <= mainSideStakers[user].rewardsDebt) + return 0; + + return rewardsAmount + mainSideStakers[user].claimableRewards - mainSideStakers[user].rewardsDebt; + } + + function getUserDependantSideRewards(address user) public view returns (uint) { + uint rewardsAmount = _calcRewardsDependantSide(dependantSideStakers[user].stake); + if (rewardsAmount + dependantSideStakers[user].claimableRewards <= dependantSideStakers[user].rewardsDebt) + return 0; + + return rewardsAmount + dependantSideStakers[user].claimableRewards - dependantSideStakers[user].rewardsDebt; + } + + // INTERNAL METHODS + + function _addInterestMainSide() internal { + uint timePassed = block.timestamp - mainSideInfo.lastInterestUpdate; + uint newRewards = mainSideInfo.totalStake * mainSideConfig.interest * timePassed / BILLION / mainSideConfig.interestRate; + + mainSideInfo.totalRewards += newRewards; + mainSideInfo.lastInterestUpdate = block.timestamp; + emit Interest(false, newRewards); + } + + function _addInterestDependantSide() internal { + if (!hasSecondSide) return; + if (dependantSideInfo.lastInterestUpdate + dependantSideConfig.interestRate > block.timestamp) return; + uint timePassed = block.timestamp - dependantSideInfo.lastInterestUpdate; + uint newRewards = dependantSideInfo.totalStake * dependantSideConfig.interest * timePassed / BILLION / dependantSideConfig.interestRate; + + dependantSideInfo.totalRewards += newRewards; + dependantSideInfo.lastInterestUpdate = block.timestamp; + emit Interest(true, newRewards); + } + function _maxUserStakeValue(address user) internal view returns (uint) { - console.log("main side stake: ", mainSideStakers[user].stake); - console.log("stake limits multiplier: ", dependantSideConfig.stakeLimitsMultiplier); return mainSideStakers[user].stake * dependantSideConfig.stakeLimitsMultiplier; } - //COMMON METHODS - // store claimable rewards - function _calcClaimableRewards(bool dependant, address user) internal { - if (dependant) { - uint rewardsAmount = _calcRewards(dependant, dependantSideStakers[user].stake); - uint rewardsWithoutDebt = rewardsAmount - dependantSideStakers[user].rewardsDebt; - dependantSideStakers[user].claimableRewards += rewardsWithoutDebt; - dependantSideInfo.totalRewardsDebt += rewardsWithoutDebt; - dependantSideStakers[user].rewardsDebt += rewardsWithoutDebt; - } else { - uint rewardsAmount = _calcRewards(dependant, mainSideStakers[user].stake); - uint rewardsWithoutDebt = rewardsAmount - mainSideStakers[user].rewardsDebt; - mainSideStakers[user].claimableRewards += rewardsWithoutDebt; - mainSideInfo.totalRewardsDebt += rewardsWithoutDebt; - mainSideStakers[user].rewardsDebt += rewardsWithoutDebt; - } + function _calcClaimableRewardsMainSide(address user) internal { + uint rewardsAmount = _calcRewardsMainSide(mainSideStakers[user].stake); + uint rewardsWithoutDebt = rewardsAmount - mainSideStakers[user].rewardsDebt; + mainSideStakers[user].claimableRewards += rewardsWithoutDebt; + mainSideInfo.totalRewardsDebt += rewardsWithoutDebt; + mainSideStakers[user].rewardsDebt += rewardsWithoutDebt; } - function _claimRewards(bool dependant, address user) internal { - if (dependant) { - uint amount = dependantSideStakers[user].claimableRewards; - if (amount == 0) return; - - dependantSideStakers[user].claimableRewards = 0; - - uint rewardTokenAmount = amount * dependantSideConfig.rewardTokenPrice; - if (dependantSideConfig.rewardToken == address(0)) { - rewardsBank.withdrawAmb(payable(user), amount); - } else { - rewardsBank.withdrawErc20(dependantSideConfig.rewardToken, payable(user), rewardTokenAmount); - } - emit Claim(true, user, rewardTokenAmount); - } else { - uint amount = mainSideStakers[user].claimableRewards; - if (amount == 0) return; - - mainSideStakers[user].claimableRewards = 0; - - uint rewardTokenAmount = amount * mainSideConfig.rewardTokenPrice; - if (mainSideConfig.rewardToken == address(0)) { - rewardsBank.withdrawAmb(payable(user), amount); - } else { - rewardsBank.withdrawErc20(mainSideConfig.rewardToken, payable(user), rewardTokenAmount); - } - emit Claim(false, user, rewardTokenAmount); - } + function _calcClaimableRewardsDependantSide(address user) internal { + uint rewardsAmount = _calcRewardsDependantSide(dependantSideStakers[user].stake); + uint rewardsWithoutDebt = rewardsAmount - dependantSideStakers[user].rewardsDebt; + dependantSideStakers[user].claimableRewards += rewardsWithoutDebt; + dependantSideInfo.totalRewardsDebt += rewardsWithoutDebt; + dependantSideStakers[user].rewardsDebt += rewardsWithoutDebt; } - function _calcRewards(bool dependant, uint amount) internal view returns (uint) { - if (dependant) { - if (dependantSideInfo.totalStake == 0 && dependantSideInfo.totalRewards == 0) return amount; - return amount * dependantSideInfo.totalRewards / dependantSideInfo.totalStake; + function _claimRewardsMainSide(address user) internal { + uint amount = mainSideStakers[user].claimableRewards; + if (amount == 0) return; + + mainSideStakers[user].claimableRewards = 0; + + uint rewardTokenAmount = amount * mainSideConfig.rewardTokenPrice; + if (mainSideConfig.rewardToken == address(0)) { + rewardsBank.withdrawAmb(payable(user), amount); } else { - if (mainSideInfo.totalStake == 0 && mainSideInfo.totalRewards == 0) return amount; - return amount * mainSideInfo.totalRewards / mainSideInfo.totalStake; + rewardsBank.withdrawErc20(mainSideConfig.rewardToken, payable(user), rewardTokenAmount); } + emit Claim(false, user, rewardTokenAmount); } + function _claimRewardsDependantSide(address user) internal { + uint amount = dependantSideStakers[user].claimableRewards; + if (amount == 0) return; - function _updateRewardsDebt(bool dependant, address user, uint newDebt) internal { - if (dependant) { - uint oldDebt = dependantSideStakers[user].rewardsDebt; - if (newDebt < oldDebt) dependantSideInfo.totalRewardsDebt -= oldDebt - newDebt; - else dependantSideInfo.totalRewardsDebt += newDebt - oldDebt; - dependantSideStakers[user].rewardsDebt = newDebt; - } else { - uint oldDebt = mainSideStakers[user].rewardsDebt; - if (newDebt < oldDebt) mainSideInfo.totalRewardsDebt -= oldDebt - newDebt; - else mainSideInfo.totalRewardsDebt += newDebt - oldDebt; - mainSideStakers[user].rewardsDebt = newDebt; - } + dependantSideStakers[user].claimableRewards = 0; + + uint rewardTokenAmount = amount * dependantSideConfig.rewardTokenPrice; + if (dependantSideConfig.rewardToken == address(0)) { + rewardsBank.withdrawAmb(payable(user), amount); + } else { + rewardsBank.withdrawErc20(dependantSideConfig.rewardToken, payable(user), rewardTokenAmount); + } + emit Claim(true, user, rewardTokenAmount); + } + + function _calcRewardsMainSide(uint amount) internal view returns (uint) { + if (mainSideInfo.totalStake == 0 && mainSideInfo.totalRewards == 0) return amount; + return amount * mainSideInfo.totalRewards / mainSideInfo.totalStake; + } + + function _calcRewardsDependantSide(uint amount) internal view returns (uint) { + if (dependantSideInfo.totalStake == 0 && dependantSideInfo.totalRewards == 0) return amount; + return amount * dependantSideInfo.totalRewards / dependantSideInfo.totalStake; + } + + function _updateRewardsDebtMainSide(address user, uint newDebt) internal { + uint oldDebt = mainSideStakers[user].rewardsDebt; + if (newDebt < oldDebt) mainSideInfo.totalRewardsDebt -= oldDebt - newDebt; + else mainSideInfo.totalRewardsDebt += newDebt - oldDebt; + mainSideStakers[user].rewardsDebt = newDebt; + } + + function _updateRewardsDebtDependantSide(address user, uint newDebt) internal { + uint oldDebt = dependantSideStakers[user].rewardsDebt; + if (newDebt < oldDebt) dependantSideInfo.totalRewardsDebt -= oldDebt - newDebt; + else dependantSideInfo.totalRewardsDebt += newDebt - oldDebt; + dependantSideStakers[user].rewardsDebt = newDebt; } function _addressToString(address x) internal pure returns (string memory) { diff --git a/test/staking/token/DoubleSidePool.ts b/test/staking/token/DoubleSidePool.ts index 2d6713a3..c4967480 100644 --- a/test/staking/token/DoubleSidePool.ts +++ b/test/staking/token/DoubleSidePool.ts @@ -139,7 +139,7 @@ describe("DoubleSidePool", function () { it("Should allow staking on the main side", async function () { const stake = ethers.utils.parseEther("1000"); - await doubleSidePool.stake(false, stake); + await doubleSidePool.stakeMainSide(stake); const info = await doubleSidePool.mainSideInfo(); expect(info.totalStake).to.equal(stake); const staker = await doubleSidePool.getMainSideStaker(owner.address); @@ -148,11 +148,11 @@ describe("DoubleSidePool", function () { it("Should not allow staking when pool is deactivated", async function () { await doubleSidePool.deactivate(); - await expect(doubleSidePool.stake(false, 1000)).to.be.revertedWith("Pool is not active"); + await expect(doubleSidePool.stakeMainSide(1000)).to.be.revertedWith("Pool is not active"); }); it("Should not allow staking below minimum stake value", async function () { - await expect(doubleSidePool.stake(false, 1)).to.be.revertedWith("Pool: stake value is too low"); + await expect(doubleSidePool.stakeMainSide(1)).to.be.revertedWith("Pool: stake value is too low"); }); }); @@ -161,14 +161,14 @@ describe("DoubleSidePool", function () { beforeEach(async function () { await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - await doubleSidePool.stake(false, stake); + await doubleSidePool.stakeMainSide(stake); }); it("Should allow unstaking with rewards", async function () { await time.increase(D1); await doubleSidePool.onBlock(); - await expect(doubleSidePool.unstake(false, stake)).to.emit(lockKeeper, "Locked"); + await expect(doubleSidePool.unstakeMainSide(stake)).to.emit(lockKeeper, "Locked"); const info = await doubleSidePool.mainSideInfo(); expect(info.totalStake).to.equal(0); const staker = await doubleSidePool.getMainSideStaker(owner.address); @@ -180,18 +180,18 @@ describe("DoubleSidePool", function () { await doubleSidePool.onBlock(); const balanceBefore = await mainToken.balanceOf(owner.address); - await doubleSidePool.unstakeFast(false, stake); + await doubleSidePool.unstakeFastMainSide(stake); const balanceAfter = await mainToken.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake.mul(90).div(100)); // 90% due to 10% penalty }); it("Should not allow unstaking more than staked", async function () { - await expect(doubleSidePool.unstake(false, stake.mul(2))).to.be.revertedWith("Not enough stake"); + await expect(doubleSidePool.unstakeMainSide(stake.mul(2))).to.be.revertedWith("Not enough stake"); }); it("Should allow unstaking when pool is deactivated", async function () { await doubleSidePool.deactivate(); - await expect(doubleSidePool.unstake(false, stake)).to.emit(lockKeeper, "Locked"); + await expect(doubleSidePool.unstakeMainSide(stake)).to.emit(lockKeeper, "Locked"); }); }); @@ -202,7 +202,7 @@ describe("DoubleSidePool", function () { }); it("Should allow claiming rewards", async function () { - await doubleSidePool.stake(false, 1000); + await doubleSidePool.stakeMainSide(1000); await time.increase(D1); await doubleSidePool.onBlock(); @@ -213,13 +213,13 @@ describe("DoubleSidePool", function () { expect(rewards).to.equal(expectedReward); const balanceBefore = await mainToken.balanceOf(owner.address); - await doubleSidePool.claim(false); + await doubleSidePool.claimMainSide(); const balanceAfter = await mainToken.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); }); it("Should allow claiming rewards when pool is deactivated", async function () { - await doubleSidePool.stake(false, 1000); + await doubleSidePool.stakeMainSide(1000); await time.increase(D1); await doubleSidePool.onBlock(); @@ -231,7 +231,7 @@ describe("DoubleSidePool", function () { expect(rewards).to.equal(expectedReward); const balanceBefore = await mainToken.balanceOf(owner.address); - await doubleSidePool.claim(false); + await doubleSidePool.claimMainSide(); const balanceAfter = await mainToken.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); }); @@ -264,10 +264,10 @@ describe("DoubleSidePool", function () { it("Should allow staking on the dependant side", async function () { const mainStake = 100000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const dependantStake = 100; - await doubleSidePool.stake(true, dependantStake); + await doubleSidePool.stakeDependantSide(dependantStake); const info = await doubleSidePool.dependantSideInfo(); expect(info.totalStake).to.equal(dependantStake); @@ -277,24 +277,24 @@ describe("DoubleSidePool", function () { it("Should not allow staking on dependant side beyond the limit", async function () { const mainStake = 1000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide( mainStake); const dependantStake = ethers.utils.parseEther("2001"); // Exceeds 2x main stake - await expect(doubleSidePool.stake(true, dependantStake)).to.be.revertedWith("Pool: user max stake value exceeded"); + await expect(doubleSidePool.stakeDependantSide(dependantStake)).to.be.revertedWith("Pool: user max stake value exceeded"); }); it("Should allow unstaking from the dependant side", async function () { await doubleSidePool.setStakeLockPeriod(0); const mainStake = 1000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const dependantStake = 100; - await doubleSidePool.stake(true, dependantStake); + await doubleSidePool.stakeDependantSide(dependantStake); await time.increase(D1); await doubleSidePool.onBlock(); - await expect(doubleSidePool.unstake(true, dependantStake)).to.emit(lockKeeper, "Locked"); + await expect(doubleSidePool.unstakeDependantSide(dependantStake)).to.emit(lockKeeper, "Locked"); const info = await doubleSidePool.dependantSideInfo(); expect(info.totalStake).to.equal(0); const staker = await doubleSidePool.getDependantSideStaker(owner.address); @@ -303,10 +303,10 @@ describe("DoubleSidePool", function () { it("Should allow claiming rewards from the dependant side", async function () { const mainStake = 1000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide( mainStake); const dependantStake = 100; - await doubleSidePool.stake(true, dependantStake); + await doubleSidePool.stakeDependantSide(dependantStake); await time.increase(D1); await doubleSidePool.onBlock(); @@ -316,7 +316,7 @@ describe("DoubleSidePool", function () { expect(rewards).to.equal(expectedReward); const balanceBefore = await dependantToken.balanceOf(owner.address); - await doubleSidePool.claim(true); + await doubleSidePool.claimDependantSide(); const balanceAfter = await dependantToken.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); }); @@ -324,37 +324,37 @@ describe("DoubleSidePool", function () { it("Should not allow staking on dependant side when total stake limit is reached", async function () { await doubleSidePool.setMaxTotalStakeValue(1000); const mainStake = 100000; // Set a high main stake to avoid individual limit - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const maxStake = 1000; - await doubleSidePool.stake(true, maxStake); + await doubleSidePool.stakeDependantSide(maxStake); - await expect(doubleSidePool.stake(true, 10)).to.be.revertedWith("Pool: max stake value exceeded"); + await expect(doubleSidePool.stakeDependantSide(10)).to.be.revertedWith("Pool: max stake value exceeded"); }); it("Should not allow unstaking from dependant side before stake lock period", async function () { const mainStake = 1000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const dependantStake = 100; - await doubleSidePool.stake(true, dependantStake); + await doubleSidePool.stakeDependantSide(dependantStake); - await expect(doubleSidePool.unstake(true, dependantStake)).to.be.revertedWith("Stake is locked"); + await expect(doubleSidePool.unstakeDependantSide(dependantStake)).to.be.revertedWith("Stake is locked"); }); it("Should allow fast unstaking from dependant side with penalty", async function () { await doubleSidePool.setStakeLockPeriod(0); const mainStake = 1000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const dependantStake = 100; - await doubleSidePool.stake(true, dependantStake); + await doubleSidePool.stakeDependantSide(dependantStake); //await time.increase(D1); //await doubleSidePool.onBlock(); const balanceBefore = await dependantToken.balanceOf(owner.address); - await doubleSidePool.unstakeFast(true, dependantStake); + await doubleSidePool.unstakeFastDependantSide(dependantStake); const balanceAfter = await dependantToken.balanceOf(owner.address); const expectedReturn = 95; // 95% due to 5% penalty @@ -363,18 +363,18 @@ describe("DoubleSidePool", function () { it("Should update stake limits when main side stake changes", async function () { const initialMainStake = 1000; - await doubleSidePool.stake(false, initialMainStake); + await doubleSidePool.stakeMainSide(initialMainStake); const maxDependantStake = 2000; // 2x multiplier - await doubleSidePool.stake(true, maxDependantStake); + await doubleSidePool.stakeDependantSide(maxDependantStake); // Increase main stake const additionalMainStake = 100; - await doubleSidePool.stake(false, additionalMainStake); + await doubleSidePool.stakeMainSide(additionalMainStake); // Now we should be able to stake more on the dependant side const additionalDependantStake = 200; - await expect(doubleSidePool.stake(true, additionalDependantStake)).to.not.be.reverted; + await expect(doubleSidePool.stakeDependantSide(additionalDependantStake)).to.not.be.reverted; }); it("Should not allow adding dependant side twice", async function () { @@ -424,10 +424,10 @@ describe("DoubleSidePool", function () { it("Should correctly calculate rewards for both sides", async function () { const mainStake = 1000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const dependantStake = 500; - await doubleSidePool.stake(true, dependantStake); + await doubleSidePool.stakeDependantSide(dependantStake); await time.increase(D1); await doubleSidePool.onBlock(); @@ -442,13 +442,13 @@ describe("DoubleSidePool", function () { it("Should allow staking and unstaking on both sides independently", async function () { await doubleSidePool.setStakeLockPeriod(0); const mainStake = ethers.utils.parseEther("1000"); - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const dependantStake = ethers.utils.parseEther("500"); - await doubleSidePool.stake(true, dependantStake); + await doubleSidePool.stakeDependantSide(dependantStake); - await doubleSidePool.unstake(false, mainStake.div(2)); - await doubleSidePool.unstake(true, dependantStake.div(2)); + await doubleSidePool.unstakeMainSide(mainStake.div(2)); + await doubleSidePool.unstakeDependantSide(dependantStake.div(2)); const mainStaker = await doubleSidePool.getMainSideStaker(owner.address); const dependantStaker = await doubleSidePool.getDependantSideStaker(owner.address); @@ -459,20 +459,20 @@ describe("DoubleSidePool", function () { it("Should enforce dependant side limits based on main side stake", async function () { const mainStake = 1000; - await doubleSidePool.stake(false, mainStake); + await doubleSidePool.stakeMainSide(mainStake); const maxDependantStake = 2000; // 2x multiplier - await doubleSidePool.stake(true, maxDependantStake); + await doubleSidePool.stakeDependantSide(maxDependantStake); // Trying to stake more should fail - await expect(doubleSidePool.stake(true, 100)).to.be.revertedWith("Pool: user max stake value exceeded"); + await expect(doubleSidePool.stakeDependantSide(100)).to.be.revertedWith("Pool: user max stake value exceeded"); // Unstake half of main side await time.increase(D1); - await doubleSidePool.unstake(false, 500); + await doubleSidePool.unstakeMainSide(500); // Trying to stake on dependant side should still fail due to existing stake - await expect(doubleSidePool.stake(true, 100)).to.be.revertedWith("Pool: user max stake value exceeded"); + await expect(doubleSidePool.stakeDependantSide(100)).to.be.revertedWith("Pool: user max stake value exceeded"); }); }); @@ -500,24 +500,24 @@ describe("DoubleSidePool", function () { }); it("Should handle zero stakes correctly", async function () { - await expect(doubleSidePool.stake(false, 0)).to.be.revertedWith("Pool: stake value is too low"); - await expect(doubleSidePool.stake(true, 0)).to.be.revertedWith("Pool: stake value is too low"); + await expect(doubleSidePool.stakeMainSide(0)).to.be.revertedWith("Pool: stake value is too low"); + await expect(doubleSidePool.stakeDependantSide(0)).to.be.revertedWith("Pool: stake value is too low"); }); it("Should handle unstaking more than staked amount", async function () { const stake = ethers.utils.parseEther("100"); - await doubleSidePool.stake(false, stake); - await doubleSidePool.stake(true, stake); + await doubleSidePool.stakeMainSide(stake); + await doubleSidePool.stakeDependantSide(stake); await time.increase(D1); - await expect(doubleSidePool.unstake(false, stake.mul(2))).to.be.revertedWith("Not enough stake"); - await expect(doubleSidePool.unstake(true, stake.mul(2))).to.be.revertedWith("Not enough stake"); + await expect(doubleSidePool.unstakeMainSide(stake.mul(2))).to.be.revertedWith("Not enough stake"); + await expect(doubleSidePool.unstakeDependantSide(stake.mul(2))).to.be.revertedWith("Not enough stake"); }); it("Should handle claiming rewards when there are no rewards", async function () { - await doubleSidePool.claim(false); - await doubleSidePool.claim(true); + await doubleSidePool.claimMainSide(); + await doubleSidePool.claimDependantSide(); // These should not revert, but also should not transfer any tokens }); @@ -527,18 +527,18 @@ describe("DoubleSidePool", function () { const stake2 = 200; const stake3 = 300; - await doubleSidePool.stake(false, stake1); - await doubleSidePool.stake(true, stake1); + await doubleSidePool.stakeMainSide(stake1); + await doubleSidePool.stakeDependantSide(stake1); await time.increase(D1 / 2); - await doubleSidePool.stake(false, stake2); - await doubleSidePool.stake(true, stake2); + await doubleSidePool.stakeMainSide(stake2); + await doubleSidePool.stakeDependantSide(stake2); await time.increase(D1 / 2); - await doubleSidePool.unstake(false, stake3); - await doubleSidePool.unstake(true, stake3); + await doubleSidePool.unstakeMainSide(stake3); + await doubleSidePool.unstakeDependantSide(stake3); const mainStaker = await doubleSidePool.getMainSideStaker(owner.address); const dependantStaker = await doubleSidePool.getDependantSideStaker(owner.address); From 0bc673deccd7169d97a3dee9f6b3cf5f8d9ce5cf Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 16 Sep 2024 20:30:12 +0300 Subject: [PATCH 37/60] Update pool --- contracts/staking/token/DoubleSidePool.sol | 509 ++++++--------------- 1 file changed, 149 insertions(+), 360 deletions(-) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DoubleSidePool.sol index 302c2493..c0874a4c 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DoubleSidePool.sol @@ -9,45 +9,37 @@ import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; //The side defined by the address of the token. Zero address means native coin -contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { +contract DepositedPool is Initializable, AccessControl, IOnBlockListener { - struct MainSideConfig { - address token; - address rewardToken; - uint rewardTokenPrice; - uint minStakeValue; - uint unstakeLockPeriod; - uint fastUnstakePenalty; - uint lockPeriod; - uint interest; - uint interestRate; - } - - struct DependantSideConfig { - address token; + struct Config { + string name; + address depositToken; + uint minDepositValue; + address profitableToken; address rewardToken; uint rewardTokenPrice; uint minStakeValue; uint unstakeLockPeriod; // Time in seconds to how long the amount is locker after unstake - uint fastUnstakePenalty; uint lockPeriod; uint interest; uint interestRate; uint maxTotalStakeValue; uint maxStakePerUserValue; uint stakeLockPeriod; // Time in seconds to how long the stake is locker before unstake - uint stakeLimitsMultiplier; + uint stakeLimitsMultiplier; // Should be represented as parts of BILLION } - struct SideInfo { + struct Info { uint totalStake; + uint totalDeposit; uint totalRewards; uint lastInterestUpdate; uint totalRewardsDebt; } - struct SideStaker { + struct Staker { uint stake; + uint deposit; uint rewardsDebt; uint claimableRewards; uint lockedWithdrawal; @@ -61,63 +53,48 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { string public name; bool public active; - bool public hasSecondSide; - MainSideConfig public mainSideConfig; - SideInfo public mainSideInfo; - DependantSideConfig public dependantSideConfig; - SideInfo public dependantSideInfo; + Config public config; + Info public info; - mapping(address => SideStaker) public mainSideStakers; - mapping(address => SideStaker) public dependantSideStakers; + mapping(address => Staker) public stakers; //EVENTS event Deactivated(); event Activated(); - event MinStakeValueChanged(bool dependant, uint minStakeValue); - event UnstakeLockePeriodChanged(bool dependant, uint period); - event RewardTokenPriceChanged(bool dependant, uint price); - event FastUnstakePenaltyChanged(bool dependant, uint penalty); - event InterestChanged(bool dependant, uint interest); - event InterestRateChanged(bool dependant, uint interestRate); - + event MinStakeValueChanged(uint minStakeValue); + event UnstakeLockePeriodChanged(uint period); + event RewardTokenPriceChanged(uint price); + event FastUnstakePenaltyChanged(uint penalty); + event InterestChanged(uint interest); + event InterestRateChanged(uint interestRate); event MaxTotalStakeValueChanged(uint poolMaxStakeValue); event MaxStakePerUserValueChanged(uint maxStakePerUserValue); event StakeLockPeriodChanged(uint period); event StakeLimitsMultiplierChanged(uint value); - event StakeChanged(bool dependant, address indexed user, uint amount); - event Claim(bool dependant, address indexed user, uint amount); - event Interest(bool dependant, uint amount); - event UnstakeLocked(bool dependant, address indexed user, uint amount, uint unlockTime, uint creationTime); - event UnstakeFast(bool dependant, address indexed user, uint amount, uint penalty); + event DepositChanged(address indexed user, uint amount); + event StakeChanged(address indexed user, uint amount); + event Claim(address indexed user, uint amount); + event Interest(uint amount); + event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); function initialize( - RewardsBank rewardsBank_, LockKeeper lockkeeper_, string memory name_, - MainSideConfig memory mainSideConfig_ + RewardsBank rewardsBank_, LockKeeper lockkeeper_, Config calldata config_ ) public initializer { lockKeeper = lockkeeper_; rewardsBank = rewardsBank_; - name = name_; + active = true; - hasSecondSide = false; - mainSideConfig = mainSideConfig_; - mainSideInfo.lastInterestUpdate = block.timestamp; + config = config_; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } // OWNER METHODS - function addDependantSide(DependantSideConfig calldata config_) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(!hasSecondSide, "Second side already exists"); - hasSecondSide = true; - dependantSideConfig = config_; - dependantSideInfo.lastInterestUpdate = block.timestamp; - } - function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { require(!active, "Pool is already active"); active = true; @@ -132,417 +109,229 @@ contract DoubleSidePool is Initializable, AccessControl, IOnBlockListener { // SETTERS FOR PARAMS - function setMinStakeValue(bool dependant, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - if (dependant) { - dependantSideConfig.minStakeValue = value; - } else { - mainSideConfig.minStakeValue = value; - } - emit MinStakeValueChanged(dependant, value); + function setMinStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + config.minStakeValue = value; + emit MinStakeValueChanged(value); } - function setInterest(bool dependant, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { - if (dependant) { - dependantSideConfig.interest = _interest; - dependantSideConfig.interestRate = _interestRate; - } else { - mainSideConfig.interest = _interest; - mainSideConfig.interestRate = _interestRate; - } - emit InterestRateChanged(dependant, _interestRate); - emit InterestChanged(dependant, _interest); - } + function setInterest(uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { + config.interest = _interest; + config.interestRate = _interestRate; - function setUnstakeLockPeriod(bool dependant, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - if (dependant) { - dependantSideConfig.unstakeLockPeriod = period; - } else { - mainSideConfig.unstakeLockPeriod = period; - } - emit UnstakeLockePeriodChanged(dependant, period); + emit InterestRateChanged(_interestRate); + emit InterestChanged(_interest); } - function setRewardTokenPrice(bool dependant, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { - if (dependant) { - dependantSideConfig.rewardTokenPrice = price; - } else { - mainSideConfig.rewardTokenPrice = price; - } - emit RewardTokenPriceChanged(dependant, price); + function setUnstakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + config.unstakeLockPeriod = period; + emit UnstakeLockePeriodChanged(period); } - function setFastUnstakePenalty(bool dependant, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { - if (dependant) { - dependantSideConfig.fastUnstakePenalty = penalty; - } else { - mainSideConfig.fastUnstakePenalty = penalty; - } - emit FastUnstakePenaltyChanged(dependant, penalty); + function setRewardTokenPrice(uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { + config.rewardTokenPrice = price; + emit RewardTokenPriceChanged(price); } function setMaxTotalStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - dependantSideConfig.maxTotalStakeValue = value; + config.maxTotalStakeValue = value; emit MaxTotalStakeValueChanged(value); } function setStakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - dependantSideConfig.stakeLockPeriod = period; + config.stakeLockPeriod = period; emit StakeLockPeriodChanged(period); } function setStakeLimitsMultiplier(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - dependantSideConfig.stakeLimitsMultiplier = value; + config.stakeLimitsMultiplier = value; emit StakeLimitsMultiplierChanged(value); } // PUBLIC METHODS - function stakeMainSide(uint amount) public payable { + function deposit(uint amount) public payable { require(active, "Pool is not active"); - require(amount >= mainSideConfig.minStakeValue, "Pool: stake value is too low"); + require(amount >= config.minDepositValue, "Pool: deposit value is too low"); if (msg.value != 0) { - require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); + require(config.depositToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(mainSideConfig.token), msg.sender, address(this), amount); + SafeERC20.safeTransferFrom(IERC20(config.depositToken), msg.sender, address(this), amount); } - uint rewardsAmount = _calcRewardsMainSide(amount); + stakers[msg.sender].deposit += amount; + info.totalDeposit += amount; + + emit DepositChanged(msg.sender, amount); + } + + function withdraw(uint amount) public { + require(stakers[msg.sender].deposit >= amount, "Not enough deposit"); - mainSideStakers[msg.sender].stake += amount; - mainSideInfo.totalStake += amount; + stakers[msg.sender].deposit -= amount; + info.totalDeposit -= amount; - mainSideInfo.totalRewards += rewardsAmount; + require(stakers[msg.sender].deposit <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); - _updateRewardsDebtMainSide(msg.sender, _calcRewardsMainSide(mainSideStakers[msg.sender].stake)); - emit StakeChanged(false, msg.sender, amount); + if (config.depositToken == address(0)) { + payable(msg.sender).transfer(amount); + } else { + SafeERC20.safeTransfer(IERC20(config.depositToken), msg.sender, amount); + } + + emit DepositChanged(msg.sender, stakers[msg.sender].deposit); } - function stakeDependantSide(uint amount) public payable { + function stake(uint amount) public payable { require(active, "Pool is not active"); - require(amount >= dependantSideConfig.minStakeValue, "Pool: stake value is too low"); + require(amount >= config.minStakeValue, "Pool: stake value is too low"); if (msg.value != 0) { - require(mainSideConfig.token == address(0), "Pool: does not accept native coin"); + require(config.profitableToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(dependantSideConfig.token), msg.sender, address(this), amount); + SafeERC20.safeTransferFrom(IERC20(config.profitableToken), msg.sender, address(this), amount); } - uint rewardsAmount = _calcRewardsDependantSide(amount); - - dependantSideStakers[msg.sender].stake += amount; - dependantSideInfo.totalStake += amount; - if (dependantSideStakers[msg.sender].stakedAt == 0) - dependantSideStakers[msg.sender].stakedAt = block.timestamp; + uint rewardsAmount = _calcRewards(amount); - require(dependantSideStakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); - require(dependantSideInfo.totalStake <= dependantSideConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); - require(dependantSideStakers[msg.sender].stake <= dependantSideConfig.maxStakePerUserValue, "Pool: max stake per user exceeded"); + stakers[msg.sender].stake += amount; + info.totalStake += amount; + info.totalRewards += rewardsAmount; + if (stakers[msg.sender].stakedAt == 0) + stakers[msg.sender].stakedAt = block.timestamp; - dependantSideInfo.totalRewards += rewardsAmount; + require(stakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); + require(info.totalStake <= config.maxTotalStakeValue, "Pool: max stake value exceeded"); + require(stakers[msg.sender].stake <= config.maxStakePerUserValue, "Pool: max stake per user exceeded"); - _updateRewardsDebtDependantSide(msg.sender, _calcRewardsDependantSide(dependantSideStakers[msg.sender].stake)); - emit StakeChanged(true, msg.sender, amount); + _updateRewardsDebt(msg.sender, _calcRewards(stakers[msg.sender].stake)); + emit StakeChanged(msg.sender, amount); } - function unstakeMainSide(uint amount) public { - require(mainSideStakers[msg.sender].stake >= amount, "Not enough stake"); + function unstake(uint amount) public { + require(stakers[msg.sender].stake >= amount, "Not enough stake"); + require(block.timestamp - stakers[msg.sender].stakedAt >= config.stakeLockPeriod, "Stake is locked"); - uint rewardsAmount = _calcRewardsMainSide(amount); + uint rewardsAmount = _calcRewards(amount); - mainSideStakers[msg.sender].stake -= amount; - mainSideInfo.totalStake -= amount; + stakers[msg.sender].stake -= amount; + info.totalStake -= amount; + info.totalRewards -= rewardsAmount; - mainSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebtMainSide(msg.sender, _calcRewardsMainSide(mainSideStakers[msg.sender].stake)); - - // cancel previous lock (if exists). canceledAmount will be added to new lock - uint canceledAmount; - if (lockKeeper.getLock(mainSideStakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists - canceledAmount = lockKeeper.cancelLock(mainSideStakers[msg.sender].lockedWithdrawal); + if (stakers[msg.sender].stake == 0) stakers[msg.sender].stakedAt = 0; - if (mainSideConfig.token == address(0)) { - // lock funds - mainSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle{value: amount + canceledAmount}( - msg.sender, address(mainSideConfig.token), uint64(block.timestamp + mainSideConfig.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainSideConfig.token)))) - ); - } else { - IERC20(mainSideConfig.token).approve(address(lockKeeper), amount + canceledAmount); - // lock funds - mainSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( - msg.sender, address(mainSideConfig.token), uint64(block.timestamp + mainSideConfig.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainSideConfig.token)))) - ); - } - - _claimRewardsMainSide(msg.sender); - - emit UnstakeLocked(false, msg.sender, amount + canceledAmount, block.timestamp + mainSideConfig.lockPeriod, block.timestamp); - emit StakeChanged(false, msg.sender, mainSideStakers[msg.sender].stake); - } - - function unstakeDependantSide(uint amount) public { - require(dependantSideStakers[msg.sender].stake >= amount, "Not enough stake"); - require(block.timestamp - dependantSideStakers[msg.sender].stakedAt >= dependantSideConfig.stakeLockPeriod, "Stake is locked"); - - uint rewardsAmount = _calcRewardsDependantSide(amount); - - dependantSideStakers[msg.sender].stake -= amount; - dependantSideInfo.totalStake -= amount; - - if (dependantSideStakers[msg.sender].stake == 0) dependantSideStakers[msg.sender].stakedAt = 0; - - dependantSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebtDependantSide(msg.sender, _calcRewardsDependantSide(dependantSideStakers[msg.sender].stake)); + _updateRewardsDebt(msg.sender, _calcRewards(stakers[msg.sender].stake)); // cancel previous lock (if exists). canceledAmount will be added to new lock uint canceledAmount; - if (lockKeeper.getLock(dependantSideStakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists - canceledAmount = lockKeeper.cancelLock(dependantSideStakers[msg.sender].lockedWithdrawal); + if (lockKeeper.getLock(stakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists + canceledAmount = lockKeeper.cancelLock(stakers[msg.sender].lockedWithdrawal); - if (mainSideConfig.token == address(0)) { + if (config.profitableToken == address(0)) { // lock funds - dependantSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle{value: amount + canceledAmount}( - msg.sender, address(dependantSideConfig.token), uint64(block.timestamp + dependantSideConfig.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(dependantSideConfig.token)))) + stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle{value: amount + canceledAmount}( + msg.sender, address(config.profitableToken), uint64(block.timestamp + config.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake")) ); } else { - IERC20(dependantSideConfig.token).approve(address(lockKeeper), amount + canceledAmount); + IERC20(config.profitableToken).approve(address(lockKeeper), amount + canceledAmount); // lock funds - dependantSideStakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( - msg.sender, address(dependantSideConfig.token), uint64(block.timestamp + dependantSideConfig.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(dependantSideConfig.token)))) + stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( + msg.sender, address(config.profitableToken), uint64(block.timestamp + config.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake")) ); } - _claimRewardsDependantSide(msg.sender); - - emit UnstakeLocked(true, msg.sender, amount + canceledAmount, block.timestamp + mainSideConfig.lockPeriod, block.timestamp); - emit StakeChanged(true, msg.sender, dependantSideStakers[msg.sender].stake); + _claimRewards(msg.sender); + emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + config.lockPeriod, block.timestamp); + emit StakeChanged(msg.sender, stakers[msg.sender].stake); } - function unstakeFastMainSide(uint amount) public { - require(mainSideStakers[msg.sender].stake >= amount, "Not enough stake"); - - uint rewardsAmount = _calcRewardsMainSide(amount); - - mainSideStakers[msg.sender].stake -= amount; - mainSideInfo.totalStake -= amount; - - mainSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebtMainSide(msg.sender, _calcRewardsMainSide(mainSideStakers[msg.sender].stake)); - - uint penalty = amount * mainSideConfig.fastUnstakePenalty / BILLION; - if (mainSideConfig.token == address(0)) { - payable(msg.sender).transfer(amount - penalty); - } else { - SafeERC20.safeTransfer(IERC20(mainSideConfig.token), msg.sender, amount - penalty); - } - - _claimRewardsMainSide(msg.sender); - - emit UnstakeFast(false, msg.sender, amount, penalty); - emit StakeChanged(false, msg.sender, amount); - } - - function unstakeFastDependantSide(uint amount) public { - require(dependantSideStakers[msg.sender].stake >= amount, "Not enough stake"); - require(block.timestamp - dependantSideStakers[msg.sender].stakedAt >= dependantSideConfig.stakeLockPeriod, "Stake is locked"); - - uint rewardsAmount = _calcRewardsDependantSide(amount); - - dependantSideStakers[msg.sender].stake -= amount; - dependantSideInfo.totalStake -= amount; - - if (dependantSideStakers[msg.sender].stake == 0) dependantSideStakers[msg.sender].stakedAt = 0; - - dependantSideInfo.totalRewards -= rewardsAmount; - _updateRewardsDebtDependantSide(msg.sender, _calcRewardsDependantSide(dependantSideStakers[msg.sender].stake)); - - uint penalty = amount * dependantSideConfig.fastUnstakePenalty / BILLION; - if (dependantSideConfig.token == address(0)) { - payable(msg.sender).transfer(amount - penalty); - } else { - SafeERC20.safeTransfer(IERC20(dependantSideConfig.token), msg.sender, amount - penalty); - } - - _claimRewardsDependantSide(msg.sender); - - emit UnstakeFast(true, msg.sender, amount, penalty); - emit StakeChanged(true, msg.sender, amount); - } - - function claimMainSide() public { - _calcClaimableRewardsMainSide(msg.sender); - _claimRewardsMainSide(msg.sender); - } - - function claimDependantSide() public { - _calcClaimableRewardsDependantSide(msg.sender); - _claimRewardsDependantSide(msg.sender); + function claim() public { + _calcClaimableRewards(msg.sender); + _claimRewards(msg.sender); } function onBlock() external { - _addInterestMainSide(); - _addInterestDependantSide(); + _addInterest(); } // VIEW METHODS - function getMainSideConfig() public view returns (MainSideConfig memory) { - return mainSideConfig; - } - - function getMainSideInfo() public view returns (SideInfo memory) { - return mainSideInfo; + function getName() public view returns (string memory) { + return config.name; } - function getMainSideStaker(address user) public view returns (SideStaker memory) { - return mainSideStakers[user]; - } - - function getDependantSideConfig() public view returns (DependantSideConfig memory) { - return dependantSideConfig; + function getConfig() public view returns (Config memory) { + return config; } - function getDependantSideInfo() public view returns (SideInfo memory) { - return dependantSideInfo; - } - - function getDependantSideStaker(address user) public view returns (SideStaker memory) { - return dependantSideStakers[user]; + function getInfo() public view returns (Info memory) { + return info; } - function getUserMainSideRewards(address user) public view returns (uint) { - uint rewardsAmount = _calcRewardsMainSide(mainSideStakers[user].stake); - if (rewardsAmount + mainSideStakers[user].claimableRewards <= mainSideStakers[user].rewardsDebt) - return 0; - - return rewardsAmount + mainSideStakers[user].claimableRewards - mainSideStakers[user].rewardsDebt; + function getStaker(address user) public view returns (Staker memory) { + return stakers[user]; } - function getUserDependantSideRewards(address user) public view returns (uint) { - uint rewardsAmount = _calcRewardsDependantSide(dependantSideStakers[user].stake); - if (rewardsAmount + dependantSideStakers[user].claimableRewards <= dependantSideStakers[user].rewardsDebt) + function getUserRewards(address user) public view returns (uint) { + uint rewardsAmount = _calcRewards(stakers[user].stake); + if (rewardsAmount + stakers[user].claimableRewards <= stakers[user].rewardsDebt) return 0; - return rewardsAmount + dependantSideStakers[user].claimableRewards - dependantSideStakers[user].rewardsDebt; + return rewardsAmount + stakers[user].claimableRewards - stakers[user].rewardsDebt; } // INTERNAL METHODS + function _addInterest() internal { + if (info.lastInterestUpdate + config.interestRate > block.timestamp) return; + uint timePassed = block.timestamp - info.lastInterestUpdate; + uint newRewards = info.totalStake * config.interest * timePassed / BILLION / config.interestRate; - function _addInterestMainSide() internal { - uint timePassed = block.timestamp - mainSideInfo.lastInterestUpdate; - uint newRewards = mainSideInfo.totalStake * mainSideConfig.interest * timePassed / BILLION / mainSideConfig.interestRate; - - mainSideInfo.totalRewards += newRewards; - mainSideInfo.lastInterestUpdate = block.timestamp; - emit Interest(false, newRewards); - } - - function _addInterestDependantSide() internal { - if (!hasSecondSide) return; - if (dependantSideInfo.lastInterestUpdate + dependantSideConfig.interestRate > block.timestamp) return; - uint timePassed = block.timestamp - dependantSideInfo.lastInterestUpdate; - uint newRewards = dependantSideInfo.totalStake * dependantSideConfig.interest * timePassed / BILLION / dependantSideConfig.interestRate; - - dependantSideInfo.totalRewards += newRewards; - dependantSideInfo.lastInterestUpdate = block.timestamp; - emit Interest(true, newRewards); + info.totalRewards += newRewards; + info.lastInterestUpdate = block.timestamp; + emit Interest(newRewards); } function _maxUserStakeValue(address user) internal view returns (uint) { - return mainSideStakers[user].stake * dependantSideConfig.stakeLimitsMultiplier; + return stakers[user].deposit * config.stakeLimitsMultiplier / BILLION; } // store claimable rewards - function _calcClaimableRewardsMainSide(address user) internal { - uint rewardsAmount = _calcRewardsMainSide(mainSideStakers[user].stake); - uint rewardsWithoutDebt = rewardsAmount - mainSideStakers[user].rewardsDebt; - mainSideStakers[user].claimableRewards += rewardsWithoutDebt; - mainSideInfo.totalRewardsDebt += rewardsWithoutDebt; - mainSideStakers[user].rewardsDebt += rewardsWithoutDebt; + function _calcClaimableRewards(address user) internal { + uint rewardsAmount = _calcRewards(stakers[user].stake); + uint rewardsWithoutDebt = rewardsAmount - stakers[user].rewardsDebt; + stakers[user].claimableRewards += rewardsWithoutDebt; + info.totalRewardsDebt += rewardsWithoutDebt; + stakers[user].rewardsDebt += rewardsWithoutDebt; } - function _calcClaimableRewardsDependantSide(address user) internal { - uint rewardsAmount = _calcRewardsDependantSide(dependantSideStakers[user].stake); - uint rewardsWithoutDebt = rewardsAmount - dependantSideStakers[user].rewardsDebt; - dependantSideStakers[user].claimableRewards += rewardsWithoutDebt; - dependantSideInfo.totalRewardsDebt += rewardsWithoutDebt; - dependantSideStakers[user].rewardsDebt += rewardsWithoutDebt; - } - - function _claimRewardsMainSide(address user) internal { - uint amount = mainSideStakers[user].claimableRewards; - if (amount == 0) return; - - mainSideStakers[user].claimableRewards = 0; - - uint rewardTokenAmount = amount * mainSideConfig.rewardTokenPrice; - if (mainSideConfig.rewardToken == address(0)) { - rewardsBank.withdrawAmb(payable(user), amount); - } else { - rewardsBank.withdrawErc20(mainSideConfig.rewardToken, payable(user), rewardTokenAmount); - } - emit Claim(false, user, rewardTokenAmount); - } - - function _claimRewardsDependantSide(address user) internal { - uint amount = dependantSideStakers[user].claimableRewards; + function _claimRewards(address user) internal { + uint amount = stakers[user].claimableRewards; if (amount == 0) return; - dependantSideStakers[user].claimableRewards = 0; + stakers[user].claimableRewards = 0; - uint rewardTokenAmount = amount * dependantSideConfig.rewardTokenPrice; - if (dependantSideConfig.rewardToken == address(0)) { + uint rewardTokenAmount = amount * config.rewardTokenPrice; + if (config.rewardToken == address(0)) { rewardsBank.withdrawAmb(payable(user), amount); } else { - rewardsBank.withdrawErc20(dependantSideConfig.rewardToken, payable(user), rewardTokenAmount); + rewardsBank.withdrawErc20(config.rewardToken, payable(user), rewardTokenAmount); } - emit Claim(true, user, rewardTokenAmount); - } - - function _calcRewardsMainSide(uint amount) internal view returns (uint) { - if (mainSideInfo.totalStake == 0 && mainSideInfo.totalRewards == 0) return amount; - return amount * mainSideInfo.totalRewards / mainSideInfo.totalStake; - } - - function _calcRewardsDependantSide(uint amount) internal view returns (uint) { - if (dependantSideInfo.totalStake == 0 && dependantSideInfo.totalRewards == 0) return amount; - return amount * dependantSideInfo.totalRewards / dependantSideInfo.totalStake; + emit Claim(user, rewardTokenAmount); } - function _updateRewardsDebtMainSide(address user, uint newDebt) internal { - uint oldDebt = mainSideStakers[user].rewardsDebt; - if (newDebt < oldDebt) mainSideInfo.totalRewardsDebt -= oldDebt - newDebt; - else mainSideInfo.totalRewardsDebt += newDebt - oldDebt; - mainSideStakers[user].rewardsDebt = newDebt; + function _calcRewards(uint amount) internal view returns (uint) { + if (info.totalStake == 0 && info.totalRewards == 0) return amount; + return amount * info.totalRewards / info.totalStake; } - function _updateRewardsDebtDependantSide(address user, uint newDebt) internal { - uint oldDebt = dependantSideStakers[user].rewardsDebt; - if (newDebt < oldDebt) dependantSideInfo.totalRewardsDebt -= oldDebt - newDebt; - else dependantSideInfo.totalRewardsDebt += newDebt - oldDebt; - dependantSideStakers[user].rewardsDebt = newDebt; + function _updateRewardsDebt(address user, uint newDebt) internal { + uint oldDebt = stakers[user].rewardsDebt; + if (newDebt < oldDebt) info.totalRewardsDebt -= oldDebt - newDebt; + else info.totalRewardsDebt += newDebt - oldDebt; + stakers[user].rewardsDebt = newDebt; } - - function _addressToString(address x) internal pure returns (string memory) { - bytes memory s = new bytes(40); - for (uint i = 0; i < 20; i++) { - uint8 b = uint8(uint(uint160(x)) / (2 ** (8 * (19 - i)))); - uint8 hi = (b / 16); - uint8 lo = (b - 16 * hi); - s[2 * i] = _char(hi); - s[2 * i + 1] = _char(lo); - } - return string(s); - } - - function _char(uint8 b) internal pure returns (bytes1 c) { - return bytes1(b + (b < 10 ? 0x30 : 0x57)); - } - } From 4e3855e4203a68350391075229368470697291be Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Tue, 17 Sep 2024 11:23:01 +0300 Subject: [PATCH 38/60] WIP --- ...bleSidePool.sol => DepositedTokenPool.sol} | 19 +++--- .../{SingleSidePool.sol => TokenPool.sol} | 2 +- contracts/staking/token/TokenPoolsManager.sol | 58 ++++++++----------- 3 files changed, 35 insertions(+), 44 deletions(-) rename contracts/staking/token/{DoubleSidePool.sol => DepositedTokenPool.sol} (97%) rename contracts/staking/token/{SingleSidePool.sol => TokenPool.sol} (99%) diff --git a/contracts/staking/token/DoubleSidePool.sol b/contracts/staking/token/DepositedTokenPool.sol similarity index 97% rename from contracts/staking/token/DoubleSidePool.sol rename to contracts/staking/token/DepositedTokenPool.sol index c0874a4c..e2361aec 100644 --- a/contracts/staking/token/DoubleSidePool.sol +++ b/contracts/staking/token/DepositedTokenPool.sol @@ -9,24 +9,23 @@ import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; //The side defined by the address of the token. Zero address means native coin -contract DepositedPool is Initializable, AccessControl, IOnBlockListener { +contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { + + //TODO: Divide on main config and profit config? struct Config { string name; address depositToken; uint minDepositValue; address profitableToken; - address rewardToken; - uint rewardTokenPrice; uint minStakeValue; uint unstakeLockPeriod; // Time in seconds to how long the amount is locker after unstake - uint lockPeriod; - uint interest; - uint interestRate; + uint stakeLockPeriod; // Time in seconds to how long the stake is locker before unstake uint maxTotalStakeValue; uint maxStakePerUserValue; - uint stakeLockPeriod; // Time in seconds to how long the stake is locker before unstake uint stakeLimitsMultiplier; // Should be represented as parts of BILLION + uint interest; + uint interestRate; } struct Info { @@ -230,14 +229,14 @@ contract DepositedPool is Initializable, AccessControl, IOnBlockListener { if (config.profitableToken == address(0)) { // lock funds stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle{value: amount + canceledAmount}( - msg.sender, address(config.profitableToken), uint64(block.timestamp + config.lockPeriod), amount + canceledAmount, + msg.sender, address(config.profitableToken), uint64(block.timestamp + config.unstakeLockPeriod), amount + canceledAmount, string(abi.encodePacked("TokenStaking unstake")) ); } else { IERC20(config.profitableToken).approve(address(lockKeeper), amount + canceledAmount); // lock funds stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( - msg.sender, address(config.profitableToken), uint64(block.timestamp + config.lockPeriod), amount + canceledAmount, + msg.sender, address(config.profitableToken), uint64(block.timestamp + config.unstakeLockPeriod), amount + canceledAmount, string(abi.encodePacked("TokenStaking unstake")) ); } @@ -245,7 +244,7 @@ contract DepositedPool is Initializable, AccessControl, IOnBlockListener { _claimRewards(msg.sender); - emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + config.lockPeriod, block.timestamp); + emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + config.unstakeLockPeriod, block.timestamp); emit StakeChanged(msg.sender, stakers[msg.sender].stake); } diff --git a/contracts/staking/token/SingleSidePool.sol b/contracts/staking/token/TokenPool.sol similarity index 99% rename from contracts/staking/token/SingleSidePool.sol rename to contracts/staking/token/TokenPool.sol index d9e26f7f..29a60020 100644 --- a/contracts/staking/token/SingleSidePool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -10,7 +10,7 @@ import "../../LockKeeper.sol"; import "hardhat/console.sol"; -contract SingleSidePool is Initializable, AccessControl, IOnBlockListener { +contract TokenPool is Initializable, AccessControl, IOnBlockListener { struct Config { IERC20 token; diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index c5ea41da..af68e9b3 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import "./SingleSidePool.sol"; -import "./DoubleSidePool.sol"; +import "./TokenPool.sol"; +import "./DepositedTokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; @@ -14,8 +14,8 @@ import "hardhat/console.sol"; contract TokenPoolsManager is AccessControl{ LockKeeper lockKeeper; RewardsBank public bank; - UpgradeableBeacon public singleSideBeacon; - UpgradeableBeacon public doubleSideBeacon; + UpgradeableBeacon public tokenPoolBeacon; + UpgradeableBeacon public depositedTokenPoolBeacon; mapping(string => address) public pools; mapping(string => address) public doubleSidePools; @@ -23,27 +23,26 @@ contract TokenPoolsManager is AccessControl{ constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon singleSideBeacon_, UpgradeableBeacon doubleSideBeacon_) { lockKeeper = lockKeeper_; bank = bank_; - singleSideBeacon = singleSideBeacon_; - doubleSideBeacon = doubleSideBeacon_; + tokenPoolBeacon = singleSideBeacon_; + depositedTokenPoolBeacon = doubleSideBeacon_; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } event PoolCreated(string name, address pool); event PoolDeactivated(string name); event PoolActivated(string name); - event DoubleSidePoolCreate(string name, address pool); - event DoubleSidePoolDeactivated(string name); - event DoubleSidePoolActivated(string name); - event DependentSideAdded(string name, address pool); + event DepositedPoolCreated(string name, address pool); + event DepositedPoolDeactivated(string name); + event DepositedPoolActivated(string name); // OWNER METHODS - function createSingleSidePool(SingleSidePool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createTokenPool(TokenPool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { console.log("Entered createPool"); bytes memory data = abi.encodeWithSignature( "initialize(address,address,(address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", bank, lockKeeper, params); - address pool = address(new BeaconProxy(address(singleSideBeacon), data)); + address pool = address(new BeaconProxy(address(tokenPoolBeacon), data)); console.log("Pool created at address: %s", pool); pools[params.name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); @@ -51,53 +50,46 @@ contract TokenPoolsManager is AccessControl{ return pool; } - function deactivateSingleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function deactivateTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(pools[_pool] != address(0), "Pool does not exist"); - SingleSidePool pool = SingleSidePool(pools[_pool]); + TokenPool pool = TokenPool(pools[_pool]); pool.deactivate(); emit PoolDeactivated(_pool); } - function activateSingleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function activateTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(pools[_pool] != address(0), "Pool does not exist"); - SingleSidePool pool = SingleSidePool(pools[_pool]); + TokenPool pool = TokenPool(pools[_pool]); pool.activate(); emit PoolActivated(_pool); } - function createDoubleSidePool(string calldata name_, DoubleSidePool.MainSideConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createDeposistedTokenPool(string calldata name_, DepositedTokenPool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { console.log("Entered createDoubleSidePool"); bytes memory data = abi.encodeWithSignature( - "initialize(address,address,string,(address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256))", - bank, lockKeeper, name_, params); - address pool = address(new BeaconProxy(address(doubleSideBeacon), data)); + "initialize(address,address,(string,address,uint256,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))", + bank, lockKeeper, params); + address pool = address(new BeaconProxy(address(depositedTokenPoolBeacon), data)); console.log("DoubleSidePool created at address: %s", pool); doubleSidePools[name_] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit DoubleSidePoolCreate(name_, pool); + emit DepositedPoolCreated(name_, pool); return pool; } - function addDependantSide(string calldata name_, DoubleSidePool.DependantSideConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(doubleSidePools[name_] != address(0), "Pool does not exist"); - DoubleSidePool pool = DoubleSidePool(doubleSidePools[name_]); - pool.addDependantSide(params); - emit DependentSideAdded(name_, address(pool)); - } - function deactivateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(doubleSidePools[_pool] != address(0), "Pool does not exist"); - DoubleSidePool pool = DoubleSidePool(doubleSidePools[_pool]); + DepositedTokenPool pool = DepositedTokenPool(doubleSidePools[_pool]); pool.deactivate(); - emit DoubleSidePoolDeactivated(_pool); + emit DepositedPoolDeactivated(_pool); } function activateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(doubleSidePools[_pool] != address(0), "Pool does not exist"); - DoubleSidePool pool = DoubleSidePool(doubleSidePools[_pool]); + DepositedTokenPool pool = DepositedTokenPool(doubleSidePools[_pool]); pool.activate(); - emit DoubleSidePoolActivated(_pool); + emit DepositedPoolActivated(_pool); } // VIEW METHODS @@ -106,7 +98,7 @@ contract TokenPoolsManager is AccessControl{ return pools[name]; } - function getDoubleSidePoolAddress(string memory name) public view returns (address) { + function getDepositedPoolAdress(string memory name) public view returns (address) { return doubleSidePools[name]; } From 5612c8e5f5b93ccc35f651e85c10778c72aca8e4 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 18 Sep 2024 13:12:08 +0300 Subject: [PATCH 39/60] Add admind methods --- .../staking/token/DepositedTokenPool.sol | 176 +++++++++++------- contracts/staking/token/TokenPoolsManager.sol | 118 +++++++++++- 2 files changed, 220 insertions(+), 74 deletions(-) diff --git a/contracts/staking/token/DepositedTokenPool.sol b/contracts/staking/token/DepositedTokenPool.sol index e2361aec..bb4f9619 100644 --- a/contracts/staking/token/DepositedTokenPool.sol +++ b/contracts/staking/token/DepositedTokenPool.sol @@ -11,21 +11,25 @@ import "../../LockKeeper.sol"; //The side defined by the address of the token. Zero address means native coin contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { - - //TODO: Divide on main config and profit config? - struct Config { + struct MainConfig { string name; address depositToken; - uint minDepositValue; address profitableToken; + address rewardToken; + uint rewardTokenPrice; + uint interest; + uint interestRate; + } + + struct LimitsConfig { + uint minDepositValue; uint minStakeValue; + uint fastUnstakePenalty; uint unstakeLockPeriod; // Time in seconds to how long the amount is locker after unstake uint stakeLockPeriod; // Time in seconds to how long the stake is locker before unstake uint maxTotalStakeValue; uint maxStakePerUserValue; uint stakeLimitsMultiplier; // Should be represented as parts of BILLION - uint interest; - uint interestRate; } struct Info { @@ -46,14 +50,13 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { } uint constant public BILLION = 1_000_000_000; + bool public active; LockKeeper public lockKeeper; RewardsBank public rewardsBank; - - string public name; - bool public active; - Config public config; + MainConfig public mainConfig; + LimitsConfig public limitsConfig; Info public info; mapping(address => Staker) public stakers; @@ -62,38 +65,44 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { event Deactivated(); event Activated(); - event MinStakeValueChanged(uint minStakeValue); - event UnstakeLockePeriodChanged(uint period); event RewardTokenPriceChanged(uint price); - event FastUnstakePenaltyChanged(uint penalty); event InterestChanged(uint interest); event InterestRateChanged(uint interestRate); + event MinDepositValueChanged(uint minDepositValue); + event MinStakeValueChanged(uint minStakeValue); + event FastUnstakePenaltyChanged(uint penalty); + event UnstakeLockPeriodChanged(uint period); + event StakeLockPeriodChanged(uint period); event MaxTotalStakeValueChanged(uint poolMaxStakeValue); event MaxStakePerUserValueChanged(uint maxStakePerUserValue); - event StakeLockPeriodChanged(uint period); event StakeLimitsMultiplierChanged(uint value); - event DepositChanged(address indexed user, uint amount); - event StakeChanged(address indexed user, uint amount); + event Deposited(address indexed user, uint amount); + event Withdrawn(address indexed user, uint amount); + event Staked(address indexed user, uint amount); event Claim(address indexed user, uint amount); event Interest(uint amount); event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); function initialize( - RewardsBank rewardsBank_, LockKeeper lockkeeper_, Config calldata config_ + RewardsBank rewardsBank_, LockKeeper lockkeeper_, MainConfig calldata config_ ) public initializer { lockKeeper = lockkeeper_; rewardsBank = rewardsBank_; active = true; - config = config_; + mainConfig = config_; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } // OWNER METHODS + function setLimitsConfig(LimitsConfig calldata config) public onlyRole(DEFAULT_ADMIN_ROLE) { + limitsConfig = config; + } + function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { require(!active, "Pool is already active"); active = true; @@ -108,41 +117,56 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { // SETTERS FOR PARAMS - function setMinStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.minStakeValue = value; - emit MinStakeValueChanged(value); + function setRewardTokenPrice(uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { + mainConfig.rewardTokenPrice = price; + emit RewardTokenPriceChanged(price); } function setInterest(uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.interest = _interest; - config.interestRate = _interestRate; + mainConfig.interest = _interest; + mainConfig.interestRate = _interestRate; emit InterestRateChanged(_interestRate); emit InterestChanged(_interest); } + function setMinDepositValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + limitsConfig.minDepositValue = value; + emit MinStakeValueChanged(value); + } + + function setMinStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + limitsConfig.minStakeValue = value; + emit MinDepositValueChanged(value); + } + + function setFastUnstakePenalty(uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { + limitsConfig.fastUnstakePenalty = penalty; + emit FastUnstakePenaltyChanged(penalty); + } + function setUnstakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.unstakeLockPeriod = period; - emit UnstakeLockePeriodChanged(period); + limitsConfig.unstakeLockPeriod = period; + emit UnstakeLockPeriodChanged(period); } - function setRewardTokenPrice(uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.rewardTokenPrice = price; - emit RewardTokenPriceChanged(price); + function setStakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + limitsConfig.stakeLockPeriod = period; + emit StakeLockPeriodChanged(period); } function setMaxTotalStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.maxTotalStakeValue = value; + limitsConfig.maxTotalStakeValue = value; emit MaxTotalStakeValueChanged(value); } - function setStakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.stakeLockPeriod = period; - emit StakeLockPeriodChanged(period); + function setMaxStakePerUserValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + limitsConfig.maxStakePerUserValue = value; + emit MaxStakePerUserValueChanged(value); } function setStakeLimitsMultiplier(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.stakeLimitsMultiplier = value; + limitsConfig.stakeLimitsMultiplier = value; emit StakeLimitsMultiplierChanged(value); } @@ -150,18 +174,18 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { function deposit(uint amount) public payable { require(active, "Pool is not active"); - require(amount >= config.minDepositValue, "Pool: deposit value is too low"); + require(amount >= limitsConfig.minDepositValue, "Pool: deposit value is too low"); if (msg.value != 0) { - require(config.depositToken == address(0), "Pool: does not accept native coin"); + require(mainConfig.depositToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(config.depositToken), msg.sender, address(this), amount); + SafeERC20.safeTransferFrom(IERC20(mainConfig.depositToken), msg.sender, address(this), amount); } stakers[msg.sender].deposit += amount; info.totalDeposit += amount; - emit DepositChanged(msg.sender, amount); + emit Deposited(msg.sender, amount); } function withdraw(uint amount) public { @@ -170,25 +194,25 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { stakers[msg.sender].deposit -= amount; info.totalDeposit -= amount; - require(stakers[msg.sender].deposit <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); + require(stakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); - if (config.depositToken == address(0)) { + if (mainConfig.depositToken == address(0)) { payable(msg.sender).transfer(amount); } else { - SafeERC20.safeTransfer(IERC20(config.depositToken), msg.sender, amount); + SafeERC20.safeTransfer(IERC20(mainConfig.depositToken), msg.sender, amount); } - emit DepositChanged(msg.sender, stakers[msg.sender].deposit); + emit Withdrawn(msg.sender, amount); } function stake(uint amount) public payable { require(active, "Pool is not active"); - require(amount >= config.minStakeValue, "Pool: stake value is too low"); + require(amount >= limitsConfig.minStakeValue, "Pool: stake value is too low"); if (msg.value != 0) { - require(config.profitableToken == address(0), "Pool: does not accept native coin"); + require(mainConfig.profitableToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(config.profitableToken), msg.sender, address(this), amount); + SafeERC20.safeTransferFrom(IERC20(mainConfig.profitableToken), msg.sender, address(this), amount); } uint rewardsAmount = _calcRewards(amount); @@ -200,16 +224,16 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { stakers[msg.sender].stakedAt = block.timestamp; require(stakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); - require(info.totalStake <= config.maxTotalStakeValue, "Pool: max stake value exceeded"); - require(stakers[msg.sender].stake <= config.maxStakePerUserValue, "Pool: max stake per user exceeded"); + require(info.totalStake <= limitsConfig.maxTotalStakeValue, "Pool: max stake value exceeded"); + require(stakers[msg.sender].stake <= limitsConfig.maxStakePerUserValue, "Pool: max stake per user exceeded"); _updateRewardsDebt(msg.sender, _calcRewards(stakers[msg.sender].stake)); - emit StakeChanged(msg.sender, amount); + emit Staked(msg.sender, amount); } function unstake(uint amount) public { require(stakers[msg.sender].stake >= amount, "Not enough stake"); - require(block.timestamp - stakers[msg.sender].stakedAt >= config.stakeLockPeriod, "Stake is locked"); + require(block.timestamp - stakers[msg.sender].stakedAt >= limitsConfig.stakeLockPeriod, "Stake is locked"); uint rewardsAmount = _calcRewards(amount); @@ -226,26 +250,46 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { if (lockKeeper.getLock(stakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists canceledAmount = lockKeeper.cancelLock(stakers[msg.sender].lockedWithdrawal); - if (config.profitableToken == address(0)) { + if (mainConfig.profitableToken == address(0)) { // lock funds stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle{value: amount + canceledAmount}( - msg.sender, address(config.profitableToken), uint64(block.timestamp + config.unstakeLockPeriod), amount + canceledAmount, + msg.sender, address(mainConfig.profitableToken), uint64(block.timestamp + limitsConfig.unstakeLockPeriod), amount + canceledAmount, string(abi.encodePacked("TokenStaking unstake")) ); } else { - IERC20(config.profitableToken).approve(address(lockKeeper), amount + canceledAmount); + IERC20(mainConfig.profitableToken).approve(address(lockKeeper), amount + canceledAmount); // lock funds stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( - msg.sender, address(config.profitableToken), uint64(block.timestamp + config.unstakeLockPeriod), amount + canceledAmount, + msg.sender, address(mainConfig.profitableToken), uint64(block.timestamp + limitsConfig.unstakeLockPeriod), amount + canceledAmount, string(abi.encodePacked("TokenStaking unstake")) ); } - _claimRewards(msg.sender); - emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + config.unstakeLockPeriod, block.timestamp); - emit StakeChanged(msg.sender, stakers[msg.sender].stake); + emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + limitsConfig.unstakeLockPeriod, block.timestamp); + } + + function unstakeFast(uint amount) public { + require(stakers[msg.sender].stake >= amount, "Not enough stake"); + require(block.timestamp - stakers[msg.sender].stakedAt >= limitsConfig.stakeLockPeriod, "Stake is locked"); + + uint rewardsAmount = _calcRewards(amount); + + stakers[msg.sender].stake -= amount; + info.totalStake -= amount; + info.totalRewards -= rewardsAmount; + + if (stakers[msg.sender].stake == 0) stakers[msg.sender].stakedAt = 0; + + _updateRewardsDebt(msg.sender, _calcRewards(stakers[msg.sender].stake)); + uint penalty = amount * limitsConfig.fastUnstakePenalty / BILLION; + if (mainConfig.profitableToken == address(0)) { + payable(msg.sender).transfer(amount - penalty); + } else { + SafeERC20.safeTransfer(IERC20(mainConfig.profitableToken), msg.sender, amount - penalty); + } + _claimRewards(msg.sender); } function claim() public { @@ -260,11 +304,15 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { // VIEW METHODS function getName() public view returns (string memory) { - return config.name; + return mainConfig.name; + } + + function getMainConfig() public view returns (MainConfig memory) { + return mainConfig; } - function getConfig() public view returns (Config memory) { - return config; + function getLimitsConfig() public view returns (LimitsConfig memory) { + return limitsConfig; } function getInfo() public view returns (Info memory) { @@ -285,9 +333,9 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { // INTERNAL METHODS function _addInterest() internal { - if (info.lastInterestUpdate + config.interestRate > block.timestamp) return; + if (info.lastInterestUpdate + mainConfig.interestRate > block.timestamp) return; uint timePassed = block.timestamp - info.lastInterestUpdate; - uint newRewards = info.totalStake * config.interest * timePassed / BILLION / config.interestRate; + uint newRewards = info.totalStake * mainConfig.interest * timePassed / BILLION / mainConfig.interestRate; info.totalRewards += newRewards; info.lastInterestUpdate = block.timestamp; @@ -295,7 +343,7 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { } function _maxUserStakeValue(address user) internal view returns (uint) { - return stakers[user].deposit * config.stakeLimitsMultiplier / BILLION; + return stakers[user].deposit * limitsConfig.stakeLimitsMultiplier / BILLION; } // store claimable rewards @@ -313,11 +361,11 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { stakers[user].claimableRewards = 0; - uint rewardTokenAmount = amount * config.rewardTokenPrice; - if (config.rewardToken == address(0)) { + uint rewardTokenAmount = amount * mainConfig.rewardTokenPrice; + if (mainConfig.rewardToken == address(0)) { rewardsBank.withdrawAmb(payable(user), amount); } else { - rewardsBank.withdrawErc20(config.rewardToken, payable(user), rewardTokenAmount); + rewardsBank.withdrawErc20(mainConfig.rewardToken, payable(user), rewardTokenAmount); } emit Claim(user, rewardTokenAmount); } diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index af68e9b3..da2694d9 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -18,7 +18,7 @@ contract TokenPoolsManager is AccessControl{ UpgradeableBeacon public depositedTokenPoolBeacon; mapping(string => address) public pools; - mapping(string => address) public doubleSidePools; + mapping(string => address) public depositedPools; constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon singleSideBeacon_, UpgradeableBeacon doubleSideBeacon_) { lockKeeper = lockKeeper_; @@ -36,6 +36,7 @@ contract TokenPoolsManager is AccessControl{ event DepositedPoolActivated(string name); // OWNER METHODS + // TOKEN POOL METHODS function createTokenPool(TokenPool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { console.log("Entered createPool"); @@ -65,33 +66,130 @@ contract TokenPoolsManager is AccessControl{ emit PoolActivated(_pool); } - function createDeposistedTokenPool(string calldata name_, DepositedTokenPool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function setMinStakeValue(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); + pool.setMinStakeValue(value); + } + + function setInterest(string memory _pool, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); + pool.setInterest(_interest, _interestRate); + } + + function setLockPeriod(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); + pool.setLockPeriod(period); + } + + function setRewardTokenPrice(string memory _pool, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); + pool.setRewardTokenPrice(price); + } + + function setFastUnstakePenalty(string memory _pool, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(pools[_pool] != address(0), "Pool does not exist"); + TokenPool pool = TokenPool(pools[_pool]); + pool.setFastUnstakePenalty(penalty); + } + + // DEPOSITED POOL METHODS + function createDeposistedTokenPool(DepositedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { console.log("Entered createDoubleSidePool"); bytes memory data = abi.encodeWithSignature( - "initialize(address,address,(string,address,uint256,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))", + "initialize(address,address,(string,address,address,address,uint256,uint256,uint256))", bank, lockKeeper, params); address pool = address(new BeaconProxy(address(depositedTokenPoolBeacon), data)); console.log("DoubleSidePool created at address: %s", pool); - doubleSidePools[name_] = pool; + depositedPools[params.name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit DepositedPoolCreated(name_, pool); + emit DepositedPoolCreated(params.name, pool); return pool; } + function configureDepositedTokenPoolLimits(string calldata name, DepositedTokenPool.LimitsConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[name] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[name]); + pool.setLimitsConfig(params); + } + function deactivateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(doubleSidePools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(doubleSidePools[_pool]); + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); pool.deactivate(); emit DepositedPoolDeactivated(_pool); } function activateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(doubleSidePools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(doubleSidePools[_pool]); + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); pool.activate(); emit DepositedPoolActivated(_pool); } + function setRewardTokenPriceD(string memory _pool, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setRewardTokenPrice(price); + } + + function setInterestD(string memory _pool, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setInterest(_interest, _interestRate); + } + + function setMinDepositValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setMinStakeValue(value); + } + + function setMinStakeValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setMinStakeValue(value); + } + + function setFastUnstakePenaltyD(string memory _pool, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setFastUnstakePenalty(penalty); + } + + function setUnstakeLockPeriodD(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setUnstakeLockPeriod(period); + } + + function setStakeLockPeriodD(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setStakeLockPeriod(period); + } + + function setMaxTotalStakeValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setMaxTotalStakeValue(value); + } + + function setMaxStakePerUserValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setMaxStakePerUserValue(value); + } + + function setStakeLimitsMultiplierD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(depositedPools[_pool] != address(0), "Pool does not exist"); + DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + pool.setStakeLimitsMultiplier(value); + } + // VIEW METHODS function getPoolAddress(string memory name) public view returns (address) { @@ -99,7 +197,7 @@ contract TokenPoolsManager is AccessControl{ } function getDepositedPoolAdress(string memory name) public view returns (address) { - return doubleSidePools[name]; + return depositedPools[name]; } } From da2d13309545a6a70119994d7b06b055e56c885f Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 18 Sep 2024 18:04:44 +0300 Subject: [PATCH 40/60] Update test for deposited pool --- .../staking/token/DepositedTokenPool.sol | 20 + test/staking/token/DepositedTokenPool.ts | 531 +++++++++++++++++ test/staking/token/DoubleSidePool.ts | 551 ------------------ .../token/{SingleSidePool.ts => TokenPool.ts} | 0 4 files changed, 551 insertions(+), 551 deletions(-) create mode 100644 test/staking/token/DepositedTokenPool.ts delete mode 100644 test/staking/token/DoubleSidePool.ts rename test/staking/token/{SingleSidePool.ts => TokenPool.ts} (100%) diff --git a/contracts/staking/token/DepositedTokenPool.sol b/contracts/staking/token/DepositedTokenPool.sol index bb4f9619..0fd8a404 100644 --- a/contracts/staking/token/DepositedTokenPool.sol +++ b/contracts/staking/token/DepositedTokenPool.sol @@ -8,6 +8,8 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; +import "hardhat/console.sol"; + //The side defined by the address of the token. Zero address means native coin contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { @@ -93,6 +95,7 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { active = true; mainConfig = config_; + info.lastInterestUpdate = block.timestamp; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -221,6 +224,7 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { info.totalStake += amount; info.totalRewards += rewardsAmount; if (stakers[msg.sender].stakedAt == 0) + console.log("Staked at: %s", block.timestamp); stakers[msg.sender].stakedAt = block.timestamp; require(stakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); @@ -233,6 +237,9 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { function unstake(uint amount) public { require(stakers[msg.sender].stake >= amount, "Not enough stake"); + console.log("StakedAt: %s", stakers[msg.sender].stakedAt); + console.log("block.timestamp: %s", block.timestamp); + console.log("stakeLockPeriod: %s", limitsConfig.stakeLockPeriod); require(block.timestamp - stakers[msg.sender].stakedAt >= limitsConfig.stakeLockPeriod, "Stake is locked"); uint rewardsAmount = _calcRewards(amount); @@ -325,6 +332,8 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { function getUserRewards(address user) public view returns (uint) { uint rewardsAmount = _calcRewards(stakers[user].stake); + console.log("Claimable rewards: %s", stakers[user].claimableRewards); + console.log("Rewards debt: %s", stakers[user].rewardsDebt); if (rewardsAmount + stakers[user].claimableRewards <= stakers[user].rewardsDebt) return 0; @@ -333,9 +342,16 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { // INTERNAL METHODS function _addInterest() internal { + console.log("Entering add interest"); if (info.lastInterestUpdate + mainConfig.interestRate > block.timestamp) return; + console.log("Adding interest"); uint timePassed = block.timestamp - info.lastInterestUpdate; + console.log("Time passed: %s", timePassed); + console.log("totalStake: %s", info.totalStake); + console.log("interest: %s", mainConfig.interest); + console.log("interestRate: %s", mainConfig.interestRate); uint newRewards = info.totalStake * mainConfig.interest * timePassed / BILLION / mainConfig.interestRate; + console.log("New rewards: %s", newRewards); info.totalRewards += newRewards; info.lastInterestUpdate = block.timestamp; @@ -361,6 +377,7 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { stakers[user].claimableRewards = 0; + // TODO: Use decimals for reward token price uint rewardTokenAmount = amount * mainConfig.rewardTokenPrice; if (mainConfig.rewardToken == address(0)) { rewardsBank.withdrawAmb(payable(user), amount); @@ -371,6 +388,9 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { } function _calcRewards(uint amount) internal view returns (uint) { + console.log("Total stake: %s", info.totalStake); + console.log("Total rewards: %s", info.totalRewards); + console.log("Amount: %s", amount); if (info.totalStake == 0 && info.totalRewards == 0) return amount; return amount * info.totalRewards / info.totalStake; } diff --git a/test/staking/token/DepositedTokenPool.ts b/test/staking/token/DepositedTokenPool.ts new file mode 100644 index 00000000..64c2dbff --- /dev/null +++ b/test/staking/token/DepositedTokenPool.ts @@ -0,0 +1,531 @@ +import { ethers, upgrades } from "hardhat"; +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import { + RewardsBank, + AirBond, + DepositedTokenPool, + RewardsBank__factory, + AirBond__factory, + LockKeeper__factory, + LockKeeper, +} from "../../../typechain-types"; + +const D1 = 24 * 60 * 60; +const BILLION = 1_000_000_000; + +import { expect } from "chai"; + +describe("DepositedTokenPool", function () { + let owner: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let depositedPool: DepositedTokenPool; + let rewardsBank: RewardsBank; + let lockKeeper: LockKeeper; + let depositToken: AirBond; + let profitableToken: AirBond; + + + async function deploy() { + const [owner, user1, user2] = await ethers.getSigners(); + + const rewardsBank = await new RewardsBank__factory(owner).deploy(); + const depositToken = await new AirBond__factory(owner).deploy(owner.address); + const profitableToken = await new AirBond__factory(owner).deploy(owner.address); + const lockKeeper = await new LockKeeper__factory(owner).deploy(); + + const depositedPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); + + const mainConfig: DepositedTokenPool.MainConfigStruct = { + name: "Test Deposited Pool", + depositToken: depositToken.address, + profitableToken: profitableToken.address, + rewardToken: profitableToken.address, + rewardTokenPrice: 1, // 1:1 ratio + interest: 0.10 * BILLION, // 10% + interestRate: D1, // 1 day + }; + + const limitsConfig: DepositedTokenPool.LimitsConfigStruct = { + minDepositValue: 10, + minStakeValue: 10, + fastUnstakePenalty: 0.10 * BILLION, // 10% + unstakeLockPeriod: D1, // 1 day + stakeLockPeriod: D1, // 1 day + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLimitsMultiplier: 2 * BILLION, // 2x + }; + + const depositedPool = (await upgrades.deployProxy(depositedPoolFactory, [ + rewardsBank.address, + lockKeeper.address, + mainConfig + ])) as DepositedTokenPool; + + await depositedPool.setLimitsConfig(limitsConfig); + + await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), depositedPool.address); + await depositToken.grantRole(await depositToken.MINTER_ROLE(), owner.address); + await profitableToken.grantRole(await profitableToken.MINTER_ROLE(), owner.address); + + // Mint tokens for testing + const mintAmount = ethers.utils.parseEther("1000000"); + await depositToken.mint(owner.address, mintAmount); + await profitableToken.mint(owner.address, mintAmount); + await profitableToken.mint(rewardsBank.address, mintAmount); + + // Approve tokens for testing + await depositToken.approve(depositedPool.address, mintAmount); + await profitableToken.approve(depositedPool.address, mintAmount); + + return { owner, user1, user2, depositedPool, rewardsBank, lockKeeper, depositToken, profitableToken}; + } + + beforeEach(async function () { + ({ owner, user1, user2, depositedPool, rewardsBank, lockKeeper, depositToken, profitableToken } = await loadFixture(deploy)); + }); + + describe("Initialization", function () { + it("Should initialize with correct main config", async function () { + const config = await depositedPool.getMainConfig(); + expect(config.name).to.equal("Test Deposited Pool"); + expect(config.depositToken).to.equal(depositToken.address); + expect(config.profitableToken).to.equal(profitableToken.address); + expect(config.rewardToken).to.equal(profitableToken.address); + expect(config.rewardTokenPrice).to.equal(1); + expect(config.interest).to.equal(0.10 * BILLION); + expect(config.interestRate).to.equal(D1); + }); + + it("Should initialize with correct limits config", async function () { + const limits = await depositedPool.getLimitsConfig(); + expect(limits.minDepositValue).to.equal(10); + expect(limits.minStakeValue).to.equal(10); + expect(limits.fastUnstakePenalty).to.equal(0.10 * BILLION); + expect(limits.unstakeLockPeriod).to.equal(D1); + expect(limits.stakeLockPeriod).to.equal(D1); + expect(limits.maxTotalStakeValue).to.equal(ethers.utils.parseEther("1000000")); + expect(limits.maxStakePerUserValue).to.equal(ethers.utils.parseEther("100000")); + expect(limits.stakeLimitsMultiplier).to.equal(2 * BILLION); + }); + }); + + describe("Deposit", function () { + it("Should allow deposit", async function () { + const depositAmount = ethers.utils.parseEther("100"); + await expect(depositedPool.deposit(depositAmount)) + .to.emit(depositedPool, "Deposited") + .withArgs(owner.address, depositAmount); + + const info = await depositedPool.getInfo(); + expect(info.totalDeposit).to.equal(depositAmount); + + const staker = await depositedPool.getStaker(owner.address); + expect(staker.deposit).to.equal(depositAmount); + }); + + it("Should not allow deposit below minimum", async function () { + const depositAmount = 1; + await expect(depositedPool.deposit(depositAmount)).to.be.revertedWith("Pool: deposit value is too low"); + }); + }); + + describe("Withdrawal", function () { + beforeEach(async function () { + await depositedPool.deposit(ethers.utils.parseEther("1000")); + }); + + it("Should allow withdrawal", async function () { + const withdrawAmount = ethers.utils.parseEther("500"); + await expect(depositedPool.withdraw(withdrawAmount)) + .to.emit(depositedPool, "Withdrawn") + .withArgs(owner.address, withdrawAmount); + + const info = await depositedPool.getInfo(); + expect(info.totalDeposit).to.equal(ethers.utils.parseEther("500")); + + const staker = await depositedPool.getStaker(owner.address); + expect(staker.deposit).to.equal(ethers.utils.parseEther("500")); + }); + + it("Should not allow withdrawal more than deposited", async function () { + const withdrawAmount = ethers.utils.parseEther("1001"); + await expect(depositedPool.withdraw(withdrawAmount)).to.be.revertedWith("Not enough deposit"); + }); + + it("Should not allow withdrawal that would violate stake limits", async function () { + await depositedPool.stake(ethers.utils.parseEther("500")); + await expect(depositedPool.withdraw(ethers.utils.parseEther("751"))) + .to.be.revertedWith("Pool: user max stake value exceeded"); + }); + }); + + describe("Stake", function () { + beforeEach(async function () { + // Deposit before staking + await depositedPool.deposit(ethers.utils.parseEther("1000")); + }); + + it("Should allow staking", async function () { + const stakeAmount = ethers.utils.parseEther("100"); + await expect(depositedPool.stake(stakeAmount)) + .to.emit(depositedPool, "Staked") + .withArgs(owner.address, stakeAmount); + + const info = await depositedPool.getInfo(); + expect(info.totalStake).to.equal(stakeAmount); + + const staker = await depositedPool.getStaker(owner.address); + expect(staker.stake).to.equal(stakeAmount); + }); + + it("Should not allow staking below minimum", async function () { + const stakeAmount = 1; + await expect(depositedPool.stake(stakeAmount)).to.be.revertedWith("Pool: stake value is too low"); + }); + + it("Should not allow staking above user limit", async function () { + const stakeAmount = ethers.utils.parseEther("2001"); + await expect(depositedPool.stake(stakeAmount)).to.be.revertedWith("Pool: user max stake value exceeded"); + }); + + it("Should not allow staking above total pool limit", async function () { + await depositedPool.setMaxTotalStakeValue(ethers.utils.parseEther("100")); + const stakeAmount = ethers.utils.parseEther("101"); + await expect(depositedPool.stake(stakeAmount)).to.be.revertedWith("Pool: max stake value exceeded"); + }); + }); + + describe("Unstake", function () { + const stakeAmount = ethers.utils.parseEther("100"); + + beforeEach(async function () { + await depositedPool.deposit(ethers.utils.parseEther("1000")); + await depositedPool.stake(stakeAmount); + await time.increase(D1); + }); + + it("Should allow unstaking", async function () { + await expect(depositedPool.unstake(stakeAmount)) + .to.emit(lockKeeper, "Locked"); + + const info = await depositedPool.getInfo(); + expect(info.totalStake).to.equal(0); + + const staker = await depositedPool.getStaker(owner.address); + expect(staker.stake).to.equal(0); + }); + + it("Should not allow unstaking more than staked", async function () { + await expect(depositedPool.unstake(stakeAmount.add(1))).to.be.revertedWith("Not enough stake"); + }); + + it("Should not allow unstaking before stake lock period", async function () { + await depositedPool.setStakeLockPeriod(D1 * 2); + await depositedPool.stake(stakeAmount); + await time.increase(D1); + await expect(depositedPool.unstake(stakeAmount)).to.be.revertedWith("Stake is locked"); + }); + }); + + describe("Fast Unstake", function () { + const stakeAmount = ethers.utils.parseEther("100"); + + beforeEach(async function () { + await depositedPool.deposit(ethers.utils.parseEther("1000")); + await depositedPool.stake(stakeAmount); + await time.increase(D1); + }); + + it("Should allow fast unstaking with penalty", async function () { + const balanceBefore = await profitableToken.balanceOf(owner.address); + await depositedPool.unstakeFast(stakeAmount); + const balanceAfter = await profitableToken.balanceOf(owner.address); + + const expectedReturn = stakeAmount.mul(90).div(100); // 90% due to 10% penalty + expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReturn); + + const info = await depositedPool.getInfo(); + expect(info.totalStake).to.equal(0); + }); + }); + + describe("Rewards", function () { + const stakeAmount = 1000; + + beforeEach(async function () { + await depositedPool.deposit(ethers.utils.parseEther("2000")); + await depositedPool.stake(stakeAmount); + }); + + it("Should calculate rewards correctly", async function () { + await time.increase(D1); + await depositedPool.onBlock(); + + const rewards = await depositedPool.getUserRewards(owner.address); + const expectedRewards = stakeAmount * 0.1; // 10% interest + expect(rewards).to.equal(expectedRewards); + }); + + it("Should allow claiming rewards", async function () { + await time.increase(D1); + await depositedPool.onBlock(); + + const balanceBefore = await profitableToken.balanceOf(owner.address); + await depositedPool.claim(); + const balanceAfter = await profitableToken.balanceOf(owner.address); + + const expectedRewards = stakeAmount * 0.1; // 10% interest + expect(balanceAfter.sub(balanceBefore)).to.equal(expectedRewards); + }); + }); + + describe("Edge cases", function () { + it("Should handle multiple deposits and stakes correctly", async function () { + await depositedPool.setStakeLockPeriod(0); // No lock period + const depositAmount = ethers.utils.parseEther("1000"); + const stakeAmount = ethers.utils.parseEther("500"); + + await depositedPool.deposit(depositAmount); + await depositedPool.stake(stakeAmount); + await depositedPool.deposit(depositAmount); + await depositedPool.stake(stakeAmount); + + const info = await depositedPool.getInfo(); + expect(info.totalDeposit).to.equal(depositAmount.mul(2)); + expect(info.totalStake).to.equal(stakeAmount.mul(2)); + + const staker = await depositedPool.getStaker(owner.address); + expect(staker.deposit).to.equal(depositAmount.mul(2)); + expect(staker.stake).to.equal(stakeAmount.mul(2)); + }); + + it("Should handle rewards correctly after multiple stakes and unstakes", async function () { + await depositedPool.setStakeLockPeriod(0); // No lock period + const depositAmount = ethers.utils.parseEther("2000"); + const stakeAmount = ethers.utils.parseEther("500"); + + await depositedPool.deposit(depositAmount); + await depositedPool.stake(stakeAmount); + await time.increase(D1 / 2); + await depositedPool.stake(stakeAmount); + await time.increase(D1 / 2); + await depositedPool.unstake(stakeAmount); + + await depositedPool.onBlock(); + + const rewards = await depositedPool.getUserRewards(owner.address); + // Expected rewards calculation is complex due to different staking times + expect(rewards).to.be.gt(0); + }); + + it("Should not allow actions when pool is deactivated", async function () { + await depositedPool.deactivate(); + + await expect(depositedPool.deposit(ethers.utils.parseEther("100"))).to.be.revertedWith("Pool is not active"); + await expect(depositedPool.stake(ethers.utils.parseEther("100"))).to.be.revertedWith("Pool is not active"); + }); + }); + + + describe("Interest calculation", function () { + beforeEach(async function () { + await depositedPool.deposit(ethers.utils.parseEther("1000")); + await depositedPool.stake(500); + }); + + it("Should calculate interest correctly over multiple periods", async function () { + for (let i = 0; i < 5; i++) { + await time.increase(D1); + await depositedPool.onBlock(); + } + + const rewards = await depositedPool.getUserRewards(owner.address); + //const expectedRewards = ethers.utils.parseEther("500").mul(10).div(100).mul(5); // 10% interest for 5 days + const expectedRewards = 500 * 0.1 * 5; // 10% interest for 5 days + expect(rewards).to.be.closeTo(expectedRewards, "100"); // Allow small rounding error + }); + + it("Should not add interest if called too frequently", async function () { + await depositedPool.onBlock(); + const infoBefore = await depositedPool.getInfo(); + + await time.increase(D1 / 2); // Half a day + await depositedPool.onBlock(); + const infoAfter = await depositedPool.getInfo(); + + expect(infoAfter.totalRewards).to.equal(infoBefore.totalRewards); + }); + }); + + describe("Multiple users", function () { + beforeEach(async function () { + // Setup for multiple users + await depositToken.transfer(user1.address, ethers.utils.parseEther("1000")); + await depositToken.transfer(user2.address, ethers.utils.parseEther("1000")); + await profitableToken.transfer(user1.address, ethers.utils.parseEther("1000")); + await profitableToken.transfer(user2.address, ethers.utils.parseEther("1000")); + + await depositToken.connect(user1).approve(depositedPool.address, ethers.utils.parseEther("1000")); + await depositToken.connect(user2).approve(depositedPool.address, ethers.utils.parseEther("1000")); + await profitableToken.connect(user1).approve(depositedPool.address, ethers.utils.parseEther("1000")); + await profitableToken.connect(user2).approve(depositedPool.address, ethers.utils.parseEther("1000")); + }); + + it("Should handle deposits and stakes from multiple users correctly", async function () { + await depositedPool.connect(user1).deposit(ethers.utils.parseEther("500")); + await depositedPool.connect(user2).deposit(ethers.utils.parseEther("300")); + await depositedPool.connect(user1).stake(ethers.utils.parseEther("200")); + await depositedPool.connect(user2).stake(ethers.utils.parseEther("100")); + + const infoUser1 = await depositedPool.getStaker(user1.address); + const infoUser2 = await depositedPool.getStaker(user2.address); + + expect(infoUser1.deposit).to.equal(ethers.utils.parseEther("500")); + expect(infoUser1.stake).to.equal(ethers.utils.parseEther("200")); + expect(infoUser2.deposit).to.equal(ethers.utils.parseEther("300")); + expect(infoUser2.stake).to.equal(ethers.utils.parseEther("100")); + + const poolInfo = await depositedPool.getInfo(); + expect(poolInfo.totalDeposit).to.equal(ethers.utils.parseEther("800")); + expect(poolInfo.totalStake).to.equal(ethers.utils.parseEther("300")); + }); + + it("Should distribute rewards correctly among multiple users", async function () { + await depositedPool.connect(user1).deposit(ethers.utils.parseEther("500")); + await depositedPool.connect(user2).deposit(ethers.utils.parseEther("500")); + await depositedPool.connect(user1).stake(ethers.utils.parseEther("300")); + await depositedPool.connect(user2).stake(ethers.utils.parseEther("200")); + + await time.increase(D1); + await depositedPool.onBlock(); + + const rewardsUser1 = await depositedPool.getUserRewards(user1.address); + const rewardsUser2 = await depositedPool.getUserRewards(user2.address); + + // User1 should have 60% of the rewards, User2 40% + expect(rewardsUser1).to.be.closeTo( + ethers.utils.parseEther("30"), // 10% of 300 + ethers.utils.parseEther("0.1") + ); + expect(rewardsUser2).to.be.closeTo( + ethers.utils.parseEther("20"), // 10% of 200 + ethers.utils.parseEther("0.1") + ); + }); + }); + + describe("Safety checks", function () { + it("Should not allow initialization twice", async function () { + const mainConfig: DepositedTokenPool.MainConfigStruct = { + name: "Test Deposited Pool", + depositToken: depositToken.address, + profitableToken: profitableToken.address, + rewardToken: profitableToken.address, + rewardTokenPrice: BILLION, + interest: 0.10 * BILLION, + interestRate: D1, + }; + + await expect(depositedPool.initialize(rewardsBank.address, lockKeeper.address, mainConfig)) + .to.be.revertedWith("Initializable: contract is already initialized"); + }); + + it("Should only allow admin to change configurations", async function () { + await expect(depositedPool.connect(user1).setRewardTokenPrice(BILLION * 2)) + .to.be.revertedWith("AccessControl: account " + user1.address.toLowerCase() + " is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"); + }); + + it("Should handle zero stakes and deposits correctly", async function () { + await expect(depositedPool.stake(0)).to.be.revertedWith("Pool: stake value is too low"); + await expect(depositedPool.deposit(0)).to.be.revertedWith("Pool: deposit value is too low"); + }); + }); + + describe("Native token handling", function () { + it("Should handle native token deposits if configured", async function () { + // Deploy a new pool with ETH as deposit token + const depositedPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); + const mainConfig: DepositedTokenPool.MainConfigStruct = { + name: "ETH Deposited Pool", + depositToken: ethers.constants.AddressZero, // ETH + profitableToken: profitableToken.address, + rewardToken: profitableToken.address, + rewardTokenPrice: BILLION, + interest: 0.10 * BILLION, + interestRate: D1, + }; + + const ethPool = (await upgrades.deployProxy(depositedPoolFactory, [ + rewardsBank.address, + lockKeeper.address, + mainConfig + ])) as DepositedTokenPool; + + await ethPool.setLimitsConfig({ + minDepositValue: ethers.utils.parseEther("0.1"), + minStakeValue: ethers.utils.parseEther("0.1"), + fastUnstakePenalty: 0.10 * BILLION, + unstakeLockPeriod: D1, + stakeLockPeriod: D1, + maxTotalStakeValue: ethers.utils.parseEther("1000"), + maxStakePerUserValue: ethers.utils.parseEther("100"), + stakeLimitsMultiplier: 2 * BILLION, + }); + + const depositAmount = ethers.utils.parseEther("1"); + await expect(ethPool.deposit(depositAmount, { value: depositAmount })) + .to.emit(ethPool, "Deposited") + .withArgs(owner.address, depositAmount); + + const info = await ethPool.getInfo(); + expect(info.totalDeposit).to.equal(depositAmount); + }); + + it("Should handle native token stakes if configured", async function () { + // Deploy a new pool with ETH as profitable token + const depositedPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); + const mainConfig: DepositedTokenPool.MainConfigStruct = { + name: "ETH Stake Pool", + depositToken: depositToken.address, + profitableToken: ethers.constants.AddressZero, // ETH + rewardToken: profitableToken.address, + rewardTokenPrice: BILLION, + interest: 0.10 * BILLION, + interestRate: D1, + }; + + const ethPool = (await upgrades.deployProxy(depositedPoolFactory, [ + rewardsBank.address, + lockKeeper.address, + mainConfig + ])) as DepositedTokenPool; + + await ethPool.setLimitsConfig({ + minDepositValue: ethers.utils.parseEther("0.1"), + minStakeValue: ethers.utils.parseEther("0.1"), + fastUnstakePenalty: 0.10 * BILLION, + unstakeLockPeriod: D1, + stakeLockPeriod: D1, + maxTotalStakeValue: ethers.utils.parseEther("1000"), + maxStakePerUserValue: ethers.utils.parseEther("100"), + stakeLimitsMultiplier: 2 * BILLION, + }); + + // First deposit some tokens + await depositToken.approve(ethPool.address, ethers.utils.parseEther("10")); + await ethPool.deposit(ethers.utils.parseEther("10")); + + const stakeAmount = ethers.utils.parseEther("1"); + await expect(ethPool.stake(stakeAmount, { value: stakeAmount })) + .to.emit(ethPool, "Staked") + .withArgs(owner.address, stakeAmount); + + const info = await ethPool.getInfo(); + expect(info.totalStake).to.equal(stakeAmount); + }); + }); +}); diff --git a/test/staking/token/DoubleSidePool.ts b/test/staking/token/DoubleSidePool.ts deleted file mode 100644 index c4967480..00000000 --- a/test/staking/token/DoubleSidePool.ts +++ /dev/null @@ -1,551 +0,0 @@ -import { ethers, upgrades } from "hardhat"; -import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { - RewardsBank, - AirBond, - DoubleSidePool, - RewardsBank__factory, - AirBond__factory, - LockKeeper__factory, - LockKeeper, -} from "../../../typechain-types"; - -const D1 = 24 * 60 * 60; -const BILLION = 1_000_000_000; - -import { expect } from "chai"; - -describe("DoubleSidePool", function () { - let owner: SignerWithAddress; - let doubleSidePool: DoubleSidePool; - let rewardsBank: RewardsBank; - let lockKeeper: LockKeeper; - let mainToken: AirBond; - let dependantToken: AirBond; - - async function deploy() { - const [owner] = await ethers.getSigners(); - const rewardsBank = await new RewardsBank__factory(owner).deploy(); - const mainToken = await new AirBond__factory(owner).deploy(owner.address); - const dependantToken = await new AirBond__factory(owner).deploy(owner.address); - const lockKeeper = await new LockKeeper__factory(owner).deploy(); - - const doubleSidePoolFactory = await ethers.getContractFactory("DoubleSidePool"); - - const mainSideConfig: DoubleSidePool.MainSideConfigStruct = { - token: mainToken.address, - rewardToken: mainToken.address, - rewardTokenPrice: 1, // 1:1 ratio - minStakeValue: 10, - unstakeLockPeriod: D1, // 1 day - fastUnstakePenalty: 0.10 * BILLION, // 10% - lockPeriod: D1, // 1 day - interest: 0.10 * BILLION, // 10% - interestRate: D1, // 1 day - }; - - const doubleSidePool = (await upgrades.deployProxy(doubleSidePoolFactory, [ - rewardsBank.address, - lockKeeper.address, - "Test Double Side Pool", - mainSideConfig - ])) as DoubleSidePool; - - await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), doubleSidePool.address); - await mainToken.grantRole(await mainToken.MINTER_ROLE(), owner.address); - await dependantToken.grantRole(await dependantToken.MINTER_ROLE(), owner.address); - - await mainToken.mint(owner.address, ethers.utils.parseEther("1000000")); - await dependantToken.mint(owner.address, ethers.utils.parseEther("1000000")); - - return { owner, doubleSidePool, rewardsBank, lockKeeper, mainToken, dependantToken }; - } - - beforeEach(async function () { - ({ owner, doubleSidePool, rewardsBank, lockKeeper, mainToken, dependantToken } = await loadFixture(deploy)); - }); - - describe("Initialization", function () { - it("Should initialize with correct main side config", async function () { - const config = await doubleSidePool.mainSideConfig(); - expect(config.token).to.equal(mainToken.address); - expect(config.rewardToken).to.equal(mainToken.address); - expect(config.rewardTokenPrice).to.equal(1); - expect(config.minStakeValue).to.equal(10); - expect(config.unstakeLockPeriod).to.equal(D1); - expect(config.fastUnstakePenalty).to.equal(0.10 * BILLION); - expect(config.lockPeriod).to.equal(D1); - expect(config.interest).to.equal(0.10 * BILLION); - expect(config.interestRate).to.equal(D1); - }); - - it("Should not have a dependant side initially", async function () { - expect(await doubleSidePool.hasSecondSide()).to.be.false; - }); - }); - - describe("Owner Methods", function () { - it("Should deactivate and activate the pool", async function () { - await doubleSidePool.deactivate(); - expect(await doubleSidePool.active()).to.be.false; - - await doubleSidePool.activate(); - expect(await doubleSidePool.active()).to.be.true; - }); - - it("Should add a dependant side", async function () { - const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { - token: dependantToken.address, - rewardToken: dependantToken.address, - rewardTokenPrice: BILLION, - minStakeValue: 5, - unstakeLockPeriod: D1, - fastUnstakePenalty: 0.05 * BILLION, - lockPeriod: D1, - interest: 0.15 * BILLION, - interestRate: D1, - maxTotalStakeValue: ethers.utils.parseEther("1000000"), - maxStakePerUserValue: ethers.utils.parseEther("100000"), - stakeLockPeriod: D1, - stakeLimitsMultiplier: 2 * BILLION, - }; - - await doubleSidePool.addDependantSide(dependantSideConfig); - expect(await doubleSidePool.hasSecondSide()).to.be.true; - - const config = await doubleSidePool.dependantSideConfig(); - expect(config.token).to.equal(dependantToken.address); - expect(config.rewardToken).to.equal(dependantToken.address); - expect(config.rewardTokenPrice).to.equal(BILLION); - expect(config.minStakeValue).to.equal(5); - expect(config.unstakeLockPeriod).to.equal(D1); - expect(config.fastUnstakePenalty).to.equal(0.05 * BILLION); - expect(config.lockPeriod).to.equal(D1); - expect(config.interest).to.equal(0.15 * BILLION); - expect(config.interestRate).to.equal(D1); - expect(config.maxTotalStakeValue).to.equal(ethers.utils.parseEther("1000000")); - expect(config.maxStakePerUserValue).to.equal(ethers.utils.parseEther("100000")); - expect(config.stakeLockPeriod).to.equal(D1); - expect(config.stakeLimitsMultiplier).to.equal(2 * BILLION); - }); - }); - - describe("Main Side Staking", function () { - beforeEach(async function () { - await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - }); - - it("Should allow staking on the main side", async function () { - const stake = ethers.utils.parseEther("1000"); - await doubleSidePool.stakeMainSide(stake); - const info = await doubleSidePool.mainSideInfo(); - expect(info.totalStake).to.equal(stake); - const staker = await doubleSidePool.getMainSideStaker(owner.address); - expect(staker.stake).to.equal(stake); - }); - - it("Should not allow staking when pool is deactivated", async function () { - await doubleSidePool.deactivate(); - await expect(doubleSidePool.stakeMainSide(1000)).to.be.revertedWith("Pool is not active"); - }); - - it("Should not allow staking below minimum stake value", async function () { - await expect(doubleSidePool.stakeMainSide(1)).to.be.revertedWith("Pool: stake value is too low"); - }); - }); - - describe("Main Side Unstaking", function () { - const stake = ethers.utils.parseEther("1000"); - - beforeEach(async function () { - await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - await doubleSidePool.stakeMainSide(stake); - }); - - it("Should allow unstaking with rewards", async function () { - await time.increase(D1); - await doubleSidePool.onBlock(); - - await expect(doubleSidePool.unstakeMainSide(stake)).to.emit(lockKeeper, "Locked"); - const info = await doubleSidePool.mainSideInfo(); - expect(info.totalStake).to.equal(0); - const staker = await doubleSidePool.getMainSideStaker(owner.address); - expect(staker.stake).to.equal(0); - }); - - it("Should allow fast unstaking with rewards", async function () { - await time.increase(D1); - await doubleSidePool.onBlock(); - - const balanceBefore = await mainToken.balanceOf(owner.address); - await doubleSidePool.unstakeFastMainSide(stake); - const balanceAfter = await mainToken.balanceOf(owner.address); - expect(balanceAfter.sub(balanceBefore)).to.equal(stake.mul(90).div(100)); // 90% due to 10% penalty - }); - - it("Should not allow unstaking more than staked", async function () { - await expect(doubleSidePool.unstakeMainSide(stake.mul(2))).to.be.revertedWith("Not enough stake"); - }); - - it("Should allow unstaking when pool is deactivated", async function () { - await doubleSidePool.deactivate(); - await expect(doubleSidePool.unstakeMainSide(stake)).to.emit(lockKeeper, "Locked"); - }); - }); - - describe("Main Side Rewards", function () { - beforeEach(async function () { - await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - await mainToken.transfer(rewardsBank.address, ethers.utils.parseEther("10000")); - }); - - it("Should allow claiming rewards", async function () { - await doubleSidePool.stakeMainSide(1000); - - await time.increase(D1); - await doubleSidePool.onBlock(); - - const expectedReward = 100; // 10% interest - const rewards = await doubleSidePool.getUserMainSideRewards(owner.address); - console.log("Main side rewards: ", rewards.toString()); - expect(rewards).to.equal(expectedReward); - - const balanceBefore = await mainToken.balanceOf(owner.address); - await doubleSidePool.claimMainSide(); - const balanceAfter = await mainToken.balanceOf(owner.address); - expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); - }); - - it("Should allow claiming rewards when pool is deactivated", async function () { - await doubleSidePool.stakeMainSide(1000); - - await time.increase(D1); - await doubleSidePool.onBlock(); - - await doubleSidePool.deactivate(); - - const expectedReward = 100; // 10% interest - const rewards = await doubleSidePool.getUserMainSideRewards(owner.address); - expect(rewards).to.equal(expectedReward); - - const balanceBefore = await mainToken.balanceOf(owner.address); - await doubleSidePool.claimMainSide(); - const balanceAfter = await mainToken.balanceOf(owner.address); - expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); - }); - }); - - describe("Dependant Side", function () { - beforeEach(async function () { - const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { - token: dependantToken.address, - rewardToken: dependantToken.address, - rewardTokenPrice: 1, - minStakeValue: 10, - unstakeLockPeriod: D1, - fastUnstakePenalty: 0.05 * BILLION, - lockPeriod: D1, - interest: 0.15 * BILLION, - interestRate: D1, - maxTotalStakeValue: BILLION * 1000, - maxStakePerUserValue: BILLION, - stakeLockPeriod: D1 * 30, - stakeLimitsMultiplier: 2, - }; - - await doubleSidePool.addDependantSide(dependantSideConfig); - await dependantToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - await mainToken.transfer(rewardsBank.address, ethers.utils.parseEther("10000")); - await dependantToken.transfer(rewardsBank.address, ethers.utils.parseEther("10000")); - }); - - it("Should allow staking on the dependant side", async function () { - const mainStake = 100000; - await doubleSidePool.stakeMainSide(mainStake); - - const dependantStake = 100; - await doubleSidePool.stakeDependantSide(dependantStake); - - const info = await doubleSidePool.dependantSideInfo(); - expect(info.totalStake).to.equal(dependantStake); - const staker = await doubleSidePool.getDependantSideStaker(owner.address); - expect(staker.stake).to.equal(dependantStake); - }); - - it("Should not allow staking on dependant side beyond the limit", async function () { - const mainStake = 1000; - await doubleSidePool.stakeMainSide( mainStake); - - const dependantStake = ethers.utils.parseEther("2001"); // Exceeds 2x main stake - await expect(doubleSidePool.stakeDependantSide(dependantStake)).to.be.revertedWith("Pool: user max stake value exceeded"); - }); - - it("Should allow unstaking from the dependant side", async function () { - await doubleSidePool.setStakeLockPeriod(0); - const mainStake = 1000; - await doubleSidePool.stakeMainSide(mainStake); - - const dependantStake = 100; - await doubleSidePool.stakeDependantSide(dependantStake); - - await time.increase(D1); - await doubleSidePool.onBlock(); - - await expect(doubleSidePool.unstakeDependantSide(dependantStake)).to.emit(lockKeeper, "Locked"); - const info = await doubleSidePool.dependantSideInfo(); - expect(info.totalStake).to.equal(0); - const staker = await doubleSidePool.getDependantSideStaker(owner.address); - expect(staker.stake).to.equal(0); - }); - - it("Should allow claiming rewards from the dependant side", async function () { - const mainStake = 1000; - await doubleSidePool.stakeMainSide( mainStake); - - const dependantStake = 100; - await doubleSidePool.stakeDependantSide(dependantStake); - - await time.increase(D1); - await doubleSidePool.onBlock(); - - const expectedReward = 15; // 15% interest - const rewards = await doubleSidePool.getUserDependantSideRewards(owner.address); - expect(rewards).to.equal(expectedReward); - - const balanceBefore = await dependantToken.balanceOf(owner.address); - await doubleSidePool.claimDependantSide(); - const balanceAfter = await dependantToken.balanceOf(owner.address); - expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReward); - }); - - it("Should not allow staking on dependant side when total stake limit is reached", async function () { - await doubleSidePool.setMaxTotalStakeValue(1000); - const mainStake = 100000; // Set a high main stake to avoid individual limit - await doubleSidePool.stakeMainSide(mainStake); - - const maxStake = 1000; - await doubleSidePool.stakeDependantSide(maxStake); - - await expect(doubleSidePool.stakeDependantSide(10)).to.be.revertedWith("Pool: max stake value exceeded"); - }); - - it("Should not allow unstaking from dependant side before stake lock period", async function () { - const mainStake = 1000; - await doubleSidePool.stakeMainSide(mainStake); - - const dependantStake = 100; - await doubleSidePool.stakeDependantSide(dependantStake); - - await expect(doubleSidePool.unstakeDependantSide(dependantStake)).to.be.revertedWith("Stake is locked"); - }); - - it("Should allow fast unstaking from dependant side with penalty", async function () { - await doubleSidePool.setStakeLockPeriod(0); - const mainStake = 1000; - await doubleSidePool.stakeMainSide(mainStake); - - const dependantStake = 100; - await doubleSidePool.stakeDependantSide(dependantStake); - - //await time.increase(D1); - //await doubleSidePool.onBlock(); - - const balanceBefore = await dependantToken.balanceOf(owner.address); - await doubleSidePool.unstakeFastDependantSide(dependantStake); - const balanceAfter = await dependantToken.balanceOf(owner.address); - - const expectedReturn = 95; // 95% due to 5% penalty - expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReturn); - }); - - it("Should update stake limits when main side stake changes", async function () { - const initialMainStake = 1000; - await doubleSidePool.stakeMainSide(initialMainStake); - - const maxDependantStake = 2000; // 2x multiplier - await doubleSidePool.stakeDependantSide(maxDependantStake); - - // Increase main stake - const additionalMainStake = 100; - await doubleSidePool.stakeMainSide(additionalMainStake); - - // Now we should be able to stake more on the dependant side - const additionalDependantStake = 200; - await expect(doubleSidePool.stakeDependantSide(additionalDependantStake)).to.not.be.reverted; - }); - - it("Should not allow adding dependant side twice", async function () { - const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { - token: dependantToken.address, - rewardToken: dependantToken.address, - rewardTokenPrice: BILLION, - minStakeValue: 5, - unstakeLockPeriod: D1, - fastUnstakePenalty: 0.05 * BILLION, - lockPeriod: D1, - interest: 0.15 * BILLION, - interestRate: D1, - maxTotalStakeValue: ethers.utils.parseEther("1000000"), - maxStakePerUserValue: ethers.utils.parseEther("100000"), - stakeLockPeriod: D1, - stakeLimitsMultiplier: 2 * BILLION, - }; - - await expect(doubleSidePool.addDependantSide(dependantSideConfig)).to.be.revertedWith("Second side already exists"); - }); - - }); - - describe("Interaction between Main and Dependant Sides", function () { - beforeEach(async function () { - const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { - token: dependantToken.address, - rewardToken: dependantToken.address, - rewardTokenPrice: BILLION, - minStakeValue: 5, - unstakeLockPeriod: D1, - fastUnstakePenalty: 0.05 * BILLION, - lockPeriod: D1, - interest: 0.15 * BILLION, - interestRate: D1, - maxTotalStakeValue: ethers.utils.parseEther("1000000"), - maxStakePerUserValue: ethers.utils.parseEther("100000"), - stakeLockPeriod: D1, - stakeLimitsMultiplier: 2, - }; - - await doubleSidePool.addDependantSide(dependantSideConfig); - await dependantToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - }); - - it("Should correctly calculate rewards for both sides", async function () { - const mainStake = 1000; - await doubleSidePool.stakeMainSide(mainStake); - - const dependantStake = 500; - await doubleSidePool.stakeDependantSide(dependantStake); - - await time.increase(D1); - await doubleSidePool.onBlock(); - - const mainRewards = await doubleSidePool.getUserMainSideRewards(owner.address); - const dependantRewards = await doubleSidePool.getUserDependantSideRewards(owner.address); - - expect(mainRewards).to.equal(mainStake * 10 / 100 ); // 10% interest - expect(dependantRewards).to.equal(dependantStake * 15 / 100); // 15% interest - }); - - it("Should allow staking and unstaking on both sides independently", async function () { - await doubleSidePool.setStakeLockPeriod(0); - const mainStake = ethers.utils.parseEther("1000"); - await doubleSidePool.stakeMainSide(mainStake); - - const dependantStake = ethers.utils.parseEther("500"); - await doubleSidePool.stakeDependantSide(dependantStake); - - await doubleSidePool.unstakeMainSide(mainStake.div(2)); - await doubleSidePool.unstakeDependantSide(dependantStake.div(2)); - - const mainStaker = await doubleSidePool.getMainSideStaker(owner.address); - const dependantStaker = await doubleSidePool.getDependantSideStaker(owner.address); - - expect(mainStaker.stake).to.equal(mainStake.div(2)); - expect(dependantStaker.stake).to.equal(dependantStake.div(2)); - }); - - it("Should enforce dependant side limits based on main side stake", async function () { - const mainStake = 1000; - await doubleSidePool.stakeMainSide(mainStake); - - const maxDependantStake = 2000; // 2x multiplier - await doubleSidePool.stakeDependantSide(maxDependantStake); - - // Trying to stake more should fail - await expect(doubleSidePool.stakeDependantSide(100)).to.be.revertedWith("Pool: user max stake value exceeded"); - - // Unstake half of main side - await time.increase(D1); - await doubleSidePool.unstakeMainSide(500); - - // Trying to stake on dependant side should still fail due to existing stake - await expect(doubleSidePool.stakeDependantSide(100)).to.be.revertedWith("Pool: user max stake value exceeded"); - }); - }); - - describe("Edge cases and error handling", function () { - beforeEach(async function () { - const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { - token: dependantToken.address, - rewardToken: dependantToken.address, - rewardTokenPrice: BILLION, - minStakeValue: 5, - unstakeLockPeriod: D1, - fastUnstakePenalty: 0.05 * BILLION, - lockPeriod: D1, - interest: 0.15 * BILLION, - interestRate: D1, - maxTotalStakeValue: ethers.utils.parseEther("1000000"), - maxStakePerUserValue: ethers.utils.parseEther("100000"), - stakeLockPeriod: D1, - stakeLimitsMultiplier: 2, - }; - - await doubleSidePool.addDependantSide(dependantSideConfig); - await dependantToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - await mainToken.approve(doubleSidePool.address, ethers.utils.parseEther("1000000")); - }); - - it("Should handle zero stakes correctly", async function () { - await expect(doubleSidePool.stakeMainSide(0)).to.be.revertedWith("Pool: stake value is too low"); - await expect(doubleSidePool.stakeDependantSide(0)).to.be.revertedWith("Pool: stake value is too low"); - }); - - it("Should handle unstaking more than staked amount", async function () { - const stake = ethers.utils.parseEther("100"); - await doubleSidePool.stakeMainSide(stake); - await doubleSidePool.stakeDependantSide(stake); - - await time.increase(D1); - - await expect(doubleSidePool.unstakeMainSide(stake.mul(2))).to.be.revertedWith("Not enough stake"); - await expect(doubleSidePool.unstakeDependantSide(stake.mul(2))).to.be.revertedWith("Not enough stake"); - }); - - it("Should handle claiming rewards when there are no rewards", async function () { - await doubleSidePool.claimMainSide(); - await doubleSidePool.claimDependantSide(); - // These should not revert, but also should not transfer any tokens - }); - - it("Should handle multiple stake and unstake operations correctly", async function () { - await doubleSidePool.setStakeLockPeriod(0); - const stake1 = 100; - const stake2 = 200; - const stake3 = 300; - - await doubleSidePool.stakeMainSide(stake1); - await doubleSidePool.stakeDependantSide(stake1); - - await time.increase(D1 / 2); - - await doubleSidePool.stakeMainSide(stake2); - await doubleSidePool.stakeDependantSide(stake2); - - await time.increase(D1 / 2); - - await doubleSidePool.unstakeMainSide(stake3); - await doubleSidePool.unstakeDependantSide(stake3); - - const mainStaker = await doubleSidePool.getMainSideStaker(owner.address); - const dependantStaker = await doubleSidePool.getDependantSideStaker(owner.address); - - expect(mainStaker.stake).to.equal(stake1 + stake2 - stake3); - expect(dependantStaker.stake).to.equal(stake1 + stake2 - stake3); - }); - }); - -}); diff --git a/test/staking/token/SingleSidePool.ts b/test/staking/token/TokenPool.ts similarity index 100% rename from test/staking/token/SingleSidePool.ts rename to test/staking/token/TokenPool.ts From b188890525f2fc81e2dc078054c2448898a57581 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Wed, 18 Sep 2024 19:10:21 +0300 Subject: [PATCH 41/60] Update tests --- contracts/staking/token/TokenPoolsManager.sol | 2 +- test/staking/token/TokenPool.ts | 90 ++++---- test/staking/token/TokenPoolsManager.ts | 209 +++++++++++++----- 3 files changed, 197 insertions(+), 104 deletions(-) diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index da2694d9..7add30d4 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -145,7 +145,7 @@ contract TokenPoolsManager is AccessControl{ function setMinDepositValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); - pool.setMinStakeValue(value); + pool.setMinDepositValue(value); } function setMinStakeValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index 09e6da62..2dcda556 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { RewardsBank, AirBond, - SingleSidePool, + TokenPool, RewardsBank__factory, AirBond__factory, LockKeeper__factory, @@ -18,7 +18,7 @@ const BILLION = 1000000000; import { expect } from "chai"; describe("SingleSidePool", function () { let owner: SignerWithAddress; - let singleSidePool: SingleSidePool; + let tokenPool: TokenPool; let rewardsBank: RewardsBank; let lockKeeper: LockKeeper; let token: AirBond; @@ -29,9 +29,9 @@ describe("SingleSidePool", function () { const token = await new AirBond__factory(owner).deploy(owner.address); const lockKeeper = await new LockKeeper__factory(owner).deploy(); - const singleSidePoolFactory = await ethers.getContractFactory("SingleSidePool"); + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - const singleSidePoolParams: SingleSidePool.ConfigStruct = { + const tokenPoolParams: TokenPool.ConfigStruct = { token: token.address, name: "Test", minStakeValue: 10, @@ -43,50 +43,50 @@ describe("SingleSidePool", function () { rewardTokenPrice: 1, }; - const singleSidePool = (await upgrades.deployProxy(singleSidePoolFactory, [rewardsBank.address, lockKeeper.address, singleSidePoolParams])) as SingleSidePool; + const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [rewardsBank.address, lockKeeper.address, tokenPoolParams])) as TokenPool; - await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), singleSidePool.address)).wait(); + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), tokenPool.address)).wait(); await (await token.grantRole(await token.MINTER_ROLE(), owner.address)).wait(); await token.mint(owner.address, 100000000000); - return { owner, singleSidePool, rewardsBank, lockKeeper, token }; + return { owner, tokenPool, rewardsBank, lockKeeper, token }; } beforeEach(async function () { - ({ owner, singleSidePool, rewardsBank, lockKeeper, token } = await loadFixture(deploy)); + ({ owner, tokenPool, rewardsBank, lockKeeper, token } = await loadFixture(deploy)); }); describe("Owner Methods", function () { it("Should deactivate and activate the pool", async function () { - await singleSidePool.deactivate(); - expect(await singleSidePool.active()).to.equal(false); + await tokenPool.deactivate(); + expect(await tokenPool.active()).to.equal(false); - await singleSidePool.activate(); - expect(await singleSidePool.active()).to.equal(true); + await tokenPool.activate(); + expect(await tokenPool.active()).to.equal(true); }); }); describe("Staking", function () { beforeEach(async function () { - await token.approve(singleSidePool.address, 1000000); + await token.approve(tokenPool.address, 1000000); }); it("Should allow staking", async function () { const stake = 1000; - await singleSidePool.stake(stake); - const info = await singleSidePool.info(); + await tokenPool.stake(stake); + const info = await tokenPool.info(); expect(info.totalStake).to.equal(stake); - expect(await singleSidePool.getStake(owner.address)).to.equal(stake); + expect(await tokenPool.getStake(owner.address)).to.equal(stake); }); it("Should not allow staking when pool is deactivated", async function () { - await singleSidePool.deactivate(); - await expect(singleSidePool.stake(1000)).to.be.revertedWith("Pool is not active"); + await tokenPool.deactivate(); + await expect(tokenPool.stake(1000)).to.be.revertedWith("Pool is not active"); }); it("Should not allow staking below minimum stake value", async function () { - await expect(singleSidePool.stake(1)).to.be.revertedWith("Pool: stake value is too low"); + await expect(tokenPool.stake(1)).to.be.revertedWith("Pool: stake value is too low"); }); }); @@ -95,62 +95,62 @@ describe("SingleSidePool", function () { const stake = 1000; beforeEach(async function () { - await token.approve(singleSidePool.address, 1000000000); - await singleSidePool.stake(stake); + await token.approve(tokenPool.address, 1000000000); + await tokenPool.stake(stake); }); it("Should allow unstaking with rewards", async function () { await time.increase(D1); - await singleSidePool.onBlock(); + await tokenPool.onBlock(); - await expect(await singleSidePool.unstake(stake)).to.emit(lockKeeper, "Locked"); - const info = await singleSidePool.info(); + await expect(await tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); + const info = await tokenPool.info(); expect(info.totalStake).to.equal(0); - expect(await singleSidePool.getStake(owner.address)).to.equal(0); + expect(await tokenPool.getStake(owner.address)).to.equal(0); }); it("Should allow fast unstaking with rewards", async function () { await time.increase(D1); - await singleSidePool.onBlock(); + await tokenPool.onBlock(); const balanceBefore = await token.balanceOf(owner.address); - await singleSidePool.unstakeFast(stake); + await tokenPool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); it("Should allow unstaking without rewards", async function () { - await expect(singleSidePool.unstake(stake)).to.emit(lockKeeper, "Locked"); + await expect(tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); }); it("Should allow fast unstaking without rewards", async function () { const balanceBefore = await token.balanceOf(owner.address); - await singleSidePool.unstakeFast(stake); + await tokenPool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); it("Should not allow unstaking more than staked", async function () { - await expect(singleSidePool.unstake(stake * 2)).to.be.revertedWith("Not enough stake"); + await expect(tokenPool.unstake(stake * 2)).to.be.revertedWith("Not enough stake"); }); it("Should not allow fast unstaking more than staked", async function () { const balanceBefore = await token.balanceOf(owner.address); - await singleSidePool.unstakeFast(stake); + await tokenPool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); it("Should allow unstaking when pool is deactivated", async function () { - await singleSidePool.deactivate(); - await expect(singleSidePool.unstake(stake)).to.emit(lockKeeper, "Locked"); + await tokenPool.deactivate(); + await expect(tokenPool.unstake(stake)).to.emit(lockKeeper, "Locked"); }); it("Should allow fast unstaking when pool is deactivated", async function () { - await singleSidePool.deactivate(); + await tokenPool.deactivate(); const balanceBefore = await token.balanceOf(owner.address); - await singleSidePool.unstakeFast(stake); + await tokenPool.unstakeFast(stake); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(stake * 0.9); }); @@ -159,42 +159,42 @@ describe("SingleSidePool", function () { describe("Rewards", function () { beforeEach(async function () { await token.mint(owner.address, 100000000000); - await token.approve(singleSidePool.address, 1000000); + await token.approve(tokenPool.address, 1000000); await token.transfer(rewardsBank.address, 10000); }); it("Should allow claiming rewards", async function () { - await singleSidePool.stake(1000); + await tokenPool.stake(1000); // Wait for 1 day await time.increase(D1); - await singleSidePool.onBlock(); + await tokenPool.onBlock(); const expectedReward = 100; - const rewards = await singleSidePool.getUserRewards(owner.address); + const rewards = await tokenPool.getUserRewards(owner.address); expect (rewards).to.equal(expectedReward); const balanceBefore = await token.balanceOf(owner.address); - await singleSidePool.claim(); + await tokenPool.claim(); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(100); }); it("Should allow claiming rewards when pool is deactivated", async function () { - await singleSidePool.stake(1000); + await tokenPool.stake(1000); // Wait for 1 day await time.increase(D1); - await singleSidePool.onBlock(); + await tokenPool.onBlock(); - await singleSidePool.deactivate(); + await tokenPool.deactivate(); const expectedReward = 100; - const rewards = await singleSidePool.getUserRewards(owner.address); + const rewards = await tokenPool.getUserRewards(owner.address); expect (rewards).to.equal(expectedReward); const balanceBefore = await token.balanceOf(owner.address); - await singleSidePool.claim(); + await tokenPool.claim(); const balanceAfter = await token.balanceOf(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(100); }); diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index f17b1961..348aab25 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -6,16 +6,17 @@ import { TokenPoolsManager, RewardsBank, AirBond__factory, - SingleSidePool, + TokenPool, + DepositedTokenPool, RewardsBank__factory, TokenPoolsManager__factory, LockKeeper__factory, LockKeeper, - DoubleSidePool, } from "../../../typechain-types"; -import SignleSidePoolJson from "../../../artifacts/contracts/staking/token/SingleSidePool.sol/SingleSidePool.json"; -import DoubleSidePoolJson from "../../../artifacts/contracts/staking/token/DoubleSidePool.sol/DoubleSidePool.json"; +import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; +import DepositedTokenPoolJson from "../../../artifacts/contracts/staking/token/DepositedTokenPool.sol/DepositedTokenPool.json"; + import { expect } from "chai"; @@ -32,16 +33,16 @@ describe("PoolsManager", function () { const rewardsBank = await new RewardsBank__factory(owner).deploy(); const airBond = await new AirBond__factory(owner).deploy(owner.address); - const singleSidePoolFactory = await ethers.getContractFactory("SingleSidePool"); - const singleSidePoolBeacon = await upgrades.deployBeacon(singleSidePoolFactory); + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); + const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); - const doubleSidePoolFactory = await ethers.getContractFactory("DoubleSidePool"); - const doubleSidePoolBeacon = await upgrades.deployBeacon(doubleSidePoolFactory); + const depositedTokenPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); + const depositedTokenPoolBeacon = await upgrades.deployBeacon(depositedTokenPoolFactory); const lockKeeper = await new LockKeeper__factory(owner).deploy(); const poolsManager = await new TokenPoolsManager__factory(owner) - .deploy(rewardsBank.address, lockKeeper.address, singleSidePoolBeacon.address, doubleSidePoolBeacon.address); + .deploy(rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address, depositedTokenPoolBeacon.address); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); const tokenAddr = airBond.address; @@ -53,116 +54,208 @@ describe("PoolsManager", function () { ({ poolsManager, rewardsBank, lockKeeper, tokenAddr, owner } = await loadFixture(deploy)); }); - describe("SingleSidePool Management", function () { - it("Should allow the owner to create a single side pool", async function () { - const singleSidePoolConfig: SingleSidePool.ConfigStruct = { + describe("TokenPool Management", function () { + it("Should allow the owner to create a token pool", async function () { + const tokenPoolConfig: TokenPool.ConfigStruct = { token: tokenAddr, name: "TestPool", - minStakeValue: 10, rewardToken: tokenAddr, rewardTokenPrice: 1, + minStakeValue: 10, fastUnstakePenalty: 100000, // 10% interest: 100000, // 10% interestRate: 24 * 60 * 60, // 24 hours lockPeriod: 24 * 60 * 60, // 24 hours }; - const tx = await poolsManager.createSingleSidePool(singleSidePoolConfig); + const tx = await poolsManager.createTokenPool(tokenPoolConfig); const receipt = await tx.wait(); - const poolAddress = receipt.events![4].args![1]; + console.log(receipt.events); + const poolAddress = receipt.events![3].args![1]; expect(await poolsManager.getPoolAddress("TestPool")).to.equal(poolAddress); }); - it("Should activate and deactivate a pool", async function () { - const singleSidePoolConfig: SingleSidePool.ConfigStruct = { + it("Should activate and deactivate a token pool", async function () { + const tokenPoolConfig: TokenPool.ConfigStruct = { token: tokenAddr, name: "TestPool", - minStakeValue: 10, rewardToken: tokenAddr, rewardTokenPrice: 1, + minStakeValue: 10, fastUnstakePenalty: 100000, // 10% interest: 100000, // 10% interestRate: 24 * 60 * 60, // 24 hours lockPeriod: 24 * 60 * 60, // 24 hours }; - await poolsManager.createSingleSidePool(singleSidePoolConfig); + await poolsManager.createTokenPool(tokenPoolConfig); const poolAddress = await poolsManager.getPoolAddress("TestPool"); - const proxyPool = new ethers.Contract(poolAddress, SignleSidePoolJson.abi, owner); + const proxyPool = new ethers.Contract(poolAddress, TokenPoolJson.abi, owner); expect(await proxyPool.active()).to.equal(true); - await poolsManager.deactivateSingleSidePool("TestPool"); + await poolsManager.deactivateTokenPool("TestPool"); expect(await proxyPool.active()).to.equal(false); + await poolsManager.activateTokenPool("TestPool"); + expect(await proxyPool.active()).to.equal(true); }); - }); - - describe("DoubleSidePool Management", function () { - it("Should allow the owner to create a double side pool", async function () { - const doubleSidePoolConfig: DoubleSidePool.MainSideConfigStruct = { + it("Should allow updating token pool parameters", async function () { + const tokenPoolConfig: TokenPool.ConfigStruct = { token: tokenAddr, + name: "TestPool", rewardToken: tokenAddr, rewardTokenPrice: 1, minStakeValue: 10, - unstakeLockPeriod: 24 * 60 * 60, // 24 hours fastUnstakePenalty: 100000, // 10% interest: 100000, // 10% interestRate: 24 * 60 * 60, // 24 hours lockPeriod: 24 * 60 * 60, // 24 hours }; - const tx = await poolsManager.createDoubleSidePool("TestPool", doubleSidePoolConfig); + await poolsManager.createTokenPool(tokenPoolConfig); + const poolAddress = await poolsManager.getPoolAddress("TestPool"); + const proxyPool = new ethers.Contract(poolAddress, TokenPoolJson.abi, owner); + + + await poolsManager.setInterest("TestPool", 200000, 48 * 60 * 60); + await poolsManager.setFastUnstakePenalty("TestPool", 200000); + await poolsManager.setMinStakeValue("TestPool", 20); + await poolsManager.setLockPeriod("TestPool", 48 * 60 * 60); + await poolsManager.setRewardTokenPrice("TestPool", 2); + const newConfig = await proxyPool.getConfig(); + expect(newConfig.interest).to.equal(200000); + expect(newConfig.interestRate).to.equal(48 * 60 * 60); + expect(newConfig.lockPeriod).to.equal(48 * 60 * 60); + expect(newConfig.rewardTokenPrice).to.equal(2); + expect(newConfig.fastUnstakePenalty).to.equal(200000); + expect(newConfig.minStakeValue).to.equal(20); + }); + }); + + describe("DepositedTokenPool Management", function () { + it("Should allow the owner to create a deposited token pool", async function () { + const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + name: "TestDepositedPool", + depositToken: tokenAddr, + profitableToken: tokenAddr, + rewardToken: tokenAddr, + rewardTokenPrice: 1, + interest: 100000, // 10% + interestRate: 24 * 60 * 60, // 24 hours + }; + + const tx = await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); const receipt = await tx.wait(); - const poolAddress = receipt.events![4].args![1]; + const poolAddress = receipt.events![3].args![1]; - expect(await poolsManager.getDoubleSidePoolAddress("TestPool")).to.equal(poolAddress); + expect(await poolsManager.getDepositedPoolAdress("TestDepositedPool")).to.equal(poolAddress); }); - // add dependant side - it("Should allow the owner to add a dependant side to a double side pool", async function () { - const doubleSidePoolConfig: DoubleSidePool.MainSideConfigStruct = { - token: tokenAddr, + it("Should configure deposited token pool limits", async function () { + const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + name: "TestDepositedPool", + depositToken: tokenAddr, + profitableToken: tokenAddr, rewardToken: tokenAddr, rewardTokenPrice: 1, - minStakeValue: 10, - unstakeLockPeriod: 24 * 60 * 60, // 24 hours + interest: 100000, // 10% + interestRate: 24 * 60 * 60, // 24 hours + }; + + await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); + + const limitsConfig: DepositedTokenPool.LimitsConfigStruct = { + minDepositValue: ethers.utils.parseEther("10"), + minStakeValue: ethers.utils.parseEther("10"), fastUnstakePenalty: 100000, // 10% + unstakeLockPeriod: 24 * 60 * 60, // 24 hours + stakeLockPeriod: 24 * 60 * 60, // 24 hours + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLimitsMultiplier: 2 * 1000000000, // 2x + }; + + await poolsManager.configureDepositedTokenPoolLimits("TestDepositedPool", limitsConfig); + + const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); + const proxyPool = new ethers.Contract(poolAddress, DepositedTokenPoolJson.abi, owner); + const configuredLimits = await proxyPool.getLimitsConfig(); + + expect(configuredLimits.minDepositValue).to.equal(limitsConfig.minDepositValue); + expect(configuredLimits.minStakeValue).to.equal(limitsConfig.minStakeValue); + expect(configuredLimits.fastUnstakePenalty).to.equal(limitsConfig.fastUnstakePenalty); + expect(configuredLimits.unstakeLockPeriod).to.equal(limitsConfig.unstakeLockPeriod); + expect(configuredLimits.stakeLockPeriod).to.equal(limitsConfig.stakeLockPeriod); + expect(configuredLimits.maxTotalStakeValue).to.equal(limitsConfig.maxTotalStakeValue); + expect(configuredLimits.maxStakePerUserValue).to.equal(limitsConfig.maxStakePerUserValue); + expect(configuredLimits.stakeLimitsMultiplier).to.equal(limitsConfig.stakeLimitsMultiplier); + }); + + it("Should activate and deactivate a deposited token pool", async function () { + const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + name: "TestDepositedPool", + depositToken: tokenAddr, + profitableToken: tokenAddr, + rewardToken: tokenAddr, + rewardTokenPrice: 1, interest: 100000, // 10% interestRate: 24 * 60 * 60, // 24 hours - lockPeriod: 24 * 60 * 60, // 24 hours }; - await poolsManager.createDoubleSidePool("TestPool", doubleSidePoolConfig); + await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); + const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); - const dependantSideConfig: DoubleSidePool.DependantSideConfigStruct = { - token: tokenAddr, + const proxyPool = new ethers.Contract(poolAddress, DepositedTokenPoolJson.abi, owner); + expect(await proxyPool.active()).to.equal(true); + await poolsManager.deactivateDoubleSidePool("TestDepositedPool"); + expect(await proxyPool.active()).to.equal(false); + await poolsManager.activateDoubleSidePool("TestDepositedPool"); + expect(await proxyPool.active()).to.equal(true); + }); + + it("Should allow updating deposited token pool parameters", async function () { + const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + name: "TestDepositedPool", + depositToken: tokenAddr, + profitableToken: tokenAddr, rewardToken: tokenAddr, rewardTokenPrice: 1, - minStakeValue: 10, - unstakeLockPeriod: 24 * 60 * 60, // 24 hours - fastUnstakePenalty: 100000, // 10% interest: 100000, // 10% interestRate: 24 * 60 * 60, // 24 hours - lockPeriod: 24 * 60 * 60, // 24 hours - maxTotalStakeValue: ethers.utils.parseEther("1000"), - maxStakePerUserValue: ethers.utils.parseEther("1000"), - stakeLockPeriod: 24 * 60 * 60, - stakeLimitsMultiplier: 24 * 60 * 60, }; - const tx = await poolsManager.addDependantSide("TestPool", dependantSideConfig); - const receipt = await tx.wait(); - const poolAddress = receipt.events![0].args![1]; - expect(await poolsManager.getDoubleSidePoolAddress("TestPool")).to.equal(poolAddress); - - const contract = new ethers.Contract(poolAddress, DoubleSidePoolJson.abi, owner); - const dependantSideConfigStruct: DoubleSidePool.DependantSideConfigStruct = await contract.getDependantSideConfig(); - expect(dependantSideConfigStruct.token).to.equal(tokenAddr); + await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); + const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); + const proxyPool = new ethers.Contract(poolAddress, DepositedTokenPoolJson.abi, owner); + + await poolsManager.setRewardTokenPriceD("TestDepositedPool", 2); + await poolsManager.setInterestD("TestDepositedPool", 200000, 48 * 60 * 60); + const updatedConfig = await proxyPool.getMainConfig(); + expect(updatedConfig.rewardTokenPrice).to.equal(2); + expect(updatedConfig.interest).to.equal(200000); + expect(updatedConfig.interestRate).to.equal(48 * 60 * 60); + + await poolsManager.setMinDepositValueD("TestDepositedPool", 20); + await poolsManager.setMinStakeValueD("TestDepositedPool", 30); + await poolsManager.setFastUnstakePenaltyD("TestDepositedPool", 200000); + await poolsManager.setUnstakeLockPeriodD("TestDepositedPool", 48 * 60 * 60); + await poolsManager.setStakeLockPeriodD("TestDepositedPool", 72 * 60 * 60); + await poolsManager.setMaxTotalStakeValueD("TestDepositedPool", ethers.utils.parseEther("2000000")); + await poolsManager.setMaxStakePerUserValueD("TestDepositedPool", ethers.utils.parseEther("200000")); + await poolsManager.setStakeLimitsMultiplierD("TestDepositedPool", 3 * 1000000000); + + const updatedLimits = await proxyPool.getLimitsConfig(); + expect(updatedLimits.minDepositValue).to.equal(20); + expect(updatedLimits.minStakeValue).to.equal(30); + expect(updatedLimits.fastUnstakePenalty).to.equal(200000); + expect(updatedLimits.unstakeLockPeriod).to.equal(48 * 60 * 60); + expect(updatedLimits.stakeLockPeriod).to.equal(72 * 60 * 60); + expect(updatedLimits.maxTotalStakeValue).to.equal(ethers.utils.parseEther("2000000")); + expect(updatedLimits.maxStakePerUserValue).to.equal(ethers.utils.parseEther("200000")); + expect(updatedLimits.stakeLimitsMultiplier).to.equal(3 * 1000000000); }); - // activate deactivate }); - }); From 695583f79bfda4be9fc1bc9bd32f81e63fb93c03 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 19 Sep 2024 10:48:37 +0300 Subject: [PATCH 42/60] Remove debug --- contracts/staking/token/DepositedTokenPool.sol | 18 ------------------ contracts/staking/token/TokenPool.sol | 3 --- contracts/staking/token/TokenPoolsManager.sol | 6 ------ test/staking/token/TokenPoolsManager.ts | 1 - 4 files changed, 28 deletions(-) diff --git a/contracts/staking/token/DepositedTokenPool.sol b/contracts/staking/token/DepositedTokenPool.sol index 0fd8a404..6a8f92a0 100644 --- a/contracts/staking/token/DepositedTokenPool.sol +++ b/contracts/staking/token/DepositedTokenPool.sol @@ -8,8 +8,6 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -import "hardhat/console.sol"; - //The side defined by the address of the token. Zero address means native coin contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { @@ -224,7 +222,6 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { info.totalStake += amount; info.totalRewards += rewardsAmount; if (stakers[msg.sender].stakedAt == 0) - console.log("Staked at: %s", block.timestamp); stakers[msg.sender].stakedAt = block.timestamp; require(stakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); @@ -237,9 +234,6 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { function unstake(uint amount) public { require(stakers[msg.sender].stake >= amount, "Not enough stake"); - console.log("StakedAt: %s", stakers[msg.sender].stakedAt); - console.log("block.timestamp: %s", block.timestamp); - console.log("stakeLockPeriod: %s", limitsConfig.stakeLockPeriod); require(block.timestamp - stakers[msg.sender].stakedAt >= limitsConfig.stakeLockPeriod, "Stake is locked"); uint rewardsAmount = _calcRewards(amount); @@ -332,8 +326,6 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { function getUserRewards(address user) public view returns (uint) { uint rewardsAmount = _calcRewards(stakers[user].stake); - console.log("Claimable rewards: %s", stakers[user].claimableRewards); - console.log("Rewards debt: %s", stakers[user].rewardsDebt); if (rewardsAmount + stakers[user].claimableRewards <= stakers[user].rewardsDebt) return 0; @@ -342,16 +334,9 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { // INTERNAL METHODS function _addInterest() internal { - console.log("Entering add interest"); if (info.lastInterestUpdate + mainConfig.interestRate > block.timestamp) return; - console.log("Adding interest"); uint timePassed = block.timestamp - info.lastInterestUpdate; - console.log("Time passed: %s", timePassed); - console.log("totalStake: %s", info.totalStake); - console.log("interest: %s", mainConfig.interest); - console.log("interestRate: %s", mainConfig.interestRate); uint newRewards = info.totalStake * mainConfig.interest * timePassed / BILLION / mainConfig.interestRate; - console.log("New rewards: %s", newRewards); info.totalRewards += newRewards; info.lastInterestUpdate = block.timestamp; @@ -388,9 +373,6 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { } function _calcRewards(uint amount) internal view returns (uint) { - console.log("Total stake: %s", info.totalStake); - console.log("Total rewards: %s", info.totalRewards); - console.log("Amount: %s", amount); if (info.totalStake == 0 && info.totalRewards == 0) return amount; return amount * info.totalRewards / info.totalStake; } diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 29a60020..3800002d 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -8,8 +8,6 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -import "hardhat/console.sol"; - contract TokenPool is Initializable, AccessControl, IOnBlockListener { struct Config { @@ -168,7 +166,6 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { } function claim() public { - console.log("claiming rewards"); _calcClaimableRewards(msg.sender); _claimRewards(msg.sender); } diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 7add30d4..be920d28 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -9,8 +9,6 @@ import "./DepositedTokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -import "hardhat/console.sol"; - contract TokenPoolsManager is AccessControl{ LockKeeper lockKeeper; RewardsBank public bank; @@ -39,12 +37,10 @@ contract TokenPoolsManager is AccessControl{ // TOKEN POOL METHODS function createTokenPool(TokenPool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { - console.log("Entered createPool"); bytes memory data = abi.encodeWithSignature( "initialize(address,address,(address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", bank, lockKeeper, params); address pool = address(new BeaconProxy(address(tokenPoolBeacon), data)); - console.log("Pool created at address: %s", pool); pools[params.name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); emit PoolCreated(params.name, pool); @@ -98,12 +94,10 @@ contract TokenPoolsManager is AccessControl{ // DEPOSITED POOL METHODS function createDeposistedTokenPool(DepositedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { - console.log("Entered createDoubleSidePool"); bytes memory data = abi.encodeWithSignature( "initialize(address,address,(string,address,address,address,uint256,uint256,uint256))", bank, lockKeeper, params); address pool = address(new BeaconProxy(address(depositedTokenPoolBeacon), data)); - console.log("DoubleSidePool created at address: %s", pool); depositedPools[params.name] = pool; bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); emit DepositedPoolCreated(params.name, pool); diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index 348aab25..b8090159 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -70,7 +70,6 @@ describe("PoolsManager", function () { const tx = await poolsManager.createTokenPool(tokenPoolConfig); const receipt = await tx.wait(); - console.log(receipt.events); const poolAddress = receipt.events![3].args![1]; expect(await poolsManager.getPoolAddress("TestPool")).to.equal(poolAddress); From 2db0b13de7954eba476bdfdafc6ad20daac7010e Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 19 Sep 2024 19:01:30 +0300 Subject: [PATCH 43/60] Fix issues --- ...itedTokenPool.sol => LimitedTokenPool.sol} | 12 +- contracts/staking/token/TokenPoolsManager.sol | 53 ++-- ...ositedTokenPool.ts => LimitedTokenPool.ts} | 252 +++++++++--------- test/staking/token/TokenPoolsManager.ts | 66 ++--- 4 files changed, 191 insertions(+), 192 deletions(-) rename contracts/staking/token/{DepositedTokenPool.sol => LimitedTokenPool.sol} (96%) rename test/staking/token/{DepositedTokenPool.ts => LimitedTokenPool.ts} (62%) diff --git a/contracts/staking/token/DepositedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol similarity index 96% rename from contracts/staking/token/DepositedTokenPool.sol rename to contracts/staking/token/LimitedTokenPool.sol index 6a8f92a0..6b3d860e 100644 --- a/contracts/staking/token/DepositedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -9,11 +9,11 @@ import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; //The side defined by the address of the token. Zero address means native coin -contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { +contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { struct MainConfig { string name; - address depositToken; + address limitsMultiplierToken; address profitableToken; address rewardToken; uint rewardTokenPrice; @@ -177,10 +177,10 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { require(active, "Pool is not active"); require(amount >= limitsConfig.minDepositValue, "Pool: deposit value is too low"); if (msg.value != 0) { - require(mainConfig.depositToken == address(0), "Pool: does not accept native coin"); + require(mainConfig.limitsMultiplierToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(mainConfig.depositToken), msg.sender, address(this), amount); + SafeERC20.safeTransferFrom(IERC20(mainConfig.limitsMultiplierToken), msg.sender, address(this), amount); } stakers[msg.sender].deposit += amount; @@ -197,10 +197,10 @@ contract DepositedTokenPool is Initializable, AccessControl, IOnBlockListener { require(stakers[msg.sender].stake <= _maxUserStakeValue(msg.sender), "Pool: user max stake value exceeded"); - if (mainConfig.depositToken == address(0)) { + if (mainConfig.limitsMultiplierToken == address(0)) { payable(msg.sender).transfer(amount); } else { - SafeERC20.safeTransfer(IERC20(mainConfig.depositToken), msg.sender, amount); + SafeERC20.safeTransfer(IERC20(mainConfig.limitsMultiplierToken), msg.sender, amount); } emit Withdrawn(msg.sender, amount); diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index be920d28..3701ed16 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -5,7 +5,7 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./TokenPool.sol"; -import "./DepositedTokenPool.sol"; +import "./LimitedTokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; @@ -35,7 +35,6 @@ contract TokenPoolsManager is AccessControl{ // OWNER METHODS // TOKEN POOL METHODS - function createTokenPool(TokenPool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { bytes memory data = abi.encodeWithSignature( "initialize(address,address,(address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", @@ -93,7 +92,7 @@ contract TokenPoolsManager is AccessControl{ } // DEPOSITED POOL METHODS - function createDeposistedTokenPool(DepositedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createLimitedTokenPool(LimitedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { bytes memory data = abi.encodeWithSignature( "initialize(address,address,(string,address,address,address,uint256,uint256,uint256))", bank, lockKeeper, params); @@ -104,83 +103,83 @@ contract TokenPoolsManager is AccessControl{ return pool; } - function configureDepositedTokenPoolLimits(string calldata name, DepositedTokenPool.LimitsConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { + function configureLimitedTokenPoolLimits(string calldata name, LimitedTokenPool.LimitsConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[name] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[name]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[name]); pool.setLimitsConfig(params); } function deactivateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.deactivate(); emit DepositedPoolDeactivated(_pool); } function activateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.activate(); emit DepositedPoolActivated(_pool); } - function setRewardTokenPriceD(string memory _pool, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setRewardTokenPriceL(string memory _pool, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setRewardTokenPrice(price); } - function setInterestD(string memory _pool, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setInterestL(string memory _pool, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setInterest(_interest, _interestRate); } - function setMinDepositValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setMinDepositValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setMinDepositValue(value); } - function setMinStakeValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setMinStakeValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setMinStakeValue(value); } - function setFastUnstakePenaltyD(string memory _pool, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setFastUnstakePenaltyL(string memory _pool, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setFastUnstakePenalty(penalty); } - function setUnstakeLockPeriodD(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setUnstakeLockPeriodL(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setUnstakeLockPeriod(period); } - function setStakeLockPeriodD(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setStakeLockPeriodL(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setStakeLockPeriod(period); } - function setMaxTotalStakeValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setMaxTotalStakeValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setMaxTotalStakeValue(value); } - function setMaxStakePerUserValueD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setMaxStakePerUserValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setMaxStakePerUserValue(value); } - function setStakeLimitsMultiplierD(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setStakeLimitsMultiplierL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); - DepositedTokenPool pool = DepositedTokenPool(depositedPools[_pool]); + LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.setStakeLimitsMultiplier(value); } diff --git a/test/staking/token/DepositedTokenPool.ts b/test/staking/token/LimitedTokenPool.ts similarity index 62% rename from test/staking/token/DepositedTokenPool.ts rename to test/staking/token/LimitedTokenPool.ts index 64c2dbff..752722f1 100644 --- a/test/staking/token/DepositedTokenPool.ts +++ b/test/staking/token/LimitedTokenPool.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { RewardsBank, AirBond, - DepositedTokenPool, + LimitedTokenPool, RewardsBank__factory, AirBond__factory, LockKeeper__factory, @@ -17,14 +17,14 @@ const BILLION = 1_000_000_000; import { expect } from "chai"; -describe("DepositedTokenPool", function () { +describe("LimitedTokenPool", function () { let owner: SignerWithAddress; let user1: SignerWithAddress; let user2: SignerWithAddress; - let depositedPool: DepositedTokenPool; + let limitedPool : LimitedTokenPool; let rewardsBank: RewardsBank; let lockKeeper: LockKeeper; - let depositToken: AirBond; + let limitsMultiplierToken: AirBond; let profitableToken: AirBond; @@ -32,15 +32,15 @@ describe("DepositedTokenPool", function () { const [owner, user1, user2] = await ethers.getSigners(); const rewardsBank = await new RewardsBank__factory(owner).deploy(); - const depositToken = await new AirBond__factory(owner).deploy(owner.address); + const limitsMultiplierToken = await new AirBond__factory(owner).deploy(owner.address); const profitableToken = await new AirBond__factory(owner).deploy(owner.address); const lockKeeper = await new LockKeeper__factory(owner).deploy(); - const depositedPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); + const limitedPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); - const mainConfig: DepositedTokenPool.MainConfigStruct = { + const mainConfig: LimitedTokenPool.MainConfigStruct = { name: "Test Deposited Pool", - depositToken: depositToken.address, + limitsMultiplierToken: limitsMultiplierToken.address, profitableToken: profitableToken.address, rewardToken: profitableToken.address, rewardTokenPrice: 1, // 1:1 ratio @@ -48,7 +48,7 @@ describe("DepositedTokenPool", function () { interestRate: D1, // 1 day }; - const limitsConfig: DepositedTokenPool.LimitsConfigStruct = { + const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { minDepositValue: 10, minStakeValue: 10, fastUnstakePenalty: 0.10 * BILLION, // 10% @@ -59,40 +59,40 @@ describe("DepositedTokenPool", function () { stakeLimitsMultiplier: 2 * BILLION, // 2x }; - const depositedPool = (await upgrades.deployProxy(depositedPoolFactory, [ + const limitedPool = (await upgrades.deployProxy(limitedPoolFactory, [ rewardsBank.address, lockKeeper.address, mainConfig - ])) as DepositedTokenPool; + ])) as LimitedTokenPool; - await depositedPool.setLimitsConfig(limitsConfig); + await limitedPool.setLimitsConfig(limitsConfig); - await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), depositedPool.address); - await depositToken.grantRole(await depositToken.MINTER_ROLE(), owner.address); + await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), limitedPool.address); + await limitsMultiplierToken.grantRole(await limitsMultiplierToken.MINTER_ROLE(), owner.address); await profitableToken.grantRole(await profitableToken.MINTER_ROLE(), owner.address); // Mint tokens for testing const mintAmount = ethers.utils.parseEther("1000000"); - await depositToken.mint(owner.address, mintAmount); + await limitsMultiplierToken.mint(owner.address, mintAmount); await profitableToken.mint(owner.address, mintAmount); await profitableToken.mint(rewardsBank.address, mintAmount); // Approve tokens for testing - await depositToken.approve(depositedPool.address, mintAmount); - await profitableToken.approve(depositedPool.address, mintAmount); + await limitsMultiplierToken.approve(limitedPool.address, mintAmount); + await profitableToken.approve(limitedPool.address, mintAmount); - return { owner, user1, user2, depositedPool, rewardsBank, lockKeeper, depositToken, profitableToken}; + return { owner, user1, user2, limitedPool, rewardsBank, lockKeeper, limitsMultiplierToken, profitableToken}; } beforeEach(async function () { - ({ owner, user1, user2, depositedPool, rewardsBank, lockKeeper, depositToken, profitableToken } = await loadFixture(deploy)); + ({ owner, user1, user2, limitedPool, rewardsBank, lockKeeper, limitsMultiplierToken, profitableToken } = await loadFixture(deploy)); }); describe("Initialization", function () { it("Should initialize with correct main config", async function () { - const config = await depositedPool.getMainConfig(); + const config = await limitedPool.getMainConfig(); expect(config.name).to.equal("Test Deposited Pool"); - expect(config.depositToken).to.equal(depositToken.address); + expect(config.limitsMultiplierToken).to.equal(limitsMultiplierToken.address); expect(config.profitableToken).to.equal(profitableToken.address); expect(config.rewardToken).to.equal(profitableToken.address); expect(config.rewardTokenPrice).to.equal(1); @@ -101,7 +101,7 @@ describe("DepositedTokenPool", function () { }); it("Should initialize with correct limits config", async function () { - const limits = await depositedPool.getLimitsConfig(); + const limits = await limitedPool.getLimitsConfig(); expect(limits.minDepositValue).to.equal(10); expect(limits.minStakeValue).to.equal(10); expect(limits.fastUnstakePenalty).to.equal(0.10 * BILLION); @@ -116,49 +116,49 @@ describe("DepositedTokenPool", function () { describe("Deposit", function () { it("Should allow deposit", async function () { const depositAmount = ethers.utils.parseEther("100"); - await expect(depositedPool.deposit(depositAmount)) - .to.emit(depositedPool, "Deposited") + await expect(limitedPool.deposit(depositAmount)) + .to.emit(limitedPool, "Deposited") .withArgs(owner.address, depositAmount); - const info = await depositedPool.getInfo(); + const info = await limitedPool.getInfo(); expect(info.totalDeposit).to.equal(depositAmount); - const staker = await depositedPool.getStaker(owner.address); + const staker = await limitedPool.getStaker(owner.address); expect(staker.deposit).to.equal(depositAmount); }); it("Should not allow deposit below minimum", async function () { const depositAmount = 1; - await expect(depositedPool.deposit(depositAmount)).to.be.revertedWith("Pool: deposit value is too low"); + await expect(limitedPool.deposit(depositAmount)).to.be.revertedWith("Pool: deposit value is too low"); }); }); describe("Withdrawal", function () { beforeEach(async function () { - await depositedPool.deposit(ethers.utils.parseEther("1000")); + await limitedPool.deposit(ethers.utils.parseEther("1000")); }); it("Should allow withdrawal", async function () { const withdrawAmount = ethers.utils.parseEther("500"); - await expect(depositedPool.withdraw(withdrawAmount)) - .to.emit(depositedPool, "Withdrawn") + await expect(limitedPool.withdraw(withdrawAmount)) + .to.emit(limitedPool, "Withdrawn") .withArgs(owner.address, withdrawAmount); - const info = await depositedPool.getInfo(); + const info = await limitedPool.getInfo(); expect(info.totalDeposit).to.equal(ethers.utils.parseEther("500")); - const staker = await depositedPool.getStaker(owner.address); + const staker = await limitedPool.getStaker(owner.address); expect(staker.deposit).to.equal(ethers.utils.parseEther("500")); }); it("Should not allow withdrawal more than deposited", async function () { const withdrawAmount = ethers.utils.parseEther("1001"); - await expect(depositedPool.withdraw(withdrawAmount)).to.be.revertedWith("Not enough deposit"); + await expect(limitedPool.withdraw(withdrawAmount)).to.be.revertedWith("Not enough deposit"); }); it("Should not allow withdrawal that would violate stake limits", async function () { - await depositedPool.stake(ethers.utils.parseEther("500")); - await expect(depositedPool.withdraw(ethers.utils.parseEther("751"))) + await limitedPool.stake(ethers.utils.parseEther("500")); + await expect(limitedPool.withdraw(ethers.utils.parseEther("751"))) .to.be.revertedWith("Pool: user max stake value exceeded"); }); }); @@ -166,36 +166,36 @@ describe("DepositedTokenPool", function () { describe("Stake", function () { beforeEach(async function () { // Deposit before staking - await depositedPool.deposit(ethers.utils.parseEther("1000")); + await limitedPool.deposit(ethers.utils.parseEther("1000")); }); it("Should allow staking", async function () { const stakeAmount = ethers.utils.parseEther("100"); - await expect(depositedPool.stake(stakeAmount)) - .to.emit(depositedPool, "Staked") + await expect(limitedPool.stake(stakeAmount)) + .to.emit(limitedPool, "Staked") .withArgs(owner.address, stakeAmount); - const info = await depositedPool.getInfo(); + const info = await limitedPool.getInfo(); expect(info.totalStake).to.equal(stakeAmount); - const staker = await depositedPool.getStaker(owner.address); + const staker = await limitedPool.getStaker(owner.address); expect(staker.stake).to.equal(stakeAmount); }); it("Should not allow staking below minimum", async function () { const stakeAmount = 1; - await expect(depositedPool.stake(stakeAmount)).to.be.revertedWith("Pool: stake value is too low"); + await expect(limitedPool.stake(stakeAmount)).to.be.revertedWith("Pool: stake value is too low"); }); it("Should not allow staking above user limit", async function () { const stakeAmount = ethers.utils.parseEther("2001"); - await expect(depositedPool.stake(stakeAmount)).to.be.revertedWith("Pool: user max stake value exceeded"); + await expect(limitedPool.stake(stakeAmount)).to.be.revertedWith("Pool: user max stake value exceeded"); }); it("Should not allow staking above total pool limit", async function () { - await depositedPool.setMaxTotalStakeValue(ethers.utils.parseEther("100")); + await limitedPool.setMaxTotalStakeValue(ethers.utils.parseEther("100")); const stakeAmount = ethers.utils.parseEther("101"); - await expect(depositedPool.stake(stakeAmount)).to.be.revertedWith("Pool: max stake value exceeded"); + await expect(limitedPool.stake(stakeAmount)).to.be.revertedWith("Pool: max stake value exceeded"); }); }); @@ -203,31 +203,31 @@ describe("DepositedTokenPool", function () { const stakeAmount = ethers.utils.parseEther("100"); beforeEach(async function () { - await depositedPool.deposit(ethers.utils.parseEther("1000")); - await depositedPool.stake(stakeAmount); + await limitedPool.deposit(ethers.utils.parseEther("1000")); + await limitedPool.stake(stakeAmount); await time.increase(D1); }); it("Should allow unstaking", async function () { - await expect(depositedPool.unstake(stakeAmount)) + await expect(limitedPool.unstake(stakeAmount)) .to.emit(lockKeeper, "Locked"); - const info = await depositedPool.getInfo(); + const info = await limitedPool.getInfo(); expect(info.totalStake).to.equal(0); - const staker = await depositedPool.getStaker(owner.address); + const staker = await limitedPool.getStaker(owner.address); expect(staker.stake).to.equal(0); }); it("Should not allow unstaking more than staked", async function () { - await expect(depositedPool.unstake(stakeAmount.add(1))).to.be.revertedWith("Not enough stake"); + await expect(limitedPool.unstake(stakeAmount.add(1))).to.be.revertedWith("Not enough stake"); }); it("Should not allow unstaking before stake lock period", async function () { - await depositedPool.setStakeLockPeriod(D1 * 2); - await depositedPool.stake(stakeAmount); - await time.increase(D1); - await expect(depositedPool.unstake(stakeAmount)).to.be.revertedWith("Stake is locked"); + await limitedPool.setStakeLockPeriod(D1 * 2); + await limitedPool.stake(stakeAmount); + await time.increase(D1 / 2); + await expect(limitedPool.unstake(stakeAmount)).to.be.revertedWith("Stake is locked"); }); }); @@ -235,20 +235,20 @@ describe("DepositedTokenPool", function () { const stakeAmount = ethers.utils.parseEther("100"); beforeEach(async function () { - await depositedPool.deposit(ethers.utils.parseEther("1000")); - await depositedPool.stake(stakeAmount); + await limitedPool.deposit(ethers.utils.parseEther("1000")); + await limitedPool.stake(stakeAmount); await time.increase(D1); }); it("Should allow fast unstaking with penalty", async function () { const balanceBefore = await profitableToken.balanceOf(owner.address); - await depositedPool.unstakeFast(stakeAmount); + await limitedPool.unstakeFast(stakeAmount); const balanceAfter = await profitableToken.balanceOf(owner.address); const expectedReturn = stakeAmount.mul(90).div(100); // 90% due to 10% penalty expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReturn); - const info = await depositedPool.getInfo(); + const info = await limitedPool.getInfo(); expect(info.totalStake).to.equal(0); }); }); @@ -257,25 +257,25 @@ describe("DepositedTokenPool", function () { const stakeAmount = 1000; beforeEach(async function () { - await depositedPool.deposit(ethers.utils.parseEther("2000")); - await depositedPool.stake(stakeAmount); + await limitedPool.deposit(ethers.utils.parseEther("2000")); + await limitedPool.stake(stakeAmount); }); it("Should calculate rewards correctly", async function () { await time.increase(D1); - await depositedPool.onBlock(); + await limitedPool.onBlock(); - const rewards = await depositedPool.getUserRewards(owner.address); + const rewards = await limitedPool.getUserRewards(owner.address); const expectedRewards = stakeAmount * 0.1; // 10% interest expect(rewards).to.equal(expectedRewards); }); it("Should allow claiming rewards", async function () { await time.increase(D1); - await depositedPool.onBlock(); + await limitedPool.onBlock(); const balanceBefore = await profitableToken.balanceOf(owner.address); - await depositedPool.claim(); + await limitedPool.claim(); const balanceAfter = await profitableToken.balanceOf(owner.address); const expectedRewards = stakeAmount * 0.1; // 10% interest @@ -285,77 +285,77 @@ describe("DepositedTokenPool", function () { describe("Edge cases", function () { it("Should handle multiple deposits and stakes correctly", async function () { - await depositedPool.setStakeLockPeriod(0); // No lock period + await limitedPool.setStakeLockPeriod(0); // No lock period const depositAmount = ethers.utils.parseEther("1000"); const stakeAmount = ethers.utils.parseEther("500"); - await depositedPool.deposit(depositAmount); - await depositedPool.stake(stakeAmount); - await depositedPool.deposit(depositAmount); - await depositedPool.stake(stakeAmount); + await limitedPool.deposit(depositAmount); + await limitedPool.stake(stakeAmount); + await limitedPool.deposit(depositAmount); + await limitedPool.stake(stakeAmount); - const info = await depositedPool.getInfo(); + const info = await limitedPool.getInfo(); expect(info.totalDeposit).to.equal(depositAmount.mul(2)); expect(info.totalStake).to.equal(stakeAmount.mul(2)); - const staker = await depositedPool.getStaker(owner.address); + const staker = await limitedPool.getStaker(owner.address); expect(staker.deposit).to.equal(depositAmount.mul(2)); expect(staker.stake).to.equal(stakeAmount.mul(2)); }); it("Should handle rewards correctly after multiple stakes and unstakes", async function () { - await depositedPool.setStakeLockPeriod(0); // No lock period + await limitedPool.setStakeLockPeriod(0); // No lock period const depositAmount = ethers.utils.parseEther("2000"); const stakeAmount = ethers.utils.parseEther("500"); - await depositedPool.deposit(depositAmount); - await depositedPool.stake(stakeAmount); + await limitedPool.deposit(depositAmount); + await limitedPool.stake(stakeAmount); await time.increase(D1 / 2); - await depositedPool.stake(stakeAmount); + await limitedPool.stake(stakeAmount); await time.increase(D1 / 2); - await depositedPool.unstake(stakeAmount); + await limitedPool.unstake(stakeAmount); - await depositedPool.onBlock(); + await limitedPool.onBlock(); - const rewards = await depositedPool.getUserRewards(owner.address); + const rewards = await limitedPool.getUserRewards(owner.address); // Expected rewards calculation is complex due to different staking times expect(rewards).to.be.gt(0); }); it("Should not allow actions when pool is deactivated", async function () { - await depositedPool.deactivate(); + await limitedPool.deactivate(); - await expect(depositedPool.deposit(ethers.utils.parseEther("100"))).to.be.revertedWith("Pool is not active"); - await expect(depositedPool.stake(ethers.utils.parseEther("100"))).to.be.revertedWith("Pool is not active"); + await expect(limitedPool.deposit(ethers.utils.parseEther("100"))).to.be.revertedWith("Pool is not active"); + await expect(limitedPool.stake(ethers.utils.parseEther("100"))).to.be.revertedWith("Pool is not active"); }); }); describe("Interest calculation", function () { beforeEach(async function () { - await depositedPool.deposit(ethers.utils.parseEther("1000")); - await depositedPool.stake(500); + await limitedPool.deposit(ethers.utils.parseEther("1000")); + await limitedPool.stake(500); }); it("Should calculate interest correctly over multiple periods", async function () { for (let i = 0; i < 5; i++) { await time.increase(D1); - await depositedPool.onBlock(); + await limitedPool.onBlock(); } - const rewards = await depositedPool.getUserRewards(owner.address); + const rewards = await limitedPool.getUserRewards(owner.address); //const expectedRewards = ethers.utils.parseEther("500").mul(10).div(100).mul(5); // 10% interest for 5 days const expectedRewards = 500 * 0.1 * 5; // 10% interest for 5 days expect(rewards).to.be.closeTo(expectedRewards, "100"); // Allow small rounding error }); it("Should not add interest if called too frequently", async function () { - await depositedPool.onBlock(); - const infoBefore = await depositedPool.getInfo(); + await limitedPool.onBlock(); + const infoBefore = await limitedPool.getInfo(); await time.increase(D1 / 2); // Half a day - await depositedPool.onBlock(); - const infoAfter = await depositedPool.getInfo(); + await limitedPool.onBlock(); + const infoAfter = await limitedPool.getInfo(); expect(infoAfter.totalRewards).to.equal(infoBefore.totalRewards); }); @@ -364,47 +364,47 @@ describe("DepositedTokenPool", function () { describe("Multiple users", function () { beforeEach(async function () { // Setup for multiple users - await depositToken.transfer(user1.address, ethers.utils.parseEther("1000")); - await depositToken.transfer(user2.address, ethers.utils.parseEther("1000")); + await limitsMultiplierToken.transfer(user1.address, ethers.utils.parseEther("1000")); + await limitsMultiplierToken.transfer(user2.address, ethers.utils.parseEther("1000")); await profitableToken.transfer(user1.address, ethers.utils.parseEther("1000")); await profitableToken.transfer(user2.address, ethers.utils.parseEther("1000")); - await depositToken.connect(user1).approve(depositedPool.address, ethers.utils.parseEther("1000")); - await depositToken.connect(user2).approve(depositedPool.address, ethers.utils.parseEther("1000")); - await profitableToken.connect(user1).approve(depositedPool.address, ethers.utils.parseEther("1000")); - await profitableToken.connect(user2).approve(depositedPool.address, ethers.utils.parseEther("1000")); + await limitsMultiplierToken.connect(user1).approve(limitedPool.address, ethers.utils.parseEther("1000")); + await limitsMultiplierToken.connect(user2).approve(limitedPool.address, ethers.utils.parseEther("1000")); + await profitableToken.connect(user1).approve(limitedPool.address, ethers.utils.parseEther("1000")); + await profitableToken.connect(user2).approve(limitedPool.address, ethers.utils.parseEther("1000")); }); it("Should handle deposits and stakes from multiple users correctly", async function () { - await depositedPool.connect(user1).deposit(ethers.utils.parseEther("500")); - await depositedPool.connect(user2).deposit(ethers.utils.parseEther("300")); - await depositedPool.connect(user1).stake(ethers.utils.parseEther("200")); - await depositedPool.connect(user2).stake(ethers.utils.parseEther("100")); + await limitedPool.connect(user1).deposit(ethers.utils.parseEther("500")); + await limitedPool.connect(user2).deposit(ethers.utils.parseEther("300")); + await limitedPool.connect(user1).stake(ethers.utils.parseEther("200")); + await limitedPool.connect(user2).stake(ethers.utils.parseEther("100")); - const infoUser1 = await depositedPool.getStaker(user1.address); - const infoUser2 = await depositedPool.getStaker(user2.address); + const infoUser1 = await limitedPool.getStaker(user1.address); + const infoUser2 = await limitedPool.getStaker(user2.address); expect(infoUser1.deposit).to.equal(ethers.utils.parseEther("500")); expect(infoUser1.stake).to.equal(ethers.utils.parseEther("200")); expect(infoUser2.deposit).to.equal(ethers.utils.parseEther("300")); expect(infoUser2.stake).to.equal(ethers.utils.parseEther("100")); - const poolInfo = await depositedPool.getInfo(); + const poolInfo = await limitedPool.getInfo(); expect(poolInfo.totalDeposit).to.equal(ethers.utils.parseEther("800")); expect(poolInfo.totalStake).to.equal(ethers.utils.parseEther("300")); }); it("Should distribute rewards correctly among multiple users", async function () { - await depositedPool.connect(user1).deposit(ethers.utils.parseEther("500")); - await depositedPool.connect(user2).deposit(ethers.utils.parseEther("500")); - await depositedPool.connect(user1).stake(ethers.utils.parseEther("300")); - await depositedPool.connect(user2).stake(ethers.utils.parseEther("200")); + await limitedPool.connect(user1).deposit(ethers.utils.parseEther("500")); + await limitedPool.connect(user2).deposit(ethers.utils.parseEther("500")); + await limitedPool.connect(user1).stake(ethers.utils.parseEther("300")); + await limitedPool.connect(user2).stake(ethers.utils.parseEther("200")); await time.increase(D1); - await depositedPool.onBlock(); + await limitedPool.onBlock(); - const rewardsUser1 = await depositedPool.getUserRewards(user1.address); - const rewardsUser2 = await depositedPool.getUserRewards(user2.address); + const rewardsUser1 = await limitedPool.getUserRewards(user1.address); + const rewardsUser2 = await limitedPool.getUserRewards(user2.address); // User1 should have 60% of the rewards, User2 40% expect(rewardsUser1).to.be.closeTo( @@ -420,9 +420,9 @@ describe("DepositedTokenPool", function () { describe("Safety checks", function () { it("Should not allow initialization twice", async function () { - const mainConfig: DepositedTokenPool.MainConfigStruct = { + const mainConfig: LimitedTokenPool.MainConfigStruct = { name: "Test Deposited Pool", - depositToken: depositToken.address, + limitsMultiplierToken: limitsMultiplierToken.address, profitableToken: profitableToken.address, rewardToken: profitableToken.address, rewardTokenPrice: BILLION, @@ -430,28 +430,28 @@ describe("DepositedTokenPool", function () { interestRate: D1, }; - await expect(depositedPool.initialize(rewardsBank.address, lockKeeper.address, mainConfig)) + await expect(limitedPool.initialize(rewardsBank.address, lockKeeper.address, mainConfig)) .to.be.revertedWith("Initializable: contract is already initialized"); }); it("Should only allow admin to change configurations", async function () { - await expect(depositedPool.connect(user1).setRewardTokenPrice(BILLION * 2)) + await expect(limitedPool.connect(user1).setRewardTokenPrice(BILLION * 2)) .to.be.revertedWith("AccessControl: account " + user1.address.toLowerCase() + " is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"); }); it("Should handle zero stakes and deposits correctly", async function () { - await expect(depositedPool.stake(0)).to.be.revertedWith("Pool: stake value is too low"); - await expect(depositedPool.deposit(0)).to.be.revertedWith("Pool: deposit value is too low"); + await expect(limitedPool.stake(0)).to.be.revertedWith("Pool: stake value is too low"); + await expect(limitedPool.deposit(0)).to.be.revertedWith("Pool: deposit value is too low"); }); }); describe("Native token handling", function () { it("Should handle native token deposits if configured", async function () { // Deploy a new pool with ETH as deposit token - const depositedPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); - const mainConfig: DepositedTokenPool.MainConfigStruct = { + const limitedPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); + const mainConfig: LimitedTokenPool.MainConfigStruct = { name: "ETH Deposited Pool", - depositToken: ethers.constants.AddressZero, // ETH + limitsMultiplierToken: ethers.constants.AddressZero, // ETH profitableToken: profitableToken.address, rewardToken: profitableToken.address, rewardTokenPrice: BILLION, @@ -459,11 +459,11 @@ describe("DepositedTokenPool", function () { interestRate: D1, }; - const ethPool = (await upgrades.deployProxy(depositedPoolFactory, [ + const ethPool = (await upgrades.deployProxy(limitedPoolFactory, [ rewardsBank.address, lockKeeper.address, mainConfig - ])) as DepositedTokenPool; + ])) as LimitedTokenPool; await ethPool.setLimitsConfig({ minDepositValue: ethers.utils.parseEther("0.1"), @@ -487,10 +487,10 @@ describe("DepositedTokenPool", function () { it("Should handle native token stakes if configured", async function () { // Deploy a new pool with ETH as profitable token - const depositedPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); - const mainConfig: DepositedTokenPool.MainConfigStruct = { + const limitedPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); + const mainConfig: LimitedTokenPool.MainConfigStruct = { name: "ETH Stake Pool", - depositToken: depositToken.address, + limitsMultiplierToken: limitsMultiplierToken.address, profitableToken: ethers.constants.AddressZero, // ETH rewardToken: profitableToken.address, rewardTokenPrice: BILLION, @@ -498,11 +498,11 @@ describe("DepositedTokenPool", function () { interestRate: D1, }; - const ethPool = (await upgrades.deployProxy(depositedPoolFactory, [ + const ethPool = (await upgrades.deployProxy(limitedPoolFactory, [ rewardsBank.address, lockKeeper.address, mainConfig - ])) as DepositedTokenPool; + ])) as LimitedTokenPool; await ethPool.setLimitsConfig({ minDepositValue: ethers.utils.parseEther("0.1"), @@ -516,7 +516,7 @@ describe("DepositedTokenPool", function () { }); // First deposit some tokens - await depositToken.approve(ethPool.address, ethers.utils.parseEther("10")); + await limitsMultiplierToken.approve(ethPool.address, ethers.utils.parseEther("10")); await ethPool.deposit(ethers.utils.parseEther("10")); const stakeAmount = ethers.utils.parseEther("1"); diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index b8090159..fde647ac 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -7,7 +7,7 @@ import { RewardsBank, AirBond__factory, TokenPool, - DepositedTokenPool, + LimitedTokenPool, RewardsBank__factory, TokenPoolsManager__factory, LockKeeper__factory, @@ -15,7 +15,7 @@ import { } from "../../../typechain-types"; import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; -import DepositedTokenPoolJson from "../../../artifacts/contracts/staking/token/DepositedTokenPool.sol/DepositedTokenPool.json"; +import LimitedTokenPoolJson from "../../../artifacts/contracts/staking/token/LimitedTokenPool.sol/LimitedTokenPool.json"; import { expect } from "chai"; @@ -36,13 +36,13 @@ describe("PoolsManager", function () { const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); - const depositedTokenPoolFactory = await ethers.getContractFactory("DepositedTokenPool"); - const depositedTokenPoolBeacon = await upgrades.deployBeacon(depositedTokenPoolFactory); + const limitedTokenPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); + const limitedTokenPoolBeacon = await upgrades.deployBeacon(limitedTokenPoolFactory); const lockKeeper = await new LockKeeper__factory(owner).deploy(); const poolsManager = await new TokenPoolsManager__factory(owner) - .deploy(rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address, depositedTokenPoolBeacon.address); + .deploy(rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address, limitedTokenPoolBeacon.address); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); const tokenAddr = airBond.address; @@ -132,11 +132,11 @@ describe("PoolsManager", function () { }); }); - describe("DepositedTokenPool Management", function () { + describe("LimitedTokenPool Management", function () { it("Should allow the owner to create a deposited token pool", async function () { - const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { name: "TestDepositedPool", - depositToken: tokenAddr, + limitsMultiplierToken: tokenAddr, profitableToken: tokenAddr, rewardToken: tokenAddr, rewardTokenPrice: 1, @@ -144,7 +144,7 @@ describe("PoolsManager", function () { interestRate: 24 * 60 * 60, // 24 hours }; - const tx = await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); + const tx = await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); const receipt = await tx.wait(); const poolAddress = receipt.events![3].args![1]; @@ -152,9 +152,9 @@ describe("PoolsManager", function () { }); it("Should configure deposited token pool limits", async function () { - const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { name: "TestDepositedPool", - depositToken: tokenAddr, + limitsMultiplierToken: tokenAddr, profitableToken: tokenAddr, rewardToken: tokenAddr, rewardTokenPrice: 1, @@ -162,9 +162,9 @@ describe("PoolsManager", function () { interestRate: 24 * 60 * 60, // 24 hours }; - await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); + await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); - const limitsConfig: DepositedTokenPool.LimitsConfigStruct = { + const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { minDepositValue: ethers.utils.parseEther("10"), minStakeValue: ethers.utils.parseEther("10"), fastUnstakePenalty: 100000, // 10% @@ -175,10 +175,10 @@ describe("PoolsManager", function () { stakeLimitsMultiplier: 2 * 1000000000, // 2x }; - await poolsManager.configureDepositedTokenPoolLimits("TestDepositedPool", limitsConfig); + await poolsManager.configureLimitedTokenPoolLimits("TestDepositedPool", limitsConfig); const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); - const proxyPool = new ethers.Contract(poolAddress, DepositedTokenPoolJson.abi, owner); + const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); const configuredLimits = await proxyPool.getLimitsConfig(); expect(configuredLimits.minDepositValue).to.equal(limitsConfig.minDepositValue); @@ -192,9 +192,9 @@ describe("PoolsManager", function () { }); it("Should activate and deactivate a deposited token pool", async function () { - const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { name: "TestDepositedPool", - depositToken: tokenAddr, + limitsMultiplierToken: tokenAddr, profitableToken: tokenAddr, rewardToken: tokenAddr, rewardTokenPrice: 1, @@ -202,10 +202,10 @@ describe("PoolsManager", function () { interestRate: 24 * 60 * 60, // 24 hours }; - await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); + await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); - const proxyPool = new ethers.Contract(poolAddress, DepositedTokenPoolJson.abi, owner); + const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); expect(await proxyPool.active()).to.equal(true); await poolsManager.deactivateDoubleSidePool("TestDepositedPool"); expect(await proxyPool.active()).to.equal(false); @@ -214,9 +214,9 @@ describe("PoolsManager", function () { }); it("Should allow updating deposited token pool parameters", async function () { - const depositedTokenPoolConfig: DepositedTokenPool.MainConfigStruct = { + const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { name: "TestDepositedPool", - depositToken: tokenAddr, + limitsMultiplierToken: tokenAddr, profitableToken: tokenAddr, rewardToken: tokenAddr, rewardTokenPrice: 1, @@ -224,25 +224,25 @@ describe("PoolsManager", function () { interestRate: 24 * 60 * 60, // 24 hours }; - await poolsManager.createDeposistedTokenPool(depositedTokenPoolConfig); + await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); - const proxyPool = new ethers.Contract(poolAddress, DepositedTokenPoolJson.abi, owner); + const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); - await poolsManager.setRewardTokenPriceD("TestDepositedPool", 2); - await poolsManager.setInterestD("TestDepositedPool", 200000, 48 * 60 * 60); + await poolsManager.setRewardTokenPriceL("TestDepositedPool", 2); + await poolsManager.setInterestL("TestDepositedPool", 200000, 48 * 60 * 60); const updatedConfig = await proxyPool.getMainConfig(); expect(updatedConfig.rewardTokenPrice).to.equal(2); expect(updatedConfig.interest).to.equal(200000); expect(updatedConfig.interestRate).to.equal(48 * 60 * 60); - await poolsManager.setMinDepositValueD("TestDepositedPool", 20); - await poolsManager.setMinStakeValueD("TestDepositedPool", 30); - await poolsManager.setFastUnstakePenaltyD("TestDepositedPool", 200000); - await poolsManager.setUnstakeLockPeriodD("TestDepositedPool", 48 * 60 * 60); - await poolsManager.setStakeLockPeriodD("TestDepositedPool", 72 * 60 * 60); - await poolsManager.setMaxTotalStakeValueD("TestDepositedPool", ethers.utils.parseEther("2000000")); - await poolsManager.setMaxStakePerUserValueD("TestDepositedPool", ethers.utils.parseEther("200000")); - await poolsManager.setStakeLimitsMultiplierD("TestDepositedPool", 3 * 1000000000); + await poolsManager.setMinDepositValueL("TestDepositedPool", 20); + await poolsManager.setMinStakeValueL("TestDepositedPool", 30); + await poolsManager.setFastUnstakePenaltyL("TestDepositedPool", 200000); + await poolsManager.setUnstakeLockPeriodL("TestDepositedPool", 48 * 60 * 60); + await poolsManager.setStakeLockPeriodL("TestDepositedPool", 72 * 60 * 60); + await poolsManager.setMaxTotalStakeValueL("TestDepositedPool", ethers.utils.parseEther("2000000")); + await poolsManager.setMaxStakePerUserValueL("TestDepositedPool", ethers.utils.parseEther("200000")); + await poolsManager.setStakeLimitsMultiplierL("TestDepositedPool", 3 * 1000000000); const updatedLimits = await proxyPool.getLimitsConfig(); expect(updatedLimits.minDepositValue).to.equal(20); From 11788734b4c5b1dd7846813a25f2ce4fcf41c921 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 19 Sep 2024 19:06:16 +0300 Subject: [PATCH 44/60] Update safe erc20 usage --- contracts/staking/token/LimitedTokenPool.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index 6b3d860e..2978f1bc 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -10,6 +10,7 @@ import "../../LockKeeper.sol"; //The side defined by the address of the token. Zero address means native coin contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { + using SafeERC20 for IERC20; struct MainConfig { string name; @@ -180,7 +181,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { require(mainConfig.limitsMultiplierToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(mainConfig.limitsMultiplierToken), msg.sender, address(this), amount); + IERC20(mainConfig.limitsMultiplierToken).safeTransferFrom(msg.sender, address(this), amount); } stakers[msg.sender].deposit += amount; @@ -200,7 +201,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { if (mainConfig.limitsMultiplierToken == address(0)) { payable(msg.sender).transfer(amount); } else { - SafeERC20.safeTransfer(IERC20(mainConfig.limitsMultiplierToken), msg.sender, amount); + IERC20(mainConfig.limitsMultiplierToken).safeTransfer(msg.sender, amount); } emit Withdrawn(msg.sender, amount); @@ -213,7 +214,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { require(mainConfig.profitableToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { - SafeERC20.safeTransferFrom(IERC20(mainConfig.profitableToken), msg.sender, address(this), amount); + IERC20(mainConfig.profitableToken).safeTransferFrom(msg.sender, address(this), amount); } uint rewardsAmount = _calcRewards(amount); @@ -288,7 +289,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { if (mainConfig.profitableToken == address(0)) { payable(msg.sender).transfer(amount - penalty); } else { - SafeERC20.safeTransfer(IERC20(mainConfig.profitableToken), msg.sender, amount - penalty); + IERC20(mainConfig.profitableToken).safeTransfer(msg.sender, amount - penalty); } _claimRewards(msg.sender); } From 29761c69274d8e5c743767548113047bb31634c3 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 23 Sep 2024 13:23:28 +0300 Subject: [PATCH 45/60] Add HBR token --- contracts/staking/token/HBRToken.sol | 21 ++++++++++++ deployments/22040.json | 34 +++++++++++++++++++ scripts/ecosystem/token_staking/deploy_hbr.ts | 31 +++++++++++++++++ src/contracts/names.ts | 1 + 4 files changed, 87 insertions(+) create mode 100644 contracts/staking/token/HBRToken.sol create mode 100644 scripts/ecosystem/token_staking/deploy_hbr.ts diff --git a/contracts/staking/token/HBRToken.sol b/contracts/staking/token/HBRToken.sol new file mode 100644 index 00000000..8a9ab6a7 --- /dev/null +++ b/contracts/staking/token/HBRToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract HBRToken is ERC20, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); // can use mint / burn methods + + constructor(address admin) ERC20("Harbor", "HBR") { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + function mint(address account, uint256 amount) external onlyRole(MINTER_ROLE) { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external onlyRole(MINTER_ROLE) { + _burn(account, amount); + } +} diff --git a/deployments/22040.json b/deployments/22040.json index 92ae099b..3e95460e 100644 --- a/deployments/22040.json +++ b/deployments/22040.json @@ -1662,5 +1662,39 @@ "implementation": "0x7184DD655aacCCb28376Fa1B59e3712566Df670b", "fullyQualifiedName": "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy" } + }, + "Ecosystem_HRBToken": { + "address": "0xCB856833c615ef09Ba779D35D663b849aD23C957", + "abi": [ + "constructor(address admin)", + "event Approval(address indexed owner, address indexed spender, uint256 value)", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "event Transfer(address indexed from, address indexed to, uint256 value)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function MINTER_ROLE() view returns (bytes32)", + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", + "function balanceOf(address account) view returns (uint256)", + "function burn(address account, uint256 amount)", + "function decimals() view returns (uint8)", + "function decreaseAllowance(address spender, uint256 subtractedValue) returns (bool)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function grantRole(bytes32 role, address account)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function increaseAllowance(address spender, uint256 addedValue) returns (bool)", + "function mint(address account, uint256 amount)", + "function name() view returns (string)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function symbol() view returns (string)", + "function totalSupply() view returns (uint256)", + "function transfer(address to, uint256 amount) returns (bool)", + "function transferFrom(address from, address to, uint256 amount) returns (bool)" + ], + "deployTx": "0x1c7a3ff3e00b95e2e01212a0078613ee807a28591b9a2024060e6c6d68fadfe9", + "fullyQualifiedName": "contracts/staking/token/HBRToken.sol:HBRToken" } } \ No newline at end of file diff --git a/scripts/ecosystem/token_staking/deploy_hbr.ts b/scripts/ecosystem/token_staking/deploy_hbr.ts new file mode 100644 index 00000000..370504a9 --- /dev/null +++ b/scripts/ecosystem/token_staking/deploy_hbr.ts @@ -0,0 +1,31 @@ +import { ethers } from "hardhat"; +import { ContractNames } from "../../../src"; +import { deploy } from "@airdao/deployments/deploying"; +import { HBRToken__factory } from "../../../typechain-types"; + +async function main() { + const {chainId} = await ethers.provider.getNetwork(); + const [deployer] = await ethers.getSigners(); + + if (chainId == 16718) { + return; + } + + const airBond = await deploy({ + contractName: ContractNames.Ecosystem_HRBToken, + artifactName: "HBRToken", + deployArgs: [deployer.address], + signer: deployer, + loadIfAlreadyDeployed: true, + }); + + await airBond.grantRole(await airBond.DEFAULT_ADMIN_ROLE(), deployer.address); // + await airBond.grantRole(await airBond.MINTER_ROLE(), deployer.address); +} + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/src/contracts/names.ts b/src/contracts/names.ts index acd65476..d2c5d536 100644 --- a/src/contracts/names.ts +++ b/src/contracts/names.ts @@ -93,6 +93,7 @@ export enum ContractNames { Ecosystem_TokenPoolsManager = "Ecosystem_TokenPoolsManager", Ecosystem_TokenPoolsManagerMultisig = "Ecosystem_TokenPoolsManager_Multisig", Ecosystem_TokenPoolsManagerRewardsBank = "Ecosystem_TokenPoolsManager_RewardsBank", + Ecosystem_HRBToken = "Ecosystem_HRBToken", } export const MULTISIGS_COMMON = { From 740a71eb0ca91181f8e10deee6a0ff4676d26d4f Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Tue, 24 Sep 2024 10:14:09 +0300 Subject: [PATCH 46/60] Fix methods names --- contracts/staking/token/TokenPoolsManager.sol | 4 ++-- test/staking/token/TokenPoolsManager.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 3701ed16..178b6a80 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -109,14 +109,14 @@ contract TokenPoolsManager is AccessControl{ pool.setLimitsConfig(params); } - function deactivateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function deactivateLimitedTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.deactivate(); emit DepositedPoolDeactivated(_pool); } - function activateDoubleSidePool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function activateLimitedTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(depositedPools[_pool] != address(0), "Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); pool.activate(); diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index fde647ac..ec66f1cf 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -207,9 +207,9 @@ describe("PoolsManager", function () { const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); expect(await proxyPool.active()).to.equal(true); - await poolsManager.deactivateDoubleSidePool("TestDepositedPool"); + await poolsManager.deactivateLimitedTokenPool("TestDepositedPool"); expect(await proxyPool.active()).to.equal(false); - await poolsManager.activateDoubleSidePool("TestDepositedPool"); + await poolsManager.activateLimitedTokenPool("TestDepositedPool"); expect(await proxyPool.active()).to.equal(true); }); From effaa101e19abe2f7a6e1656ca336374bad008d2 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 3 Oct 2024 12:08:48 +0300 Subject: [PATCH 47/60] Update deploy script --- package.json | 3 +- .../ecosystem/token_staking/create_hbr_amb.ts | 61 +++++++++++++++++++ scripts/ecosystem/token_staking/deploy.ts | 18 +++--- 3 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 scripts/ecosystem/token_staking/create_hbr_amb.ts diff --git a/package.json b/package.json index b77f0aec..b5596886 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "deploy_government": "hardhat run scripts/ecosystem/government/deploy.ts --network dev", "deploy_liquid_staking": "hardhat run scripts/ecosystem/liquid_staking/deploy.ts --network dev", "deploy_token_staking": "hardhat run scripts/ecosystem/token_staking/deploy.ts --network dev", - + "deploy_hbr_token": "hardhat run scripts/ecosystem/deploy_hbr.ts --network dev", + "create_hbr_amb_pool": "hardhat run scripts/ecosystem/create_hbr_amb_pool.ts --network dev", "sourcify:dev": "hardhat sourcify --network dev", "sourcify:test": "hardhat sourcify --network test", diff --git a/scripts/ecosystem/token_staking/create_hbr_amb.ts b/scripts/ecosystem/token_staking/create_hbr_amb.ts new file mode 100644 index 00000000..cc32bc41 --- /dev/null +++ b/scripts/ecosystem/token_staking/create_hbr_amb.ts @@ -0,0 +1,61 @@ +import { ethers } from "hardhat"; +import { wrapProviderToError } from "../../../src/utils/AmbErrorProvider"; +import { loadDeployment } from "@airdao/deployments/deploying"; + +import { + LimitedTokenPool +} from "../../../typechain-types"; + +const BILLIION = 1_000_000_000; + +async function main() { + const { chainId } = await ethers.provider.getNetwork(); + + const [deployer] = await ethers.getSigners(); + wrapProviderToError(deployer.provider!); + + const hbrToken = loadDeployment("HBRToken", chainId, deployer); + + const poolsManager = loadDeployment("TokenPoolsManager", chainId, deployer); + + const mainConfig: LimitedTokenPool.MainConfigStruct = { + name: "HBR-AMB", + limitsMultiplierToken: hbrToken.address, + profitableToken: ethers.constants.AddressZero, + rewardToken: ethers.constants.AddressZero, + rewardTokenPrice: 1, + interest: 0.1 * BILLIION, + interestRate: 24 * 60 * 60, + }; + + const createTx = poolsManager.createLimitedTokenPool(mainConfig); + const createReceipt = await createTx.wait(); + console.log("createReceipt", createReceipt); + + const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { + minDepositValue: 1, + minStakeValue: 1, + fastUnstakePenalty: 0, + unstakeLockPeriod: 24 * 60 * 60, + stakeLockPeriod: 24 * 60 * 60, + maxTotalStakeValue: 1 * BILLIION, + maxStakePerUserValue: 0.01 * BILLIION, + stakeLimitsMultiplier: 10, + }; + + const configureLimitsTx = poolsManager.configureLimitedTokenPoolLimits("HBR-AMB", limitsConfig); + const configureLimitsReceipt = await configureLimitsTx.wait(); + console.log("configureLimitsReceipt", configureLimitsReceipt); + + const poolAddress = await poolsManager.getLimitedTokenPoolAddress("HBR-AMB"); + console.log("poolAddress:", poolAddress); +} + + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} + diff --git a/scripts/ecosystem/token_staking/deploy.ts b/scripts/ecosystem/token_staking/deploy.ts index fc2557b7..634947ed 100644 --- a/scripts/ecosystem/token_staking/deploy.ts +++ b/scripts/ecosystem/token_staking/deploy.ts @@ -9,12 +9,14 @@ import { LockKeeper__factory } from "../../../typechain-types"; +import { wrapProviderToError } from "../../../src/utils/AmbErrorProvider"; import { deployMultisig } from "../../utils/deployMultisig"; export async function main() { const { chainId } = await ethers.provider.getNetwork(); const [deployer] = await ethers.getSigners(); + wrapProviderToError(deployer.provider!); const multisig = await deployMultisig(ContractNames.Ecosystem_TokenPoolsManagerMultisig, deployer); @@ -37,18 +39,20 @@ export async function main() { console.log("deploying TokenPool Beacon"); - const singleSidePoolFactory = await ethers.getContractFactory("SingleSidePool"); - const singleSideBeacon = await upgrades.deployBeacon(singleSidePoolFactory); - console.log("SingleSidePool Beacon deployed to:", singleSideBeacon.address); + const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); + const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); + console.log("TokenPool Beacon deployed to:", tokenPoolBeacon.address); - const doubleSidePoolFactory = await ethers.getContractFactory("DoubleSidePool"); - const doubleSideBeacon = await upgrades.deployBeacon(doubleSidePoolFactory); - console.log("DoubleSidePool Beacon deployed to:", doubleSideBeacon.address); + console.log("deploying LimitedTokenPool Beacon"); + const limitedTokenPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); + const limitedTokenPoolBeacon = await upgrades.deployBeacon(limitedTokenPoolFactory); + console.log("LimitedTokenPool Beacon deployed to:", limitedTokenPoolBeacon.address); + console.log("deploying TokenPoolsManager"); const poolsManager = await deploy({ contractName: ContractNames.Ecosystem_TokenPoolsManager, artifactName: "TokenPoolsManager", - deployArgs: [rewardsBank.address, lockKeeper.address, singleSideBeacon.address, doubleSideBeacon.address], + deployArgs: [rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address, limitedTokenPoolBeacon.address], signer: deployer, loadIfAlreadyDeployed: true, }); From 8b5fb95d90c43bd42265f529cad92aeccc57dc10 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 3 Oct 2024 13:44:42 +0300 Subject: [PATCH 48/60] Update deploy script --- .openzeppelin/unknown-30746.json | 604 ++++++++++++++++++ contracts/staking/token/TokenPoolsManager.sol | 4 +- deployments/30746.json | 87 +++ package-lock.json | 4 +- package.json | 4 +- .../ecosystem/token_staking/create_hbr_amb.ts | 10 +- scripts/ecosystem/token_staking/deploy.ts | 2 + scripts/ecosystem/token_staking/deploy_hbr.ts | 4 +- 8 files changed, 706 insertions(+), 13 deletions(-) diff --git a/.openzeppelin/unknown-30746.json b/.openzeppelin/unknown-30746.json index 28a24401..d8b2e734 100644 --- a/.openzeppelin/unknown-30746.json +++ b/.openzeppelin/unknown-30746.json @@ -937,6 +937,610 @@ } } } + }, + "dd58fdbbaca2dcd8633518f390f40b06a94d7eb20c56d0ed62c4ad16edb29f04": { + "address": "0x939362e616DabEb19da923B8C8Fffcf2b8d3eca4", + "txHash": "0x5782310f3f2c0abdd0ce4f96520d32a803c1d4cc3dd9067f601b6cf6984c9250", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "_roles", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:56" + }, + { + "label": "active", + "offset": 0, + "slot": "2", + "type": "t_bool", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:41" + }, + { + "label": "rewardsBank", + "offset": 1, + "slot": "2", + "type": "t_contract(RewardsBank)10670", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:42" + }, + { + "label": "lockKeeper", + "offset": 0, + "slot": "3", + "type": "t_contract(LockKeeper)7997", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:43" + }, + { + "label": "config", + "offset": 0, + "slot": "4", + "type": "t_struct(Config)19258_storage", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:45" + }, + { + "label": "info", + "offset": 0, + "slot": "13", + "type": "t_struct(Info)19267_storage", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:46" + }, + { + "label": "stakers", + "offset": 0, + "slot": "17", + "type": "t_mapping(t_address,t_struct(Staker)19276_storage)", + "contract": "TokenPool", + "src": "contracts/staking/token/TokenPool.sol:48" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IERC20)5068": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(LockKeeper)7997": { + "label": "contract LockKeeper", + "numberOfBytes": "20" + }, + "t_contract(RewardsBank)10670": { + "label": "contract RewardsBank", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Staker)19276_storage)": { + "label": "mapping(address => struct TokenPool.Staker)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Config)19258_storage": { + "label": "struct TokenPool.Config", + "members": [ + { + "label": "token", + "type": "t_contract(IERC20)5068", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "rewardToken", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "rewardTokenPrice", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "minStakeValue", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "fastUnstakePenalty", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "interest", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "interestRate", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "lockPeriod", + "type": "t_uint256", + "offset": 0, + "slot": "8" + } + ], + "numberOfBytes": "288" + }, + "t_struct(Info)19267_storage": { + "label": "struct TokenPool.Info", + "members": [ + { + "label": "totalStake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "totalRewards", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastInterestUpdate", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "totalRewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(RoleData)3267_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Staker)19276_storage": { + "label": "struct TokenPool.Staker", + "members": [ + { + "label": "stake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "rewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "claimableRewards", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "lockedWithdrawal", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "68f07e86aa9f359afc979ff992f908a66ef4ff5ce04fe5964878e836fde17c77": { + "address": "0x8631AF99fB2A4aCaffF6Af7C6c5A696ADf163c2a", + "txHash": "0x53c72984efb3c9a22531ea207cfa11877ccd35e5b0bc388eb9a2a16b7c9142c4", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "_roles", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:56" + }, + { + "label": "active", + "offset": 0, + "slot": "2", + "type": "t_bool", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:54" + }, + { + "label": "lockKeeper", + "offset": 1, + "slot": "2", + "type": "t_contract(LockKeeper)7997", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:56" + }, + { + "label": "rewardsBank", + "offset": 0, + "slot": "3", + "type": "t_contract(RewardsBank)10670", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:57" + }, + { + "label": "mainConfig", + "offset": 0, + "slot": "4", + "type": "t_struct(MainConfig)17728_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:59" + }, + { + "label": "limitsConfig", + "offset": 0, + "slot": "11", + "type": "t_struct(LimitsConfig)17745_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:60" + }, + { + "label": "info", + "offset": 0, + "slot": "19", + "type": "t_struct(Info)17756_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:61" + }, + { + "label": "stakers", + "offset": 0, + "slot": "24", + "type": "t_mapping(t_address,t_struct(Staker)17769_storage)", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:63" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(LockKeeper)7997": { + "label": "contract LockKeeper", + "numberOfBytes": "20" + }, + "t_contract(RewardsBank)10670": { + "label": "contract RewardsBank", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Staker)17769_storage)": { + "label": "mapping(address => struct LimitedTokenPool.Staker)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Info)17756_storage": { + "label": "struct LimitedTokenPool.Info", + "members": [ + { + "label": "totalStake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "totalDeposit", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "totalRewards", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "lastInterestUpdate", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "totalRewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(LimitsConfig)17745_storage": { + "label": "struct LimitedTokenPool.LimitsConfig", + "members": [ + { + "label": "minDepositValue", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "minStakeValue", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "fastUnstakePenalty", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "unstakeLockPeriod", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "stakeLockPeriod", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "maxTotalStakeValue", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "maxStakePerUserValue", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "stakeLimitsMultiplier", + "type": "t_uint256", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_struct(MainConfig)17728_storage": { + "label": "struct LimitedTokenPool.MainConfig", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "limitsMultiplierToken", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "profitableToken", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "rewardToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "rewardTokenPrice", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "interest", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "interestRate", + "type": "t_uint256", + "offset": 0, + "slot": "6" + } + ], + "numberOfBytes": "224" + }, + "t_struct(RoleData)3267_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Staker)17769_storage": { + "label": "struct LimitedTokenPool.Staker", + "members": [ + { + "label": "stake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "deposit", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "rewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "claimableRewards", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "lockedWithdrawal", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "stakedAt", + "type": "t_uint256", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 178b6a80..e1e3c57c 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -185,11 +185,11 @@ contract TokenPoolsManager is AccessControl{ // VIEW METHODS - function getPoolAddress(string memory name) public view returns (address) { + function getTokenPoolAddress(string memory name) public view returns (address) { return pools[name]; } - function getDepositedPoolAdress(string memory name) public view returns (address) { + function getLimitedTokenPoolAdress(string memory name) public view returns (address) { return depositedPools[name]; } diff --git a/deployments/30746.json b/deployments/30746.json index b91f605b..494a3a4f 100644 --- a/deployments/30746.json +++ b/deployments/30746.json @@ -1721,5 +1721,92 @@ ], "deployTx": "0x9ed3d31b3b7615386da8defd5f0ce2ba6a0f209479bded528fc73989765472c1", "fullyQualifiedName": "contracts/funds/RewardsBank.sol:RewardsBank" + }, + "Ecosystem_TokenPoolsManager": { + "address": "0x3748b64E235A1EAb7c6bdDca706848d8B8a74D06", + "abi": [ + "constructor(address bank_, address lockKeeper_, address singleSideBeacon_, address doubleSideBeacon_)", + "event DepositedPoolActivated(string name)", + "event DepositedPoolCreated(string name, address pool)", + "event DepositedPoolDeactivated(string name)", + "event PoolActivated(string name)", + "event PoolCreated(string name, address pool)", + "event PoolDeactivated(string name)", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function activateLimitedTokenPool(string _pool)", + "function activateTokenPool(string _pool)", + "function bank() view returns (address)", + "function configureLimitedTokenPoolLimits(string name, tuple(uint256 minDepositValue, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 unstakeLockPeriod, uint256 stakeLockPeriod, uint256 maxTotalStakeValue, uint256 maxStakePerUserValue, uint256 stakeLimitsMultiplier) params)", + "function createLimitedTokenPool(tuple(string name, address limitsMultiplierToken, address profitableToken, address rewardToken, uint256 rewardTokenPrice, uint256 interest, uint256 interestRate) params) returns (address)", + "function createTokenPool(tuple(address token, string name, address rewardToken, uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) params) returns (address)", + "function deactivateLimitedTokenPool(string _pool)", + "function deactivateTokenPool(string _pool)", + "function depositedPools(string) view returns (address)", + "function depositedTokenPoolBeacon() view returns (address)", + "function getLimitedTokenPoolAdress(string name) view returns (address)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function getTokenPoolAddress(string name) view returns (address)", + "function grantRole(bytes32 role, address account)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function pools(string) view returns (address)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function setFastUnstakePenalty(string _pool, uint256 penalty)", + "function setFastUnstakePenaltyL(string _pool, uint256 penalty)", + "function setInterest(string _pool, uint256 _interest, uint256 _interestRate)", + "function setInterestL(string _pool, uint256 _interest, uint256 _interestRate)", + "function setLockPeriod(string _pool, uint256 period)", + "function setMaxStakePerUserValueL(string _pool, uint256 value)", + "function setMaxTotalStakeValueL(string _pool, uint256 value)", + "function setMinDepositValueL(string _pool, uint256 value)", + "function setMinStakeValue(string _pool, uint256 value)", + "function setMinStakeValueL(string _pool, uint256 value)", + "function setRewardTokenPrice(string _pool, uint256 price)", + "function setRewardTokenPriceL(string _pool, uint256 price)", + "function setStakeLimitsMultiplierL(string _pool, uint256 value)", + "function setStakeLockPeriodL(string _pool, uint256 period)", + "function setUnstakeLockPeriodL(string _pool, uint256 period)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function tokenPoolBeacon() view returns (address)" + ], + "deployTx": "0xe0778e564cb58711395d6f266b8730121e6461530aa851bba25fbc6db0d0717f", + "fullyQualifiedName": "contracts/staking/token/TokenPoolsManager.sol:TokenPoolsManager" + }, + "Ecosystem_HRBToken": { + "address": "0xbf2B802AbBC4f3aCd2B591FB167a75D8C0145456", + "abi": [ + "constructor(address admin)", + "event Approval(address indexed owner, address indexed spender, uint256 value)", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "event Transfer(address indexed from, address indexed to, uint256 value)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function MINTER_ROLE() view returns (bytes32)", + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", + "function balanceOf(address account) view returns (uint256)", + "function burn(address account, uint256 amount)", + "function decimals() view returns (uint8)", + "function decreaseAllowance(address spender, uint256 subtractedValue) returns (bool)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function grantRole(bytes32 role, address account)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function increaseAllowance(address spender, uint256 addedValue) returns (bool)", + "function mint(address account, uint256 amount)", + "function name() view returns (string)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function symbol() view returns (string)", + "function totalSupply() view returns (uint256)", + "function transfer(address to, uint256 amount) returns (bool)", + "function transferFrom(address from, address to, uint256 amount) returns (bool)" + ], + "deployTx": "0x30dbcd1b5e0643e7de2d4e8253cf63a60562033ab8a9b0abe4c90ac3f6969be8", + "fullyQualifiedName": "contracts/staking/token/HBRToken.sol:HBRToken" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b3b86fb1..01cb68fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@airdao/airdao-node-contracts", - "version": "1.3.16", + "version": "1.3.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@airdao/airdao-node-contracts", - "version": "1.3.16", + "version": "1.3.19", "dependencies": { "csv-parse": "^5.5.6", "ethers": "^5.7.2" diff --git a/package.json b/package.json index b5596886..ea8c4e34 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "deploy_government": "hardhat run scripts/ecosystem/government/deploy.ts --network dev", "deploy_liquid_staking": "hardhat run scripts/ecosystem/liquid_staking/deploy.ts --network dev", "deploy_token_staking": "hardhat run scripts/ecosystem/token_staking/deploy.ts --network dev", - "deploy_hbr_token": "hardhat run scripts/ecosystem/deploy_hbr.ts --network dev", - "create_hbr_amb_pool": "hardhat run scripts/ecosystem/create_hbr_amb_pool.ts --network dev", + "deploy_hbr_token": "hardhat run scripts/ecosystem/token_staking/deploy_hbr.ts --network dev", + "create_hbr_amb_pool": "hardhat run scripts/ecosystem/token_staking/create_hbr_amb.ts --network dev", "sourcify:dev": "hardhat sourcify --network dev", "sourcify:test": "hardhat sourcify --network test", diff --git a/scripts/ecosystem/token_staking/create_hbr_amb.ts b/scripts/ecosystem/token_staking/create_hbr_amb.ts index cc32bc41..1f1c7f4e 100644 --- a/scripts/ecosystem/token_staking/create_hbr_amb.ts +++ b/scripts/ecosystem/token_staking/create_hbr_amb.ts @@ -14,9 +14,9 @@ async function main() { const [deployer] = await ethers.getSigners(); wrapProviderToError(deployer.provider!); - const hbrToken = loadDeployment("HBRToken", chainId, deployer); + const hbrToken = loadDeployment("Ecosystem_HRBToken", chainId, deployer); - const poolsManager = loadDeployment("TokenPoolsManager", chainId, deployer); + const poolsManager = loadDeployment("Ecosystem_TokenPoolsManager", chainId, deployer); const mainConfig: LimitedTokenPool.MainConfigStruct = { name: "HBR-AMB", @@ -28,7 +28,7 @@ async function main() { interestRate: 24 * 60 * 60, }; - const createTx = poolsManager.createLimitedTokenPool(mainConfig); + const createTx = await poolsManager.createLimitedTokenPool(mainConfig); const createReceipt = await createTx.wait(); console.log("createReceipt", createReceipt); @@ -43,11 +43,11 @@ async function main() { stakeLimitsMultiplier: 10, }; - const configureLimitsTx = poolsManager.configureLimitedTokenPoolLimits("HBR-AMB", limitsConfig); + const configureLimitsTx = await poolsManager.configureLimitedTokenPoolLimits("HBR-AMB", limitsConfig); const configureLimitsReceipt = await configureLimitsTx.wait(); console.log("configureLimitsReceipt", configureLimitsReceipt); - const poolAddress = await poolsManager.getLimitedTokenPoolAddress("HBR-AMB"); + const poolAddress = await poolsManager.getLimitedTokenPoolAdress("HBR-AMB"); console.log("poolAddress:", poolAddress); } diff --git a/scripts/ecosystem/token_staking/deploy.ts b/scripts/ecosystem/token_staking/deploy.ts index 634947ed..3a5b0961 100644 --- a/scripts/ecosystem/token_staking/deploy.ts +++ b/scripts/ecosystem/token_staking/deploy.ts @@ -41,11 +41,13 @@ export async function main() { const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); + await tokenPoolBeacon.deployed(); console.log("TokenPool Beacon deployed to:", tokenPoolBeacon.address); console.log("deploying LimitedTokenPool Beacon"); const limitedTokenPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); const limitedTokenPoolBeacon = await upgrades.deployBeacon(limitedTokenPoolFactory); + await limitedTokenPoolBeacon.deployed(); console.log("LimitedTokenPool Beacon deployed to:", limitedTokenPoolBeacon.address); console.log("deploying TokenPoolsManager"); diff --git a/scripts/ecosystem/token_staking/deploy_hbr.ts b/scripts/ecosystem/token_staking/deploy_hbr.ts index 370504a9..29011bdb 100644 --- a/scripts/ecosystem/token_staking/deploy_hbr.ts +++ b/scripts/ecosystem/token_staking/deploy_hbr.ts @@ -19,8 +19,8 @@ async function main() { loadIfAlreadyDeployed: true, }); - await airBond.grantRole(await airBond.DEFAULT_ADMIN_ROLE(), deployer.address); // - await airBond.grantRole(await airBond.MINTER_ROLE(), deployer.address); + await (await airBond.grantRole(await airBond.DEFAULT_ADMIN_ROLE(), deployer.address)).wait(); // + await (await airBond.grantRole(await airBond.MINTER_ROLE(), deployer.address)).wait(); } if (require.main === module) { From e3e5cda08826df20cde3b0d2eda81a87a325edc8 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Thu, 3 Oct 2024 19:32:33 +0300 Subject: [PATCH 49/60] Add method to get max user stake --- .openzeppelin/unknown-30746.json | 334 ++++++++++++++++++ contracts/staking/token/LimitedTokenPool.sol | 4 + deployments/30746.json | 18 +- .../ecosystem/token_staking/create_hbr_amb.ts | 2 +- scripts/ecosystem/token_staking/deploy_hbr.ts | 2 +- src/contracts/names.ts | 2 +- 6 files changed, 350 insertions(+), 12 deletions(-) diff --git a/.openzeppelin/unknown-30746.json b/.openzeppelin/unknown-30746.json index d8b2e734..0d3620ef 100644 --- a/.openzeppelin/unknown-30746.json +++ b/.openzeppelin/unknown-30746.json @@ -1541,6 +1541,340 @@ } } } + }, + "fb03629654350fce2bef831cd33ff1d614c70cc6f0d4d17fd17025e845fb64aa": { + "address": "0x95DCF0d9e69A75B93cE420d5B13dDAA397bF7958", + "txHash": "0xcf9ca95d4ad3e4afa695b240d7a0b6271fe3abee24eccaf795f91dca89f091e8", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "_roles", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:56" + }, + { + "label": "active", + "offset": 0, + "slot": "2", + "type": "t_bool", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:54" + }, + { + "label": "lockKeeper", + "offset": 1, + "slot": "2", + "type": "t_contract(LockKeeper)7997", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:56" + }, + { + "label": "rewardsBank", + "offset": 0, + "slot": "3", + "type": "t_contract(RewardsBank)10670", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:57" + }, + { + "label": "mainConfig", + "offset": 0, + "slot": "4", + "type": "t_struct(MainConfig)17728_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:59" + }, + { + "label": "limitsConfig", + "offset": 0, + "slot": "11", + "type": "t_struct(LimitsConfig)17745_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:60" + }, + { + "label": "info", + "offset": 0, + "slot": "19", + "type": "t_struct(Info)17756_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:61" + }, + { + "label": "stakers", + "offset": 0, + "slot": "24", + "type": "t_mapping(t_address,t_struct(Staker)17769_storage)", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:63" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(LockKeeper)7997": { + "label": "contract LockKeeper", + "numberOfBytes": "20" + }, + "t_contract(RewardsBank)10670": { + "label": "contract RewardsBank", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Staker)17769_storage)": { + "label": "mapping(address => struct LimitedTokenPool.Staker)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Info)17756_storage": { + "label": "struct LimitedTokenPool.Info", + "members": [ + { + "label": "totalStake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "totalDeposit", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "totalRewards", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "lastInterestUpdate", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "totalRewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(LimitsConfig)17745_storage": { + "label": "struct LimitedTokenPool.LimitsConfig", + "members": [ + { + "label": "minDepositValue", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "minStakeValue", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "fastUnstakePenalty", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "unstakeLockPeriod", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "stakeLockPeriod", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "maxTotalStakeValue", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "maxStakePerUserValue", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "stakeLimitsMultiplier", + "type": "t_uint256", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_struct(MainConfig)17728_storage": { + "label": "struct LimitedTokenPool.MainConfig", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "limitsMultiplierToken", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "profitableToken", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "rewardToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "rewardTokenPrice", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "interest", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "interestRate", + "type": "t_uint256", + "offset": 0, + "slot": "6" + } + ], + "numberOfBytes": "224" + }, + "t_struct(RoleData)3267_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Staker)17769_storage": { + "label": "struct LimitedTokenPool.Staker", + "members": [ + { + "label": "stake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "deposit", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "rewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "claimableRewards", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "lockedWithdrawal", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "stakedAt", + "type": "t_uint256", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index 2978f1bc..c7548c8a 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -333,6 +333,10 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { return rewardsAmount + stakers[user].claimableRewards - stakers[user].rewardsDebt; } + function getMaxUserStakeValue(address user) public view returns (uint) { + return _maxUserStakeValue(user); + } + // INTERNAL METHODS function _addInterest() internal { if (info.lastInterestUpdate + mainConfig.interestRate > block.timestamp) return; diff --git a/deployments/30746.json b/deployments/30746.json index 494a3a4f..5399b855 100644 --- a/deployments/30746.json +++ b/deployments/30746.json @@ -1663,7 +1663,7 @@ } }, "Ecosystem_TokenPoolsManager_Multisig": { - "address": "0xF7c8f345Ac1d29F13c16d8Ae34f534D9056E3FF2", + "address": "0x724AfcA5194639475094d6a67c4c47d52D6CeC63", "abi": [ "constructor(address[] _signers, bool[] isInitiatorFlags, uint256 _threshold, address owner)", "event Confirmation(address indexed sender, uint256 indexed txId)", @@ -1699,11 +1699,11 @@ "function transferOwnership(address newOwner)", "function withdraw(address to, uint256 amount)" ], - "deployTx": "0xbd191d729a142b5c69cba31f0ce5154b5b91722444e14b89b68b2fba88b699f6", + "deployTx": "0x67d093a2dc5c8ca61dd8996ee8a41efb6b1c84c7ecd8f8523ad4647a67238ac8", "fullyQualifiedName": "contracts/multisig/Multisig.sol:Multisig" }, "Ecosystem_TokenPoolsManager_RewardsBank": { - "address": "0x92f47Ee54B8320A4E74Ddb256dc6e9129bCFD053", + "address": "0xCCd30d8848ac1559a8215B556fB52C2dd12c7516", "abi": [ "constructor()", "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", @@ -1719,11 +1719,11 @@ "function withdrawAmb(address addressTo, uint256 amount)", "function withdrawErc20(address tokenAddress, address addressTo, uint256 amount)" ], - "deployTx": "0x9ed3d31b3b7615386da8defd5f0ce2ba6a0f209479bded528fc73989765472c1", + "deployTx": "0xe81a1e26055f7b0ae2127089697c7825d459fa22a3678f27a116555403dfccd9", "fullyQualifiedName": "contracts/funds/RewardsBank.sol:RewardsBank" }, "Ecosystem_TokenPoolsManager": { - "address": "0x3748b64E235A1EAb7c6bdDca706848d8B8a74D06", + "address": "0xe684E41da338363C2777c0097356752d6f24BDae", "abi": [ "constructor(address bank_, address lockKeeper_, address singleSideBeacon_, address doubleSideBeacon_)", "event DepositedPoolActivated(string name)", @@ -1772,11 +1772,11 @@ "function supportsInterface(bytes4 interfaceId) view returns (bool)", "function tokenPoolBeacon() view returns (address)" ], - "deployTx": "0xe0778e564cb58711395d6f266b8730121e6461530aa851bba25fbc6db0d0717f", + "deployTx": "0xa3d88b934c95d3df29c2dc19537a6449cc5ebce303eaf50a2cb6860775f43496", "fullyQualifiedName": "contracts/staking/token/TokenPoolsManager.sol:TokenPoolsManager" }, - "Ecosystem_HRBToken": { - "address": "0xbf2B802AbBC4f3aCd2B591FB167a75D8C0145456", + "Ecosystem_HBRToken": { + "address": "0x7b6B4cc8704EC9533F69E46682c9aD010F30F0C9", "abi": [ "constructor(address admin)", "event Approval(address indexed owner, address indexed spender, uint256 value)", @@ -1806,7 +1806,7 @@ "function transfer(address to, uint256 amount) returns (bool)", "function transferFrom(address from, address to, uint256 amount) returns (bool)" ], - "deployTx": "0x30dbcd1b5e0643e7de2d4e8253cf63a60562033ab8a9b0abe4c90ac3f6969be8", + "deployTx": "0xdda8f77783e2347249ba80546a99665569b58c1399a175d8fa9d3b1c0153485c", "fullyQualifiedName": "contracts/staking/token/HBRToken.sol:HBRToken" } } \ No newline at end of file diff --git a/scripts/ecosystem/token_staking/create_hbr_amb.ts b/scripts/ecosystem/token_staking/create_hbr_amb.ts index 1f1c7f4e..62ab3412 100644 --- a/scripts/ecosystem/token_staking/create_hbr_amb.ts +++ b/scripts/ecosystem/token_staking/create_hbr_amb.ts @@ -14,7 +14,7 @@ async function main() { const [deployer] = await ethers.getSigners(); wrapProviderToError(deployer.provider!); - const hbrToken = loadDeployment("Ecosystem_HRBToken", chainId, deployer); + const hbrToken = loadDeployment("Ecosystem_HBRToken", chainId, deployer); const poolsManager = loadDeployment("Ecosystem_TokenPoolsManager", chainId, deployer); diff --git a/scripts/ecosystem/token_staking/deploy_hbr.ts b/scripts/ecosystem/token_staking/deploy_hbr.ts index 29011bdb..b096af31 100644 --- a/scripts/ecosystem/token_staking/deploy_hbr.ts +++ b/scripts/ecosystem/token_staking/deploy_hbr.ts @@ -12,7 +12,7 @@ async function main() { } const airBond = await deploy({ - contractName: ContractNames.Ecosystem_HRBToken, + contractName: ContractNames.Ecosystem_HBRToken, artifactName: "HBRToken", deployArgs: [deployer.address], signer: deployer, diff --git a/src/contracts/names.ts b/src/contracts/names.ts index d2c5d536..e8ccb18d 100644 --- a/src/contracts/names.ts +++ b/src/contracts/names.ts @@ -93,7 +93,7 @@ export enum ContractNames { Ecosystem_TokenPoolsManager = "Ecosystem_TokenPoolsManager", Ecosystem_TokenPoolsManagerMultisig = "Ecosystem_TokenPoolsManager_Multisig", Ecosystem_TokenPoolsManagerRewardsBank = "Ecosystem_TokenPoolsManager_RewardsBank", - Ecosystem_HRBToken = "Ecosystem_HRBToken", + Ecosystem_HBRToken = "Ecosystem_HBRToken", } export const MULTISIGS_COMMON = { From 2ec78fa7143e693e9c379db019b83db27aaaa70b Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 11:02:24 +0300 Subject: [PATCH 50/60] Update --- contracts/staking/token/LimitedTokenPool.sol | 84 +------- .../token/LimitedTokenPoolsManager.sol | 73 +++++++ contracts/staking/token/TokenPool.sol | 85 ++++---- contracts/staking/token/TokenPoolsManager.sol | 189 ++++-------------- 4 files changed, 152 insertions(+), 279 deletions(-) create mode 100644 contracts/staking/token/LimitedTokenPoolsManager.sol diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index c7548c8a..2d692019 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -17,12 +17,12 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { address limitsMultiplierToken; address profitableToken; address rewardToken; - uint rewardTokenPrice; - uint interest; - uint interestRate; } struct LimitsConfig { + uint rewardTokenPrice; + uint interest; + uint interestRate; uint minDepositValue; uint minStakeValue; uint fastUnstakePenalty; @@ -66,21 +66,11 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { event Deactivated(); event Activated(); - event RewardTokenPriceChanged(uint price); - event InterestChanged(uint interest); - event InterestRateChanged(uint interestRate); - event MinDepositValueChanged(uint minDepositValue); - event MinStakeValueChanged(uint minStakeValue); - event FastUnstakePenaltyChanged(uint penalty); - event UnstakeLockPeriodChanged(uint period); - event StakeLockPeriodChanged(uint period); - event MaxTotalStakeValueChanged(uint poolMaxStakeValue); - event MaxStakePerUserValueChanged(uint maxStakePerUserValue); - event StakeLimitsMultiplierChanged(uint value); + event LimitsConfigChanged(LimitsConfig config); event Deposited(address indexed user, uint amount); event Withdrawn(address indexed user, uint amount); - event Staked(address indexed user, uint amount); + event Staked(address indexed user, uint amount, uint timestamp); event Claim(address indexed user, uint amount); event Interest(uint amount); event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); @@ -103,6 +93,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { function setLimitsConfig(LimitsConfig calldata config) public onlyRole(DEFAULT_ADMIN_ROLE) { limitsConfig = config; + emit LimitsConfigChanged(config); } function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { @@ -117,61 +108,6 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { emit Deactivated(); } - // SETTERS FOR PARAMS - - function setRewardTokenPrice(uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { - mainConfig.rewardTokenPrice = price; - emit RewardTokenPriceChanged(price); - } - - function setInterest(uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { - mainConfig.interest = _interest; - mainConfig.interestRate = _interestRate; - - emit InterestRateChanged(_interestRate); - emit InterestChanged(_interest); - } - - function setMinDepositValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.minDepositValue = value; - emit MinStakeValueChanged(value); - } - - function setMinStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.minStakeValue = value; - emit MinDepositValueChanged(value); - } - - function setFastUnstakePenalty(uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.fastUnstakePenalty = penalty; - emit FastUnstakePenaltyChanged(penalty); - } - - function setUnstakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.unstakeLockPeriod = period; - emit UnstakeLockPeriodChanged(period); - } - - function setStakeLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.stakeLockPeriod = period; - emit StakeLockPeriodChanged(period); - } - - function setMaxTotalStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.maxTotalStakeValue = value; - emit MaxTotalStakeValueChanged(value); - } - - function setMaxStakePerUserValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.maxStakePerUserValue = value; - emit MaxStakePerUserValueChanged(value); - } - - function setStakeLimitsMultiplier(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - limitsConfig.stakeLimitsMultiplier = value; - emit StakeLimitsMultiplierChanged(value); - } - // PUBLIC METHODS function deposit(uint amount) public payable { @@ -230,7 +166,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { require(stakers[msg.sender].stake <= limitsConfig.maxStakePerUserValue, "Pool: max stake per user exceeded"); _updateRewardsDebt(msg.sender, _calcRewards(stakers[msg.sender].stake)); - emit Staked(msg.sender, amount); + emit Staked(msg.sender, amount, block.timestamp); } function unstake(uint amount) public { @@ -339,9 +275,9 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { // INTERNAL METHODS function _addInterest() internal { - if (info.lastInterestUpdate + mainConfig.interestRate > block.timestamp) return; + if (info.lastInterestUpdate + limitsConfig.interestRate > block.timestamp) return; uint timePassed = block.timestamp - info.lastInterestUpdate; - uint newRewards = info.totalStake * mainConfig.interest * timePassed / BILLION / mainConfig.interestRate; + uint newRewards = info.totalStake * limitsConfig.interest * timePassed / BILLION / limitsConfig.interestRate; info.totalRewards += newRewards; info.lastInterestUpdate = block.timestamp; @@ -368,7 +304,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { stakers[user].claimableRewards = 0; // TODO: Use decimals for reward token price - uint rewardTokenAmount = amount * mainConfig.rewardTokenPrice; + uint rewardTokenAmount = amount * limitsConfig.rewardTokenPrice; if (mainConfig.rewardToken == address(0)) { rewardsBank.withdrawAmb(payable(user), amount); } else { diff --git a/contracts/staking/token/LimitedTokenPoolsManager.sol b/contracts/staking/token/LimitedTokenPoolsManager.sol new file mode 100644 index 00000000..e638635d --- /dev/null +++ b/contracts/staking/token/LimitedTokenPoolsManager.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import "./LimitedTokenPool.sol"; +import "../../funds/RewardsBank.sol"; +import "../../LockKeeper.sol"; + +contract LimitedTokenPoolsManager is AccessControl { + LockKeeper lockKeeper; + RewardsBank public bank; + UpgradeableBeacon public limitedTokenPoolBeacon; + + address[] public pools; + + constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon doubleSideBeacon_) { + lockKeeper = lockKeeper_; + bank = bank_; + limitedTokenPoolBeacon = doubleSideBeacon_; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + event LimitedPoolCreated(address pool); + event LimitedPoolConfigured(address pool, LimitedTokenPool.LimitsConfig params); + event LimitedPoolDeactivated(address pool); + event LimitedPoolActivated(address pool); + + // LIMITED POOL METHODS + function createPool(LimitedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + bytes memory data = abi.encodeWithSignature( + "initialize(address,address,(string,address,address,address))", + bank, lockKeeper, params); + address pool = address(new BeaconProxy(address(limitedTokenPoolBeacon), data)); + pools.push(pool); + bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); + emit LimitedPoolCreated(pool); + return pool; + } + + function configurePool(address _pool, LimitedTokenPool.LimitsConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_isPool(_pool),"Pool does not exist"); + LimitedTokenPool pool = LimitedTokenPool(_pool); + pool.setLimitsConfig(params); + emit LimitedPoolConfigured(_pool, params); + } + + function deactivatePool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_isPool(_pool),"Pool does not exist"); + LimitedTokenPool pool = LimitedTokenPool(_pool); + pool.deactivate(); + emit LimitedPoolDeactivated(_pool); + } + + function activatePool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_isPool(_pool),"Pool does not exist"); + LimitedTokenPool pool = LimitedTokenPool(_pool); + pool.activate(); + emit LimitedPoolActivated(_pool); + } + + function _isPool(address pool) internal view returns (bool) { + for (uint i = 0; i < pools.length; i++) { + if (pools[i] == pool) { + return true; + } + } + return false; + } + +} + diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 3800002d..3d0c987b 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -10,10 +10,13 @@ import "../../LockKeeper.sol"; contract TokenPool is Initializable, AccessControl, IOnBlockListener { - struct Config { + struct MainConfig { IERC20 token; string name; address rewardToken; + } + + struct LimitsConfig { uint rewardTokenPrice; // The coefficient to calculate the reward token amount uint minStakeValue; uint fastUnstakePenalty; @@ -42,7 +45,8 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { RewardsBank rewardsBank; LockKeeper lockKeeper; - Config public config; + MainConfig public mainConfig; // immutable + LimitsConfig public limitsConfig; // mutable Info public info; mapping(address => Staker) public stakers; @@ -51,21 +55,19 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { event Deactivated(); event Activated(); - event MinStakeValueChanged(uint minStakeValue); - event InterestRateChanged(uint interest, uint interestRate); - event LockPeriodChanged(uint period); - event RewardTokenPriceChanged(uint price); - event FastUnstakePenaltyChanged(uint penalty); + event LimitsConfigChanged(LimitsConfig limitsConfig); event StakeChanged(address indexed user, uint amount); event Claim(address indexed user, uint amount); event Interest(uint amount); event UnstakeLocked(address indexed user, uint amount, uint unlockTime, uint creationTime); event UnstakeFast(address indexed user, uint amount, uint penalty); - function initialize(RewardsBank bank_, LockKeeper keeper_, Config calldata config_) public initializer { + function initialize(RewardsBank bank_, LockKeeper keeper_, MainConfig calldata mainConfig_, LimitsConfig calldata limitsConfig_) public initializer { + //TODO: Should validate input params rewardsBank = bank_; lockKeeper = keeper_; - config = config_; + mainConfig = mainConfig_; + limitsConfig = limitsConfig_; info.lastInterestUpdate = block.timestamp; @@ -76,6 +78,12 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { // OWNER METHODS + function setLimitsConfig(LimitsConfig calldata _limitsConfig) public onlyRole(DEFAULT_ADMIN_ROLE) { + //TODO: Validate input params + limitsConfig = _limitsConfig; + emit LimitsConfigChanged(_limitsConfig); + } + function activate() public onlyRole(DEFAULT_ADMIN_ROLE) { require(!active, "Pool is already active"); active = true; @@ -88,39 +96,12 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { emit Deactivated(); } - function setMinStakeValue(uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.minStakeValue = value; - emit MinStakeValueChanged(value); - } - - function setInterest(uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { - _addInterest(); - config.interest = _interest; - config.interestRate = _interestRate; - emit InterestRateChanged(config.interest, config.interestRate); - } - - function setLockPeriod(uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.lockPeriod = period; - emit LockPeriodChanged(period); - } - - function setRewardTokenPrice(uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.rewardTokenPrice = price; - emit RewardTokenPriceChanged(price); - } - - function setFastUnstakePenalty(uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { - config.fastUnstakePenalty = penalty; - emit FastUnstakePenaltyChanged(penalty); - } - // PUBLIC METHODS function stake(uint amount) public { require(active, "Pool is not active"); - require(amount >= config.minStakeValue, "Pool: stake value is too low"); - require(config.token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + require(amount >= limitsConfig.minStakeValue, "Pool: stake value is too low"); + require(mainConfig.token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); _stake(msg.sender, amount); @@ -137,17 +118,17 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { if (lockKeeper.getLock(stakers[msg.sender].lockedWithdrawal).totalClaims > 0) // prev lock exists canceledAmount = lockKeeper.cancelLock(stakers[msg.sender].lockedWithdrawal); - config.token.approve(address(lockKeeper), amount + canceledAmount); + mainConfig.token.approve(address(lockKeeper), amount + canceledAmount); // lock funds stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( - msg.sender, address(config.token), uint64(block.timestamp + config.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(config.token)))) + msg.sender, address(mainConfig.token), uint64(block.timestamp + limitsConfig.lockPeriod), amount + canceledAmount, + string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainConfig.token)))) ); _claimRewards(msg.sender); - emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + config.lockPeriod, block.timestamp); + emit UnstakeLocked(msg.sender, amount + canceledAmount, block.timestamp + limitsConfig.lockPeriod, block.timestamp); emit StakeChanged(msg.sender, stakers[msg.sender].stake); } @@ -156,8 +137,8 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { _unstake(msg.sender, amount); - uint penalty = amount * config.fastUnstakePenalty / BILLION; - SafeERC20.safeTransfer(config.token, msg.sender, amount - penalty); + uint penalty = amount * limitsConfig.fastUnstakePenalty / BILLION; + SafeERC20.safeTransfer(mainConfig.token, msg.sender, amount - penalty); _claimRewards(msg.sender); @@ -176,8 +157,12 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { // VIEW METHODS - function getConfig() public view returns (Config memory) { - return config; + function getMainConfig() public view returns (MainConfig memory) { + return mainConfig; + } + + function getLimitsConfig() public view returns (LimitsConfig memory) { + return limitsConfig; } function getInfo() public view returns (Info memory) { @@ -208,9 +193,9 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { } function _addInterest() internal { - if (info.lastInterestUpdate + config.interestRate > block.timestamp) return; + if (info.lastInterestUpdate + limitsConfig.interestRate > block.timestamp) return; uint timePassed = block.timestamp - info.lastInterestUpdate; - uint newRewards = info.totalStake * config.interest * timePassed / BILLION / config.interestRate; + uint newRewards = info.totalStake * limitsConfig.interest * timePassed / BILLION / limitsConfig.interestRate; info.totalRewards += newRewards; info.lastInterestUpdate = block.timestamp; @@ -251,8 +236,8 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { stakers[user].claimableRewards = 0; - uint rewardTokenAmount = amount * config.rewardTokenPrice; - rewardsBank.withdrawErc20(config.rewardToken, payable(user), rewardTokenAmount); + uint rewardTokenAmount = amount * limitsConfig.rewardTokenPrice; + rewardsBank.withdrawErc20(mainConfig.rewardToken, payable(user), rewardTokenAmount); emit Claim(user, rewardTokenAmount); } diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index e1e3c57c..0031405c 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -5,192 +5,71 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./TokenPool.sol"; -import "./LimitedTokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; contract TokenPoolsManager is AccessControl{ LockKeeper lockKeeper; RewardsBank public bank; - UpgradeableBeacon public tokenPoolBeacon; - UpgradeableBeacon public depositedTokenPoolBeacon; + UpgradeableBeacon public beacon; - mapping(string => address) public pools; - mapping(string => address) public depositedPools; + address[] public pools; - constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon singleSideBeacon_, UpgradeableBeacon doubleSideBeacon_) { + constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon _beacon) { lockKeeper = lockKeeper_; bank = bank_; - tokenPoolBeacon = singleSideBeacon_; - depositedTokenPoolBeacon = doubleSideBeacon_; + beacon = _beacon; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } - event PoolCreated(string name, address pool); - event PoolDeactivated(string name); - event PoolActivated(string name); - event DepositedPoolCreated(string name, address pool); - event DepositedPoolDeactivated(string name); - event DepositedPoolActivated(string name); + event PoolCreated(address pool); + event PoolConfigured(address pool, TokenPool.LimitsConfig params); + event PoolDeactivated(address pool); + event PoolActivated(address pool); // OWNER METHODS // TOKEN POOL METHODS - function createTokenPool(TokenPool.Config calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createPool(TokenPool.MainConfig calldata mainConfig, TokenPool.LimitsConfig calldata limitsConfig) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { bytes memory data = abi.encodeWithSignature( - "initialize(address,address,(address,string,address,uint256,uint256,uint256,uint256,uint256,uint256))", - bank, lockKeeper, params); - address pool = address(new BeaconProxy(address(tokenPoolBeacon), data)); - pools[params.name] = pool; + "initialize(address,address,(address,string,address),(uint256,uint256,uint256,uint256,uint256,uint256))", + bank, lockKeeper, mainConfig, limitsConfig); + address pool = address(new BeaconProxy(address(beacon), data)); + pools.push(pool); bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit PoolCreated(params.name, pool); + emit PoolCreated(pool); return pool; } - function deactivateTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.deactivate(); - emit PoolDeactivated(_pool); - } - - function activateTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - - TokenPool pool = TokenPool(pools[_pool]); - pool.activate(); - emit PoolActivated(_pool); - } - - function setMinStakeValue(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.setMinStakeValue(value); - } - - function setInterest(string memory _pool, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.setInterest(_interest, _interestRate); - } - - function setLockPeriod(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.setLockPeriod(period); - } - - function setRewardTokenPrice(string memory _pool, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.setRewardTokenPrice(price); - } - - function setFastUnstakePenalty(string memory _pool, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(pools[_pool] != address(0), "Pool does not exist"); - TokenPool pool = TokenPool(pools[_pool]); - pool.setFastUnstakePenalty(penalty); - } - - // DEPOSITED POOL METHODS - function createLimitedTokenPool(LimitedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { - bytes memory data = abi.encodeWithSignature( - "initialize(address,address,(string,address,address,address,uint256,uint256,uint256))", - bank, lockKeeper, params); - address pool = address(new BeaconProxy(address(depositedTokenPoolBeacon), data)); - depositedPools[params.name] = pool; - bank.grantRole(bank.DEFAULT_ADMIN_ROLE(), address(pool)); - emit DepositedPoolCreated(params.name, pool); - return pool; - } - - function configureLimitedTokenPoolLimits(string calldata name, LimitedTokenPool.LimitsConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[name] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[name]); - pool.setLimitsConfig(params); + function configurePool(address pool, TokenPool.LimitsConfig calldata limitsConfig) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_isPool(pool), "Pool does not exist"); + TokenPool(pool).setLimitsConfig(limitsConfig); + emit PoolConfigured(pool, limitsConfig); } - function deactivateLimitedTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); + function deactivateTokenPool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_isPool(_pool), "Pool does not exist"); + TokenPool pool = TokenPool(_pool); pool.deactivate(); - emit DepositedPoolDeactivated(_pool); + emit PoolDeactivated(_pool); } - function activateLimitedTokenPool(string memory _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); + function activateTokenPool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_isPool(_pool), "Pool does not exist"); + TokenPool pool = TokenPool(_pool); pool.activate(); - emit DepositedPoolActivated(_pool); - } - - function setRewardTokenPriceL(string memory _pool, uint price) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setRewardTokenPrice(price); - } - - function setInterestL(string memory _pool, uint _interest, uint _interestRate) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setInterest(_interest, _interestRate); - } - - function setMinDepositValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setMinDepositValue(value); - } - - function setMinStakeValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setMinStakeValue(value); - } - - function setFastUnstakePenaltyL(string memory _pool, uint penalty) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setFastUnstakePenalty(penalty); - } - - function setUnstakeLockPeriodL(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setUnstakeLockPeriod(period); - } - - function setStakeLockPeriodL(string memory _pool, uint period) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setStakeLockPeriod(period); - } - - function setMaxTotalStakeValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setMaxTotalStakeValue(value); - } - - function setMaxStakePerUserValueL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setMaxStakePerUserValue(value); - } - - function setStakeLimitsMultiplierL(string memory _pool, uint value) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(depositedPools[_pool] != address(0), "Pool does not exist"); - LimitedTokenPool pool = LimitedTokenPool(depositedPools[_pool]); - pool.setStakeLimitsMultiplier(value); + emit PoolActivated(_pool); } - // VIEW METHODS + // INTERNAL METHODS - function getTokenPoolAddress(string memory name) public view returns (address) { - return pools[name]; + function _isPool(address pool) internal view returns (bool) { + for (uint i = 0; i < pools.length; i++) { + if (pools[i] == pool) { + return true; + } + } + return false; } - function getLimitedTokenPoolAdress(string memory name) public view returns (address) { - return depositedPools[name]; - } } From 0d7d9436e77d04333c6f4866a02f18e54725473d Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 11:17:58 +0300 Subject: [PATCH 51/60] update --- contracts/staking/token/LimitedTokenPool.sol | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index 2d692019..e1c3e75e 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -20,8 +20,8 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { } struct LimitsConfig { - uint rewardTokenPrice; - uint interest; + uint rewardTokenPrice; // Represented as parts of BILLION 1 = Billion + uint interest; // represented as parts of BILLION. 100% = Billion uint interestRate; uint minDepositValue; uint minStakeValue; @@ -30,7 +30,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { uint stakeLockPeriod; // Time in seconds to how long the stake is locker before unstake uint maxTotalStakeValue; uint maxStakePerUserValue; - uint stakeLimitsMultiplier; // Should be represented as parts of BILLION + uint stakeLimitsMultiplier; // Represented as parts of BILLION 1 = Billion } struct Info { @@ -92,6 +92,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { // OWNER METHODS function setLimitsConfig(LimitsConfig calldata config) public onlyRole(DEFAULT_ADMIN_ROLE) { + //TODO: Validate config limitsConfig = config; emit LimitsConfigChanged(config); } @@ -150,6 +151,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { require(mainConfig.profitableToken == address(0), "Pool: does not accept native coin"); require(msg.value == amount, "Pool: wrong amount of native coin"); } else { + require(mainConfig.profitableToken != address(0), "Pool: does not accept ERC20 tokens"); IERC20(mainConfig.profitableToken).safeTransferFrom(msg.sender, address(this), amount); } @@ -304,7 +306,7 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { stakers[user].claimableRewards = 0; // TODO: Use decimals for reward token price - uint rewardTokenAmount = amount * limitsConfig.rewardTokenPrice; + uint rewardTokenAmount = amount * limitsConfig.rewardTokenPrice / BILLION; if (mainConfig.rewardToken == address(0)) { rewardsBank.withdrawAmb(payable(user), amount); } else { @@ -324,4 +326,9 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { else info.totalRewardsDebt += newDebt - oldDebt; stakers[user].rewardsDebt = newDebt; } + + function _isLimitsConfigValid(LimitsConfig calldata config) internal pure returns (bool) { + + return true; + } } From ba3191ec7001696464e1aeef90bdaaa029243b4d Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 11:20:32 +0300 Subject: [PATCH 52/60] update --- .../staking/token/LimitedTokenPoolsManager.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/staking/token/LimitedTokenPoolsManager.sol b/contracts/staking/token/LimitedTokenPoolsManager.sol index e638635d..0b270a8b 100644 --- a/contracts/staking/token/LimitedTokenPoolsManager.sol +++ b/contracts/staking/token/LimitedTokenPoolsManager.sol @@ -1,25 +1,24 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./LimitedTokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -contract LimitedTokenPoolsManager is AccessControl { +contract LimitedTokenPoolsManager is Ownable { LockKeeper lockKeeper; RewardsBank public bank; UpgradeableBeacon public limitedTokenPoolBeacon; address[] public pools; - constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon doubleSideBeacon_) { + constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon doubleSideBeacon_) Ownable() { lockKeeper = lockKeeper_; bank = bank_; limitedTokenPoolBeacon = doubleSideBeacon_; - _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } event LimitedPoolCreated(address pool); @@ -28,7 +27,7 @@ contract LimitedTokenPoolsManager is AccessControl { event LimitedPoolActivated(address pool); // LIMITED POOL METHODS - function createPool(LimitedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { + function createPool(LimitedTokenPool.MainConfig calldata params) public onlyOwner returns (address) { bytes memory data = abi.encodeWithSignature( "initialize(address,address,(string,address,address,address))", bank, lockKeeper, params); @@ -39,21 +38,21 @@ contract LimitedTokenPoolsManager is AccessControl { return pool; } - function configurePool(address _pool, LimitedTokenPool.LimitsConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { + function configurePool(address _pool, LimitedTokenPool.LimitsConfig calldata params) public onlyOwner { require(_isPool(_pool),"Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(_pool); pool.setLimitsConfig(params); emit LimitedPoolConfigured(_pool, params); } - function deactivatePool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function deactivatePool(address _pool) public onlyOwner { require(_isPool(_pool),"Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(_pool); pool.deactivate(); emit LimitedPoolDeactivated(_pool); } - function activatePool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { + function activatePool(address _pool) public onlyOwner { require(_isPool(_pool),"Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(_pool); pool.activate(); From 0cde971f592db0586300bd2e099f8e5e4f3eab38 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 15:56:49 +0300 Subject: [PATCH 53/60] Fix tests --- contracts/staking/token/LimitedTokenPool.sol | 5 - test/staking/token/LimitedTokenPool.ts | 70 ++++--- .../staking/token/LimitedTokenPoolsManager.ts | 179 +++++++++++++++++ test/staking/token/TokenPool.ts | 11 +- test/staking/token/TokenPoolsManager.ts | 190 ++++-------------- 5 files changed, 267 insertions(+), 188 deletions(-) create mode 100644 test/staking/token/LimitedTokenPoolsManager.ts diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index e1c3e75e..d8ec98f4 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -326,9 +326,4 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { else info.totalRewardsDebt += newDebt - oldDebt; stakers[user].rewardsDebt = newDebt; } - - function _isLimitsConfigValid(LimitsConfig calldata config) internal pure returns (bool) { - - return true; - } } diff --git a/test/staking/token/LimitedTokenPool.ts b/test/staking/token/LimitedTokenPool.ts index 752722f1..ce41fde8 100644 --- a/test/staking/token/LimitedTokenPool.ts +++ b/test/staking/token/LimitedTokenPool.ts @@ -43,12 +43,12 @@ describe("LimitedTokenPool", function () { limitsMultiplierToken: limitsMultiplierToken.address, profitableToken: profitableToken.address, rewardToken: profitableToken.address, - rewardTokenPrice: 1, // 1:1 ratio - interest: 0.10 * BILLION, // 10% - interestRate: D1, // 1 day }; const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { + rewardTokenPrice: BILLION, // 1:1 ratio + interest: 0.10 * BILLION, // 10% + interestRate: D1, // 1 day minDepositValue: 10, minStakeValue: 10, fastUnstakePenalty: 0.10 * BILLION, // 10% @@ -95,13 +95,13 @@ describe("LimitedTokenPool", function () { expect(config.limitsMultiplierToken).to.equal(limitsMultiplierToken.address); expect(config.profitableToken).to.equal(profitableToken.address); expect(config.rewardToken).to.equal(profitableToken.address); - expect(config.rewardTokenPrice).to.equal(1); - expect(config.interest).to.equal(0.10 * BILLION); - expect(config.interestRate).to.equal(D1); }); it("Should initialize with correct limits config", async function () { const limits = await limitedPool.getLimitsConfig(); + expect(limits.rewardTokenPrice).to.equal(BILLION); + expect(limits.interest).to.equal(0.10 * BILLION); + expect(limits.interestRate).to.equal(D1); expect(limits.minDepositValue).to.equal(10); expect(limits.minStakeValue).to.equal(10); expect(limits.fastUnstakePenalty).to.equal(0.10 * BILLION); @@ -171,9 +171,7 @@ describe("LimitedTokenPool", function () { it("Should allow staking", async function () { const stakeAmount = ethers.utils.parseEther("100"); - await expect(limitedPool.stake(stakeAmount)) - .to.emit(limitedPool, "Staked") - .withArgs(owner.address, stakeAmount); + await limitedPool.stake(stakeAmount); const info = await limitedPool.getInfo(); expect(info.totalStake).to.equal(stakeAmount); @@ -193,7 +191,12 @@ describe("LimitedTokenPool", function () { }); it("Should not allow staking above total pool limit", async function () { - await limitedPool.setMaxTotalStakeValue(ethers.utils.parseEther("100")); + const limits= await limitedPool.getLimitsConfig(); + const updatedLimits = { + ...limits, + maxTotalStakeValue: ethers.utils.parseEther("100") + }; + await limitedPool.setLimitsConfig(updatedLimits); const stakeAmount = ethers.utils.parseEther("101"); await expect(limitedPool.stake(stakeAmount)).to.be.revertedWith("Pool: max stake value exceeded"); }); @@ -224,7 +227,12 @@ describe("LimitedTokenPool", function () { }); it("Should not allow unstaking before stake lock period", async function () { - await limitedPool.setStakeLockPeriod(D1 * 2); + const limits = await limitedPool.getLimitsConfig(); + const updatedLimits = { + ...limits, + stakeLockPeriod: ethers.BigNumber.from(D1 * 2) + }; + await limitedPool.setLimitsConfig(updatedLimits); await limitedPool.stake(stakeAmount); await time.increase(D1 / 2); await expect(limitedPool.unstake(stakeAmount)).to.be.revertedWith("Stake is locked"); @@ -285,7 +293,12 @@ describe("LimitedTokenPool", function () { describe("Edge cases", function () { it("Should handle multiple deposits and stakes correctly", async function () { - await limitedPool.setStakeLockPeriod(0); // No lock period + const limits = await limitedPool.getLimitsConfig(); + const updatedLimits = { + ...limits, + stakeLockPeriod: 0, + }; + await limitedPool.setLimitsConfig(updatedLimits); const depositAmount = ethers.utils.parseEther("1000"); const stakeAmount = ethers.utils.parseEther("500"); @@ -304,7 +317,12 @@ describe("LimitedTokenPool", function () { }); it("Should handle rewards correctly after multiple stakes and unstakes", async function () { - await limitedPool.setStakeLockPeriod(0); // No lock period + const limits = await limitedPool.getLimitsConfig(); + const updatedLimits = { + ...limits, + stakeLockPeriod: 0, + }; + await limitedPool.setLimitsConfig(updatedLimits); const depositAmount = ethers.utils.parseEther("2000"); const stakeAmount = ethers.utils.parseEther("500"); @@ -425,9 +443,6 @@ describe("LimitedTokenPool", function () { limitsMultiplierToken: limitsMultiplierToken.address, profitableToken: profitableToken.address, rewardToken: profitableToken.address, - rewardTokenPrice: BILLION, - interest: 0.10 * BILLION, - interestRate: D1, }; await expect(limitedPool.initialize(rewardsBank.address, lockKeeper.address, mainConfig)) @@ -435,7 +450,12 @@ describe("LimitedTokenPool", function () { }); it("Should only allow admin to change configurations", async function () { - await expect(limitedPool.connect(user1).setRewardTokenPrice(BILLION * 2)) + const limits = await limitedPool.getLimitsConfig(); + const updatedLimits = { + ...limits, + rewardTokenPrice: BILLION * 2 + }; + await expect(limitedPool.connect(user1).setLimitsConfig(updatedLimits)) .to.be.revertedWith("AccessControl: account " + user1.address.toLowerCase() + " is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"); }); @@ -454,9 +474,6 @@ describe("LimitedTokenPool", function () { limitsMultiplierToken: ethers.constants.AddressZero, // ETH profitableToken: profitableToken.address, rewardToken: profitableToken.address, - rewardTokenPrice: BILLION, - interest: 0.10 * BILLION, - interestRate: D1, }; const ethPool = (await upgrades.deployProxy(limitedPoolFactory, [ @@ -466,6 +483,9 @@ describe("LimitedTokenPool", function () { ])) as LimitedTokenPool; await ethPool.setLimitsConfig({ + rewardTokenPrice: BILLION, + interest: 0.10 * BILLION, + interestRate: D1, minDepositValue: ethers.utils.parseEther("0.1"), minStakeValue: ethers.utils.parseEther("0.1"), fastUnstakePenalty: 0.10 * BILLION, @@ -493,9 +513,6 @@ describe("LimitedTokenPool", function () { limitsMultiplierToken: limitsMultiplierToken.address, profitableToken: ethers.constants.AddressZero, // ETH rewardToken: profitableToken.address, - rewardTokenPrice: BILLION, - interest: 0.10 * BILLION, - interestRate: D1, }; const ethPool = (await upgrades.deployProxy(limitedPoolFactory, [ @@ -505,6 +522,9 @@ describe("LimitedTokenPool", function () { ])) as LimitedTokenPool; await ethPool.setLimitsConfig({ + rewardTokenPrice: BILLION, + interest: 0.10 * BILLION, + interestRate: D1, minDepositValue: ethers.utils.parseEther("0.1"), minStakeValue: ethers.utils.parseEther("0.1"), fastUnstakePenalty: 0.10 * BILLION, @@ -520,9 +540,7 @@ describe("LimitedTokenPool", function () { await ethPool.deposit(ethers.utils.parseEther("10")); const stakeAmount = ethers.utils.parseEther("1"); - await expect(ethPool.stake(stakeAmount, { value: stakeAmount })) - .to.emit(ethPool, "Staked") - .withArgs(owner.address, stakeAmount); + await ethPool.stake(stakeAmount, { value: stakeAmount }); const info = await ethPool.getInfo(); expect(info.totalStake).to.equal(stakeAmount); diff --git a/test/staking/token/LimitedTokenPoolsManager.ts b/test/staking/token/LimitedTokenPoolsManager.ts new file mode 100644 index 00000000..d33d8341 --- /dev/null +++ b/test/staking/token/LimitedTokenPoolsManager.ts @@ -0,0 +1,179 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { ethers, upgrades } from "hardhat"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import { + LimitedTokenPoolsManager, + RewardsBank, + AirBond__factory, + LimitedTokenPool, + RewardsBank__factory, + LimitedTokenPoolsManager__factory, + LockKeeper__factory, + LockKeeper, +} from "../../../typechain-types"; + +import LimitedTokenPoolJson from "../../../artifacts/contracts/staking/token/LimitedTokenPool.sol/LimitedTokenPool.json"; + +import { expect } from "chai"; + +describe("LimitedTokenPoolsManager", function () { + let poolsManager: LimitedTokenPoolsManager; + let rewardsBank: RewardsBank; + let tokenAddr: string; + let owner: SignerWithAddress; + let lockKeeper: LockKeeper; + + async function deploy() { + const [owner] = await ethers.getSigners(); + + const rewardsBank = await new RewardsBank__factory(owner).deploy(); + const airBond = await new AirBond__factory(owner).deploy(owner.address); + + const limitedTokenPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); + const limitedTokenPoolBeacon = await upgrades.deployBeacon(limitedTokenPoolFactory); + + const lockKeeper = await new LockKeeper__factory(owner).deploy(); + + const poolsManager = await new LimitedTokenPoolsManager__factory(owner) + .deploy(rewardsBank.address, lockKeeper.address, limitedTokenPoolBeacon.address); + + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); + const tokenAddr = airBond.address; + + return { poolsManager, rewardsBank, lockKeeper, tokenAddr, owner }; + } + + beforeEach(async function () { + ({ poolsManager, rewardsBank, lockKeeper, tokenAddr, owner } = await loadFixture(deploy)); + }); + + describe("LimitedTokenPool Management", function () { + it("Should allow the owner to create a limited token pool and set limits", async function () { + const mainConfig: LimitedTokenPool.MainConfigStruct = { + name: "TestPool", + limitsMultiplierToken: tokenAddr, + profitableToken: tokenAddr, + rewardToken: tokenAddr, + }; + + const tx = await poolsManager.createPool(mainConfig); + const receipt = await tx.wait(); + const poolAddress = receipt.events![4].args![0]; + + expect(await poolsManager.pools(0)).to.equal(poolAddress); + + const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { + rewardTokenPrice: ethers.utils.parseUnits("1", 9), // 1 BILLION + interest: ethers.utils.parseUnits("0.1", 9), // 10% + interestRate: 24 * 60 * 60, // 24 hours + minDepositValue: ethers.utils.parseEther("10"), + minStakeValue: ethers.utils.parseEther("10"), + fastUnstakePenalty: ethers.utils.parseUnits("0.1", 9), // 10% + unstakeLockPeriod: 24 * 60 * 60, // 24 hours + stakeLockPeriod: 24 * 60 * 60, // 24 hours + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLimitsMultiplier: ethers.utils.parseUnits("2", 9), // 2x + }; + + await poolsManager.configurePool(poolAddress, limitsConfig); + + const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); + const updatedConfig = await proxyPool.getLimitsConfig(); + + expect(updatedConfig.rewardTokenPrice).to.equal(limitsConfig.rewardTokenPrice); + expect(updatedConfig.interest).to.equal(limitsConfig.interest); + expect(updatedConfig.interestRate).to.equal(limitsConfig.interestRate); + expect(updatedConfig.minDepositValue).to.equal(limitsConfig.minDepositValue); + expect(updatedConfig.minStakeValue).to.equal(limitsConfig.minStakeValue); + expect(updatedConfig.fastUnstakePenalty).to.equal(limitsConfig.fastUnstakePenalty); + expect(updatedConfig.unstakeLockPeriod).to.equal(limitsConfig.unstakeLockPeriod); + expect(updatedConfig.stakeLockPeriod).to.equal(limitsConfig.stakeLockPeriod); + expect(updatedConfig.maxTotalStakeValue).to.equal(limitsConfig.maxTotalStakeValue); + expect(updatedConfig.maxStakePerUserValue).to.equal(limitsConfig.maxStakePerUserValue); + expect(updatedConfig.stakeLimitsMultiplier).to.equal(limitsConfig.stakeLimitsMultiplier); + }); + + it("Should activate and deactivate a limited token pool", async function () { + const mainConfig: LimitedTokenPool.MainConfigStruct = { + name: "TestPool", + limitsMultiplierToken: tokenAddr, + profitableToken: tokenAddr, + rewardToken: tokenAddr, + }; + + const tx = await poolsManager.createPool(mainConfig); + const receipt = await tx.wait(); + console.log(receipt.events); + const poolAddress = receipt.events![4].args![0]; + + const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); + expect(await proxyPool.active()).to.equal(true); + await poolsManager.deactivatePool(poolAddress); + expect(await proxyPool.active()).to.equal(false); + await poolsManager.activatePool(poolAddress); + expect(await proxyPool.active()).to.equal(true); + }); + + it("Should allow updating limited token pool parameters", async function () { + const mainConfig: LimitedTokenPool.MainConfigStruct = { + name: "TestPool", + limitsMultiplierToken: tokenAddr, + profitableToken: tokenAddr, + rewardToken: tokenAddr, + }; + + const tx = await poolsManager.createPool(mainConfig); + const receipt = await tx.wait(); + const poolAddress = receipt.events![4].args![0]; + + const initialLimitsConfig: LimitedTokenPool.LimitsConfigStruct = { + rewardTokenPrice: ethers.utils.parseUnits("1", 9), + interest: ethers.utils.parseUnits("0.1", 9), + interestRate: 24 * 60 * 60, + minDepositValue: ethers.utils.parseEther("10"), + minStakeValue: ethers.utils.parseEther("10"), + fastUnstakePenalty: ethers.utils.parseUnits("0.1", 9), + unstakeLockPeriod: 24 * 60 * 60, + stakeLockPeriod: 24 * 60 * 60, + maxTotalStakeValue: ethers.utils.parseEther("1000000"), + maxStakePerUserValue: ethers.utils.parseEther("100000"), + stakeLimitsMultiplier: ethers.utils.parseUnits("2", 9), + }; + + await poolsManager.configurePool(poolAddress, initialLimitsConfig); + + const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); + + const newLimitsConfig: LimitedTokenPool.LimitsConfigStruct = { + rewardTokenPrice: ethers.utils.parseUnits("2", 9), + interest: ethers.utils.parseUnits("0.2", 9), + interestRate: 48 * 60 * 60, + minDepositValue: ethers.utils.parseEther("20"), + minStakeValue: ethers.utils.parseEther("20"), + fastUnstakePenalty: ethers.utils.parseUnits("0.2", 9), + unstakeLockPeriod: 48 * 60 * 60, + stakeLockPeriod: 48 * 60 * 60, + maxTotalStakeValue: ethers.utils.parseEther("2000000"), + maxStakePerUserValue: ethers.utils.parseEther("200000"), + stakeLimitsMultiplier: ethers.utils.parseUnits("3", 9), + }; + + await poolsManager.configurePool(poolAddress, newLimitsConfig); + const updatedConfig = await proxyPool.getLimitsConfig(); + + expect(updatedConfig.rewardTokenPrice).to.equal(newLimitsConfig.rewardTokenPrice); + expect(updatedConfig.interest).to.equal(newLimitsConfig.interest); + expect(updatedConfig.interestRate).to.equal(newLimitsConfig.interestRate); + expect(updatedConfig.minDepositValue).to.equal(newLimitsConfig.minDepositValue); + expect(updatedConfig.minStakeValue).to.equal(newLimitsConfig.minStakeValue); + expect(updatedConfig.fastUnstakePenalty).to.equal(newLimitsConfig.fastUnstakePenalty); + expect(updatedConfig.unstakeLockPeriod).to.equal(newLimitsConfig.unstakeLockPeriod); + expect(updatedConfig.stakeLockPeriod).to.equal(newLimitsConfig.stakeLockPeriod); + expect(updatedConfig.maxTotalStakeValue).to.equal(newLimitsConfig.maxTotalStakeValue); + expect(updatedConfig.maxStakePerUserValue).to.equal(newLimitsConfig.maxStakePerUserValue); + expect(updatedConfig.stakeLimitsMultiplier).to.equal(newLimitsConfig.stakeLimitsMultiplier); + }); + }); +}); diff --git a/test/staking/token/TokenPool.ts b/test/staking/token/TokenPool.ts index 2dcda556..d6ce8612 100644 --- a/test/staking/token/TokenPool.ts +++ b/test/staking/token/TokenPool.ts @@ -16,7 +16,7 @@ const D1 = 24 * 60 * 60; const BILLION = 1000000000; import { expect } from "chai"; -describe("SingleSidePool", function () { +describe("TokenPool", function () { let owner: SignerWithAddress; let tokenPool: TokenPool; let rewardsBank: RewardsBank; @@ -31,19 +31,22 @@ describe("SingleSidePool", function () { const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); - const tokenPoolParams: TokenPool.ConfigStruct = { + const mainConfig: TokenPool.MainConfigStruct = { token: token.address, name: "Test", + rewardToken: token.address, + }; + + const limitsConfig: TokenPool.LimitsConfigStruct = { minStakeValue: 10, fastUnstakePenalty: 0.10 * BILLION, // 10% interest: 0.10 * BILLION, // 10% interestRate: D1, // 1 day lockPeriod: D1, // 1 day - rewardToken: token.address, rewardTokenPrice: 1, }; - const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [rewardsBank.address, lockKeeper.address, tokenPoolParams])) as TokenPool; + const tokenPool = (await upgrades.deployProxy(tokenPoolFactory, [rewardsBank.address, lockKeeper.address, mainConfig, limitsConfig])) as TokenPool; await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), tokenPool.address)).wait(); await (await token.grantRole(await token.MINTER_ROLE(), owner.address)).wait(); diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index ec66f1cf..4c7e2c32 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -7,7 +7,6 @@ import { RewardsBank, AirBond__factory, TokenPool, - LimitedTokenPool, RewardsBank__factory, TokenPoolsManager__factory, LockKeeper__factory, @@ -15,8 +14,6 @@ import { } from "../../../typechain-types"; import TokenPoolJson from "../../../artifacts/contracts/staking/token/TokenPool.sol/TokenPool.json"; -import LimitedTokenPoolJson from "../../../artifacts/contracts/staking/token/LimitedTokenPool.sol/LimitedTokenPool.json"; - import { expect } from "chai"; @@ -36,13 +33,10 @@ describe("PoolsManager", function () { const tokenPoolFactory = await ethers.getContractFactory("TokenPool"); const tokenPoolBeacon = await upgrades.deployBeacon(tokenPoolFactory); - const limitedTokenPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); - const limitedTokenPoolBeacon = await upgrades.deployBeacon(limitedTokenPoolFactory); - const lockKeeper = await new LockKeeper__factory(owner).deploy(); const poolsManager = await new TokenPoolsManager__factory(owner) - .deploy(rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address, limitedTokenPoolBeacon.address); + .deploy(rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address); await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); const tokenAddr = airBond.address; @@ -56,10 +50,13 @@ describe("PoolsManager", function () { describe("TokenPool Management", function () { it("Should allow the owner to create a token pool", async function () { - const tokenPoolConfig: TokenPool.ConfigStruct = { + const mainConfig: TokenPool.MainConfigStruct = { token: tokenAddr, name: "TestPool", rewardToken: tokenAddr, + }; + + const limitsConfig: TokenPool.LimitsConfigStruct = { rewardTokenPrice: 1, minStakeValue: 10, fastUnstakePenalty: 100000, // 10% @@ -68,18 +65,21 @@ describe("PoolsManager", function () { lockPeriod: 24 * 60 * 60, // 24 hours }; - const tx = await poolsManager.createTokenPool(tokenPoolConfig); + const tx = await poolsManager.createPool(mainConfig, limitsConfig); const receipt = await tx.wait(); - const poolAddress = receipt.events![3].args![1]; + const poolAddress = receipt.events![4].args![0]; - expect(await poolsManager.getPoolAddress("TestPool")).to.equal(poolAddress); + expect(await poolsManager.pools(0)).to.equal(poolAddress); }); it("Should activate and deactivate a token pool", async function () { - const tokenPoolConfig: TokenPool.ConfigStruct = { + const mainConfig: TokenPool.MainConfigStruct = { token: tokenAddr, name: "TestPool", rewardToken: tokenAddr, + }; + + const limitsConfig: TokenPool.LimitsConfigStruct = { rewardTokenPrice: 1, minStakeValue: 10, fastUnstakePenalty: 100000, // 10% @@ -88,22 +88,26 @@ describe("PoolsManager", function () { lockPeriod: 24 * 60 * 60, // 24 hours }; - await poolsManager.createTokenPool(tokenPoolConfig); - const poolAddress = await poolsManager.getPoolAddress("TestPool"); + const tx = await poolsManager.createPool(mainConfig, limitsConfig); + const receipt = await tx.wait(); + const poolAddress = receipt.events![4].args![0]; const proxyPool = new ethers.Contract(poolAddress, TokenPoolJson.abi, owner); expect(await proxyPool.active()).to.equal(true); - await poolsManager.deactivateTokenPool("TestPool"); + await poolsManager.deactivateTokenPool(poolAddress); expect(await proxyPool.active()).to.equal(false); - await poolsManager.activateTokenPool("TestPool"); + await poolsManager.activateTokenPool(poolAddress); expect(await proxyPool.active()).to.equal(true); }); it("Should allow updating token pool parameters", async function () { - const tokenPoolConfig: TokenPool.ConfigStruct = { + const mainConfig: TokenPool.MainConfigStruct = { token: tokenAddr, name: "TestPool", rewardToken: tokenAddr, + }; + + const limitsConfig: TokenPool.LimitsConfigStruct = { rewardTokenPrice: 1, minStakeValue: 10, fastUnstakePenalty: 100000, // 10% @@ -112,149 +116,29 @@ describe("PoolsManager", function () { lockPeriod: 24 * 60 * 60, // 24 hours }; - await poolsManager.createTokenPool(tokenPoolConfig); - const poolAddress = await poolsManager.getPoolAddress("TestPool"); - const proxyPool = new ethers.Contract(poolAddress, TokenPoolJson.abi, owner); - - - await poolsManager.setInterest("TestPool", 200000, 48 * 60 * 60); - await poolsManager.setFastUnstakePenalty("TestPool", 200000); - await poolsManager.setMinStakeValue("TestPool", 20); - await poolsManager.setLockPeriod("TestPool", 48 * 60 * 60); - await poolsManager.setRewardTokenPrice("TestPool", 2); - const newConfig = await proxyPool.getConfig(); - expect(newConfig.interest).to.equal(200000); - expect(newConfig.interestRate).to.equal(48 * 60 * 60); - expect(newConfig.lockPeriod).to.equal(48 * 60 * 60); - expect(newConfig.rewardTokenPrice).to.equal(2); - expect(newConfig.fastUnstakePenalty).to.equal(200000); - expect(newConfig.minStakeValue).to.equal(20); - }); - }); - - describe("LimitedTokenPool Management", function () { - it("Should allow the owner to create a deposited token pool", async function () { - const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { - name: "TestDepositedPool", - limitsMultiplierToken: tokenAddr, - profitableToken: tokenAddr, - rewardToken: tokenAddr, - rewardTokenPrice: 1, - interest: 100000, // 10% - interestRate: 24 * 60 * 60, // 24 hours - }; - - const tx = await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); + const tx = await poolsManager.createPool(mainConfig, limitsConfig); const receipt = await tx.wait(); - const poolAddress = receipt.events![3].args![1]; - - expect(await poolsManager.getDepositedPoolAdress("TestDepositedPool")).to.equal(poolAddress); - }); - - it("Should configure deposited token pool limits", async function () { - const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { - name: "TestDepositedPool", - limitsMultiplierToken: tokenAddr, - profitableToken: tokenAddr, - rewardToken: tokenAddr, - rewardTokenPrice: 1, - interest: 100000, // 10% - interestRate: 24 * 60 * 60, // 24 hours - }; - - await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); - - const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { - minDepositValue: ethers.utils.parseEther("10"), - minStakeValue: ethers.utils.parseEther("10"), - fastUnstakePenalty: 100000, // 10% - unstakeLockPeriod: 24 * 60 * 60, // 24 hours - stakeLockPeriod: 24 * 60 * 60, // 24 hours - maxTotalStakeValue: ethers.utils.parseEther("1000000"), - maxStakePerUserValue: ethers.utils.parseEther("100000"), - stakeLimitsMultiplier: 2 * 1000000000, // 2x - }; - - await poolsManager.configureLimitedTokenPoolLimits("TestDepositedPool", limitsConfig); - - const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); - const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); - const configuredLimits = await proxyPool.getLimitsConfig(); - - expect(configuredLimits.minDepositValue).to.equal(limitsConfig.minDepositValue); - expect(configuredLimits.minStakeValue).to.equal(limitsConfig.minStakeValue); - expect(configuredLimits.fastUnstakePenalty).to.equal(limitsConfig.fastUnstakePenalty); - expect(configuredLimits.unstakeLockPeriod).to.equal(limitsConfig.unstakeLockPeriod); - expect(configuredLimits.stakeLockPeriod).to.equal(limitsConfig.stakeLockPeriod); - expect(configuredLimits.maxTotalStakeValue).to.equal(limitsConfig.maxTotalStakeValue); - expect(configuredLimits.maxStakePerUserValue).to.equal(limitsConfig.maxStakePerUserValue); - expect(configuredLimits.stakeLimitsMultiplier).to.equal(limitsConfig.stakeLimitsMultiplier); - }); - - it("Should activate and deactivate a deposited token pool", async function () { - const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { - name: "TestDepositedPool", - limitsMultiplierToken: tokenAddr, - profitableToken: tokenAddr, - rewardToken: tokenAddr, - rewardTokenPrice: 1, - interest: 100000, // 10% - interestRate: 24 * 60 * 60, // 24 hours - }; - - await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); - const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); - - const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); - expect(await proxyPool.active()).to.equal(true); - await poolsManager.deactivateLimitedTokenPool("TestDepositedPool"); - expect(await proxyPool.active()).to.equal(false); - await poolsManager.activateLimitedTokenPool("TestDepositedPool"); - expect(await proxyPool.active()).to.equal(true); - }); + const poolAddress = receipt.events![4].args![0]; + const proxyPool = new ethers.Contract(poolAddress, TokenPoolJson.abi, owner); - it("Should allow updating deposited token pool parameters", async function () { - const limitedTokenPoolConfig: LimitedTokenPool.MainConfigStruct = { - name: "TestDepositedPool", - limitsMultiplierToken: tokenAddr, - profitableToken: tokenAddr, - rewardToken: tokenAddr, - rewardTokenPrice: 1, - interest: 100000, // 10% - interestRate: 24 * 60 * 60, // 24 hours + const newLimitsConfig: TokenPool.LimitsConfigStruct = { + rewardTokenPrice: 2, + minStakeValue: 20, + fastUnstakePenalty: 200000, // 20% + interest: 200000, // 20% + interestRate: 48 * 60 * 60, // 48 hours + lockPeriod: 48 * 60 * 60, // 48 hours }; - await poolsManager.createLimitedTokenPool(limitedTokenPoolConfig); - const poolAddress = await poolsManager.getDepositedPoolAdress("TestDepositedPool"); - const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); - - await poolsManager.setRewardTokenPriceL("TestDepositedPool", 2); - await poolsManager.setInterestL("TestDepositedPool", 200000, 48 * 60 * 60); - const updatedConfig = await proxyPool.getMainConfig(); + await poolsManager.configurePool(poolAddress, newLimitsConfig); + const updatedConfig = await proxyPool.getLimitsConfig(); + expect(updatedConfig.rewardTokenPrice).to.equal(2); + expect(updatedConfig.minStakeValue).to.equal(20); + expect(updatedConfig.fastUnstakePenalty).to.equal(200000); expect(updatedConfig.interest).to.equal(200000); expect(updatedConfig.interestRate).to.equal(48 * 60 * 60); - - await poolsManager.setMinDepositValueL("TestDepositedPool", 20); - await poolsManager.setMinStakeValueL("TestDepositedPool", 30); - await poolsManager.setFastUnstakePenaltyL("TestDepositedPool", 200000); - await poolsManager.setUnstakeLockPeriodL("TestDepositedPool", 48 * 60 * 60); - await poolsManager.setStakeLockPeriodL("TestDepositedPool", 72 * 60 * 60); - await poolsManager.setMaxTotalStakeValueL("TestDepositedPool", ethers.utils.parseEther("2000000")); - await poolsManager.setMaxStakePerUserValueL("TestDepositedPool", ethers.utils.parseEther("200000")); - await poolsManager.setStakeLimitsMultiplierL("TestDepositedPool", 3 * 1000000000); - - const updatedLimits = await proxyPool.getLimitsConfig(); - expect(updatedLimits.minDepositValue).to.equal(20); - expect(updatedLimits.minStakeValue).to.equal(30); - expect(updatedLimits.fastUnstakePenalty).to.equal(200000); - expect(updatedLimits.unstakeLockPeriod).to.equal(48 * 60 * 60); - expect(updatedLimits.stakeLockPeriod).to.equal(72 * 60 * 60); - expect(updatedLimits.maxTotalStakeValue).to.equal(ethers.utils.parseEther("2000000")); - expect(updatedLimits.maxStakePerUserValue).to.equal(ethers.utils.parseEther("200000")); - expect(updatedLimits.stakeLimitsMultiplier).to.equal(3 * 1000000000); + expect(updatedConfig.lockPeriod).to.equal(48 * 60 * 60); }); }); - }); - From ad3b93bb50743b061e6a3e8e16f17ffb81c202ca Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 16:50:51 +0300 Subject: [PATCH 54/60] Add receive --- contracts/staking/token/LimitedTokenPool.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index d8ec98f4..858817b0 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -326,4 +326,6 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { else info.totalRewardsDebt += newDebt - oldDebt; stakers[user].rewardsDebt = newDebt; } + + function receive() external payable {} } From 80835f2d8f56cc7523dd02d24e194668712758d9 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 17:00:07 +0300 Subject: [PATCH 55/60] Remove unneeded getters --- contracts/staking/token/LimitedTokenPool.sol | 12 ------- test/staking/token/LimitedTokenPool.ts | 36 +++++++++---------- .../staking/token/LimitedTokenPoolsManager.ts | 4 +-- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index 858817b0..9ae559fa 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -247,18 +247,6 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { return mainConfig.name; } - function getMainConfig() public view returns (MainConfig memory) { - return mainConfig; - } - - function getLimitsConfig() public view returns (LimitsConfig memory) { - return limitsConfig; - } - - function getInfo() public view returns (Info memory) { - return info; - } - function getStaker(address user) public view returns (Staker memory) { return stakers[user]; } diff --git a/test/staking/token/LimitedTokenPool.ts b/test/staking/token/LimitedTokenPool.ts index ce41fde8..99fef783 100644 --- a/test/staking/token/LimitedTokenPool.ts +++ b/test/staking/token/LimitedTokenPool.ts @@ -90,7 +90,7 @@ describe("LimitedTokenPool", function () { describe("Initialization", function () { it("Should initialize with correct main config", async function () { - const config = await limitedPool.getMainConfig(); + const config = await limitedPool.mainConfig(); expect(config.name).to.equal("Test Deposited Pool"); expect(config.limitsMultiplierToken).to.equal(limitsMultiplierToken.address); expect(config.profitableToken).to.equal(profitableToken.address); @@ -98,7 +98,7 @@ describe("LimitedTokenPool", function () { }); it("Should initialize with correct limits config", async function () { - const limits = await limitedPool.getLimitsConfig(); + const limits = await limitedPool.limitsConfig(); expect(limits.rewardTokenPrice).to.equal(BILLION); expect(limits.interest).to.equal(0.10 * BILLION); expect(limits.interestRate).to.equal(D1); @@ -120,7 +120,7 @@ describe("LimitedTokenPool", function () { .to.emit(limitedPool, "Deposited") .withArgs(owner.address, depositAmount); - const info = await limitedPool.getInfo(); + const info = await limitedPool.info(); expect(info.totalDeposit).to.equal(depositAmount); const staker = await limitedPool.getStaker(owner.address); @@ -144,7 +144,7 @@ describe("LimitedTokenPool", function () { .to.emit(limitedPool, "Withdrawn") .withArgs(owner.address, withdrawAmount); - const info = await limitedPool.getInfo(); + const info = await limitedPool.info(); expect(info.totalDeposit).to.equal(ethers.utils.parseEther("500")); const staker = await limitedPool.getStaker(owner.address); @@ -173,7 +173,7 @@ describe("LimitedTokenPool", function () { const stakeAmount = ethers.utils.parseEther("100"); await limitedPool.stake(stakeAmount); - const info = await limitedPool.getInfo(); + const info = await limitedPool.info(); expect(info.totalStake).to.equal(stakeAmount); const staker = await limitedPool.getStaker(owner.address); @@ -191,7 +191,7 @@ describe("LimitedTokenPool", function () { }); it("Should not allow staking above total pool limit", async function () { - const limits= await limitedPool.getLimitsConfig(); + const limits= await limitedPool.limitsConfig(); const updatedLimits = { ...limits, maxTotalStakeValue: ethers.utils.parseEther("100") @@ -215,7 +215,7 @@ describe("LimitedTokenPool", function () { await expect(limitedPool.unstake(stakeAmount)) .to.emit(lockKeeper, "Locked"); - const info = await limitedPool.getInfo(); + const info = await limitedPool.info(); expect(info.totalStake).to.equal(0); const staker = await limitedPool.getStaker(owner.address); @@ -227,7 +227,7 @@ describe("LimitedTokenPool", function () { }); it("Should not allow unstaking before stake lock period", async function () { - const limits = await limitedPool.getLimitsConfig(); + const limits = await limitedPool.limitsConfig(); const updatedLimits = { ...limits, stakeLockPeriod: ethers.BigNumber.from(D1 * 2) @@ -256,7 +256,7 @@ describe("LimitedTokenPool", function () { const expectedReturn = stakeAmount.mul(90).div(100); // 90% due to 10% penalty expect(balanceAfter.sub(balanceBefore)).to.equal(expectedReturn); - const info = await limitedPool.getInfo(); + const info = await limitedPool.info(); expect(info.totalStake).to.equal(0); }); }); @@ -293,7 +293,7 @@ describe("LimitedTokenPool", function () { describe("Edge cases", function () { it("Should handle multiple deposits and stakes correctly", async function () { - const limits = await limitedPool.getLimitsConfig(); + const limits = await limitedPool.limitsConfig(); const updatedLimits = { ...limits, stakeLockPeriod: 0, @@ -307,7 +307,7 @@ describe("LimitedTokenPool", function () { await limitedPool.deposit(depositAmount); await limitedPool.stake(stakeAmount); - const info = await limitedPool.getInfo(); + const info = await limitedPool.info(); expect(info.totalDeposit).to.equal(depositAmount.mul(2)); expect(info.totalStake).to.equal(stakeAmount.mul(2)); @@ -317,7 +317,7 @@ describe("LimitedTokenPool", function () { }); it("Should handle rewards correctly after multiple stakes and unstakes", async function () { - const limits = await limitedPool.getLimitsConfig(); + const limits = await limitedPool.limitsConfig(); const updatedLimits = { ...limits, stakeLockPeriod: 0, @@ -369,11 +369,11 @@ describe("LimitedTokenPool", function () { it("Should not add interest if called too frequently", async function () { await limitedPool.onBlock(); - const infoBefore = await limitedPool.getInfo(); + const infoBefore = await limitedPool.info(); await time.increase(D1 / 2); // Half a day await limitedPool.onBlock(); - const infoAfter = await limitedPool.getInfo(); + const infoAfter = await limitedPool.info(); expect(infoAfter.totalRewards).to.equal(infoBefore.totalRewards); }); @@ -407,7 +407,7 @@ describe("LimitedTokenPool", function () { expect(infoUser2.deposit).to.equal(ethers.utils.parseEther("300")); expect(infoUser2.stake).to.equal(ethers.utils.parseEther("100")); - const poolInfo = await limitedPool.getInfo(); + const poolInfo = await limitedPool.info(); expect(poolInfo.totalDeposit).to.equal(ethers.utils.parseEther("800")); expect(poolInfo.totalStake).to.equal(ethers.utils.parseEther("300")); }); @@ -450,7 +450,7 @@ describe("LimitedTokenPool", function () { }); it("Should only allow admin to change configurations", async function () { - const limits = await limitedPool.getLimitsConfig(); + const limits = await limitedPool.limitsConfig(); const updatedLimits = { ...limits, rewardTokenPrice: BILLION * 2 @@ -501,7 +501,7 @@ describe("LimitedTokenPool", function () { .to.emit(ethPool, "Deposited") .withArgs(owner.address, depositAmount); - const info = await ethPool.getInfo(); + const info = await ethPool.info(); expect(info.totalDeposit).to.equal(depositAmount); }); @@ -542,7 +542,7 @@ describe("LimitedTokenPool", function () { const stakeAmount = ethers.utils.parseEther("1"); await ethPool.stake(stakeAmount, { value: stakeAmount }); - const info = await ethPool.getInfo(); + const info = await ethPool.info(); expect(info.totalStake).to.equal(stakeAmount); }); }); diff --git a/test/staking/token/LimitedTokenPoolsManager.ts b/test/staking/token/LimitedTokenPoolsManager.ts index d33d8341..861a5014 100644 --- a/test/staking/token/LimitedTokenPoolsManager.ts +++ b/test/staking/token/LimitedTokenPoolsManager.ts @@ -80,7 +80,7 @@ describe("LimitedTokenPoolsManager", function () { await poolsManager.configurePool(poolAddress, limitsConfig); const proxyPool = new ethers.Contract(poolAddress, LimitedTokenPoolJson.abi, owner); - const updatedConfig = await proxyPool.getLimitsConfig(); + const updatedConfig = await proxyPool.limitsConfig(); expect(updatedConfig.rewardTokenPrice).to.equal(limitsConfig.rewardTokenPrice); expect(updatedConfig.interest).to.equal(limitsConfig.interest); @@ -161,7 +161,7 @@ describe("LimitedTokenPoolsManager", function () { }; await poolsManager.configurePool(poolAddress, newLimitsConfig); - const updatedConfig = await proxyPool.getLimitsConfig(); + const updatedConfig = await proxyPool.limitsConfig(); expect(updatedConfig.rewardTokenPrice).to.equal(newLimitsConfig.rewardTokenPrice); expect(updatedConfig.interest).to.equal(newLimitsConfig.interest); From c2119f857270618164be1be2a3a1a1ed313edfcf Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 19:15:09 +0300 Subject: [PATCH 56/60] Update --- contracts/staking/token/LimitedTokenPool.sol | 14 +++++--- contracts/staking/token/TokenPool.sol | 34 ++---------------- test/staking/token/LimitedTokenPool.ts | 37 +++++++++++--------- 3 files changed, 31 insertions(+), 54 deletions(-) diff --git a/contracts/staking/token/LimitedTokenPool.sol b/contracts/staking/token/LimitedTokenPool.sol index 9ae559fa..2ad0fdea 100644 --- a/contracts/staking/token/LimitedTokenPool.sol +++ b/contracts/staking/token/LimitedTokenPool.sol @@ -56,11 +56,11 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { LockKeeper public lockKeeper; RewardsBank public rewardsBank; - MainConfig public mainConfig; - LimitsConfig public limitsConfig; + MainConfig public mainConfig; // immutable + LimitsConfig public limitsConfig; // mutable Info public info; - mapping(address => Staker) public stakers; + mapping(address => Staker) private stakers; //EVENTS @@ -247,8 +247,12 @@ contract LimitedTokenPool is Initializable, AccessControl, IOnBlockListener { return mainConfig.name; } - function getStaker(address user) public view returns (Staker memory) { - return stakers[user]; + function getStake(address user) public view returns (uint) { + return stakers[user].stake; + } + + function getDeposit(address user) public view returns (uint) { + return stakers[user].deposit; } function getUserRewards(address user) public view returns (uint) { diff --git a/contracts/staking/token/TokenPool.sol b/contracts/staking/token/TokenPool.sol index 3d0c987b..3e987bd6 100644 --- a/contracts/staking/token/TokenPool.sol +++ b/contracts/staking/token/TokenPool.sol @@ -49,7 +49,7 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { LimitsConfig public limitsConfig; // mutable Info public info; - mapping(address => Staker) public stakers; + mapping(address => Staker) private stakers; //EVENTS @@ -63,7 +63,6 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { event UnstakeFast(address indexed user, uint amount, uint penalty); function initialize(RewardsBank bank_, LockKeeper keeper_, MainConfig calldata mainConfig_, LimitsConfig calldata limitsConfig_) public initializer { - //TODO: Should validate input params rewardsBank = bank_; lockKeeper = keeper_; mainConfig = mainConfig_; @@ -123,7 +122,7 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { // lock funds stakers[msg.sender].lockedWithdrawal = lockKeeper.lockSingle( msg.sender, address(mainConfig.token), uint64(block.timestamp + limitsConfig.lockPeriod), amount + canceledAmount, - string(abi.encodePacked("TokenStaking unstake: ", _addressToString(address(mainConfig.token)))) + string(abi.encodePacked("TokenStaking unstake")) ); _claimRewards(msg.sender); @@ -157,18 +156,6 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { // VIEW METHODS - function getMainConfig() public view returns (MainConfig memory) { - return mainConfig; - } - - function getLimitsConfig() public view returns (LimitsConfig memory) { - return limitsConfig; - } - - function getInfo() public view returns (Info memory) { - return info; - } - function getStake(address user) public view returns (uint) { return stakers[user].stake; } @@ -245,21 +232,4 @@ contract TokenPool is Initializable, AccessControl, IOnBlockListener { if (info.totalStake == 0 && info.totalRewards == 0) return amount; return amount * info.totalRewards /info.totalStake; } - - function _addressToString(address x) internal pure returns (string memory) { - bytes memory s = new bytes(40); - for (uint i = 0; i < 20; i++) { - uint8 b = uint8(uint(uint160(x)) / (2 ** (8 * (19 - i)))); - uint8 hi = (b / 16); - uint8 lo = (b - 16 * hi); - s[2 * i] = _char(hi); - s[2 * i + 1] = _char(lo); - } - return string(s); - } - - function _char(uint8 b) internal pure returns (bytes1 c) { - return bytes1(b + (b < 10 ? 0x30 : 0x57)); - } - } diff --git a/test/staking/token/LimitedTokenPool.ts b/test/staking/token/LimitedTokenPool.ts index 99fef783..5b3a43b9 100644 --- a/test/staking/token/LimitedTokenPool.ts +++ b/test/staking/token/LimitedTokenPool.ts @@ -123,8 +123,8 @@ describe("LimitedTokenPool", function () { const info = await limitedPool.info(); expect(info.totalDeposit).to.equal(depositAmount); - const staker = await limitedPool.getStaker(owner.address); - expect(staker.deposit).to.equal(depositAmount); + const deposit = await limitedPool.getDeposit(owner.address); + expect(deposit).to.equal(depositAmount); }); it("Should not allow deposit below minimum", async function () { @@ -147,8 +147,8 @@ describe("LimitedTokenPool", function () { const info = await limitedPool.info(); expect(info.totalDeposit).to.equal(ethers.utils.parseEther("500")); - const staker = await limitedPool.getStaker(owner.address); - expect(staker.deposit).to.equal(ethers.utils.parseEther("500")); + const deposit = await limitedPool.getDeposit(owner.address); + expect(deposit).to.equal(ethers.utils.parseEther("500")); }); it("Should not allow withdrawal more than deposited", async function () { @@ -176,8 +176,8 @@ describe("LimitedTokenPool", function () { const info = await limitedPool.info(); expect(info.totalStake).to.equal(stakeAmount); - const staker = await limitedPool.getStaker(owner.address); - expect(staker.stake).to.equal(stakeAmount); + const stake = await limitedPool.getStake(owner.address); + expect(stake).to.equal(stakeAmount); }); it("Should not allow staking below minimum", async function () { @@ -218,8 +218,8 @@ describe("LimitedTokenPool", function () { const info = await limitedPool.info(); expect(info.totalStake).to.equal(0); - const staker = await limitedPool.getStaker(owner.address); - expect(staker.stake).to.equal(0); + const stake = await limitedPool.getStake(owner.address); + expect(stake).to.equal(0); }); it("Should not allow unstaking more than staked", async function () { @@ -311,9 +311,10 @@ describe("LimitedTokenPool", function () { expect(info.totalDeposit).to.equal(depositAmount.mul(2)); expect(info.totalStake).to.equal(stakeAmount.mul(2)); - const staker = await limitedPool.getStaker(owner.address); - expect(staker.deposit).to.equal(depositAmount.mul(2)); - expect(staker.stake).to.equal(stakeAmount.mul(2)); + const deposit = await limitedPool.getDeposit(owner.address); + const stake = await limitedPool.getStake(owner.address); + expect(deposit).to.equal(depositAmount.mul(2)); + expect(stake).to.equal(stakeAmount.mul(2)); }); it("Should handle rewards correctly after multiple stakes and unstakes", async function () { @@ -399,13 +400,15 @@ describe("LimitedTokenPool", function () { await limitedPool.connect(user1).stake(ethers.utils.parseEther("200")); await limitedPool.connect(user2).stake(ethers.utils.parseEther("100")); - const infoUser1 = await limitedPool.getStaker(user1.address); - const infoUser2 = await limitedPool.getStaker(user2.address); + const depositUser1 = await limitedPool.getDeposit(user1.address); + const stakeUser1 = await limitedPool.getStake(user1.address); + const depositUser2 = await limitedPool.getDeposit(user2.address); + const stakeUser2 = await limitedPool.getStake(user2.address); - expect(infoUser1.deposit).to.equal(ethers.utils.parseEther("500")); - expect(infoUser1.stake).to.equal(ethers.utils.parseEther("200")); - expect(infoUser2.deposit).to.equal(ethers.utils.parseEther("300")); - expect(infoUser2.stake).to.equal(ethers.utils.parseEther("100")); + expect(depositUser1).to.equal(ethers.utils.parseEther("500")); + expect(stakeUser1).to.equal(ethers.utils.parseEther("200")); + expect(depositUser2).to.equal(ethers.utils.parseEther("300")); + expect(stakeUser2).to.equal(ethers.utils.parseEther("100")); const poolInfo = await limitedPool.info(); expect(poolInfo.totalDeposit).to.equal(ethers.utils.parseEther("800")); From 46aa1aab930fcc654c2cd68a30d3d8df293277db Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 19:34:14 +0300 Subject: [PATCH 57/60] fix test --- test/staking/token/TokenPoolsManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/staking/token/TokenPoolsManager.ts b/test/staking/token/TokenPoolsManager.ts index 4c7e2c32..3d7332b1 100644 --- a/test/staking/token/TokenPoolsManager.ts +++ b/test/staking/token/TokenPoolsManager.ts @@ -131,7 +131,7 @@ describe("PoolsManager", function () { }; await poolsManager.configurePool(poolAddress, newLimitsConfig); - const updatedConfig = await proxyPool.getLimitsConfig(); + const updatedConfig = await proxyPool.limitsConfig(); expect(updatedConfig.rewardTokenPrice).to.equal(2); expect(updatedConfig.minStakeValue).to.equal(20); From 1e691d79dee0556330b73bb6c11364a3940f76a7 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Mon, 7 Oct 2024 20:25:56 +0300 Subject: [PATCH 58/60] Update --- .../token/LimitedTokenPoolsManager.sol | 13 +-- contracts/staking/token/TokenPoolsManager.sol | 2 +- .../token_staking/deploy_limited_manager.ts | 81 +++++++++++++++++++ .../{deploy.ts => deploy_token_manager.ts} | 8 +- src/contracts/names.ts | 8 +- 5 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 scripts/ecosystem/token_staking/deploy_limited_manager.ts rename scripts/ecosystem/token_staking/{deploy.ts => deploy_token_manager.ts} (87%) diff --git a/contracts/staking/token/LimitedTokenPoolsManager.sol b/contracts/staking/token/LimitedTokenPoolsManager.sol index 0b270a8b..98b81d18 100644 --- a/contracts/staking/token/LimitedTokenPoolsManager.sol +++ b/contracts/staking/token/LimitedTokenPoolsManager.sol @@ -8,17 +8,18 @@ import "./LimitedTokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -contract LimitedTokenPoolsManager is Ownable { +contract LimitedTokenPoolsManager is AccessControl { LockKeeper lockKeeper; RewardsBank public bank; UpgradeableBeacon public limitedTokenPoolBeacon; address[] public pools; - constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon doubleSideBeacon_) Ownable() { + constructor(RewardsBank bank_, LockKeeper lockKeeper_, UpgradeableBeacon doubleSideBeacon_) { lockKeeper = lockKeeper_; bank = bank_; limitedTokenPoolBeacon = doubleSideBeacon_; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } event LimitedPoolCreated(address pool); @@ -27,7 +28,7 @@ contract LimitedTokenPoolsManager is Ownable { event LimitedPoolActivated(address pool); // LIMITED POOL METHODS - function createPool(LimitedTokenPool.MainConfig calldata params) public onlyOwner returns (address) { + function createPool(LimitedTokenPool.MainConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) returns (address) { bytes memory data = abi.encodeWithSignature( "initialize(address,address,(string,address,address,address))", bank, lockKeeper, params); @@ -38,21 +39,21 @@ contract LimitedTokenPoolsManager is Ownable { return pool; } - function configurePool(address _pool, LimitedTokenPool.LimitsConfig calldata params) public onlyOwner { + function configurePool(address _pool, LimitedTokenPool.LimitsConfig calldata params) public onlyRole(DEFAULT_ADMIN_ROLE) { require(_isPool(_pool),"Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(_pool); pool.setLimitsConfig(params); emit LimitedPoolConfigured(_pool, params); } - function deactivatePool(address _pool) public onlyOwner { + function deactivatePool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(_isPool(_pool),"Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(_pool); pool.deactivate(); emit LimitedPoolDeactivated(_pool); } - function activatePool(address _pool) public onlyOwner { + function activatePool(address _pool) public onlyRole(DEFAULT_ADMIN_ROLE) { require(_isPool(_pool),"Pool does not exist"); LimitedTokenPool pool = LimitedTokenPool(_pool); pool.activate(); diff --git a/contracts/staking/token/TokenPoolsManager.sol b/contracts/staking/token/TokenPoolsManager.sol index 0031405c..0fde8aae 100644 --- a/contracts/staking/token/TokenPoolsManager.sol +++ b/contracts/staking/token/TokenPoolsManager.sol @@ -8,7 +8,7 @@ import "./TokenPool.sol"; import "../../funds/RewardsBank.sol"; import "../../LockKeeper.sol"; -contract TokenPoolsManager is AccessControl{ +contract TokenPoolsManager is AccessControl { LockKeeper lockKeeper; RewardsBank public bank; UpgradeableBeacon public beacon; diff --git a/scripts/ecosystem/token_staking/deploy_limited_manager.ts b/scripts/ecosystem/token_staking/deploy_limited_manager.ts new file mode 100644 index 00000000..acdee291 --- /dev/null +++ b/scripts/ecosystem/token_staking/deploy_limited_manager.ts @@ -0,0 +1,81 @@ +import { ethers, upgrades } from "hardhat"; +import { deploy } from "@airdao/deployments/deploying"; + +import { ContractNames } from "../../../src"; + +import { + LimitedTokenPoolsManager__factory, + RewardsBank__factory, + LockKeeper__factory +} from "../../../typechain-types"; + +import { wrapProviderToError } from "../../../src/utils/AmbErrorProvider"; +import { deployMultisig } from "../../utils/deployMultisig"; + +export async function main() { + const { chainId } = await ethers.provider.getNetwork(); + + const [deployer] = await ethers.getSigners(); + wrapProviderToError(deployer.provider!); + + const multisig = await deployMultisig(ContractNames.Ecosystem_LimitedTokenPoolsManager, deployer); + + const rewardsBank = await deploy({ + contractName: ContractNames.Ecosystem_LimitedTokenPoolsManagerRewardsBank, + artifactName: "RewardsBank", + deployArgs: [], + signer: deployer, + loadIfAlreadyDeployed: true, + }); + + const lockKeeper = await deploy({ + contractName: ContractNames.LockKeeper, + artifactName: "LockKeeper", + deployArgs: [], + signer: deployer, + loadIfAlreadyDeployed: true, + isUpgradeableProxy: true, + }); + + console.log("deploying LimitedTokenPool Beacon"); + const limitedTokenPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); + const limitedTokenPoolBeacon = await upgrades.deployBeacon(limitedTokenPoolFactory); + await limitedTokenPoolBeacon.deployed(); + console.log("LimitedTokenPool Beacon deployed to:", limitedTokenPoolBeacon.address); + + console.log("deploying TokenPoolsManager"); + const poolsManager = await deploy({ + contractName: ContractNames.Ecosystem_TokenPoolsManager, + artifactName: "TokenPoolsManager", + deployArgs: [rewardsBank.address, lockKeeper.address, limitedTokenPoolBeacon.address], + signer: deployer, + loadIfAlreadyDeployed: true, + }); + + console.log("Grant poolsManager rewardsBank admin roles"); + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), poolsManager.address)).wait(); + + console.log("Grant multisig rewardsBank admin role"); + await (await rewardsBank.grantRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); + + console.log("Grant multisig poolsManager admin role"); + await (await poolsManager.grantRole(await poolsManager.DEFAULT_ADMIN_ROLE(), multisig.address)).wait(); + + if (chainId != 16718) return; // continue only on prod + + console.log("Revoking roles from deployer"); + + console.log("Revoke rewardsBank admin role from deployer"); + await (await rewardsBank.revokeRole(await rewardsBank.DEFAULT_ADMIN_ROLE(), deployer.address)).wait(); + + console.log("Revoke poolsManager admin role from deployer"); + await (await poolsManager.revokeRole(await poolsManager.DEFAULT_ADMIN_ROLE(), deployer.address)).wait(); + +} + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/scripts/ecosystem/token_staking/deploy.ts b/scripts/ecosystem/token_staking/deploy_token_manager.ts similarity index 87% rename from scripts/ecosystem/token_staking/deploy.ts rename to scripts/ecosystem/token_staking/deploy_token_manager.ts index 3a5b0961..47ecaebc 100644 --- a/scripts/ecosystem/token_staking/deploy.ts +++ b/scripts/ecosystem/token_staking/deploy_token_manager.ts @@ -44,17 +44,11 @@ export async function main() { await tokenPoolBeacon.deployed(); console.log("TokenPool Beacon deployed to:", tokenPoolBeacon.address); - console.log("deploying LimitedTokenPool Beacon"); - const limitedTokenPoolFactory = await ethers.getContractFactory("LimitedTokenPool"); - const limitedTokenPoolBeacon = await upgrades.deployBeacon(limitedTokenPoolFactory); - await limitedTokenPoolBeacon.deployed(); - console.log("LimitedTokenPool Beacon deployed to:", limitedTokenPoolBeacon.address); - console.log("deploying TokenPoolsManager"); const poolsManager = await deploy({ contractName: ContractNames.Ecosystem_TokenPoolsManager, artifactName: "TokenPoolsManager", - deployArgs: [rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address, limitedTokenPoolBeacon.address], + deployArgs: [rewardsBank.address, lockKeeper.address, tokenPoolBeacon.address], signer: deployer, loadIfAlreadyDeployed: true, }); diff --git a/src/contracts/names.ts b/src/contracts/names.ts index e8ccb18d..7095d59e 100644 --- a/src/contracts/names.ts +++ b/src/contracts/names.ts @@ -86,13 +86,12 @@ export enum ContractNames { Ecosystem_GovernmentMultisig = "Ecosystem_Government_Multisig", - Ecosystem_SingleSidePool = "Ecosystem_SingleSidePool", - Ecosystem_SingleSidePoolBeacon = "Ecosystem_SingleSidePool_Beacon", - Ecosystem_DoubleSidePool = "Ecosystem_DoubleSidePool", - Ecosystem_DoubleSidePoolBeacon = "Ecosystem_DoubleSidePool_Beacon", Ecosystem_TokenPoolsManager = "Ecosystem_TokenPoolsManager", Ecosystem_TokenPoolsManagerMultisig = "Ecosystem_TokenPoolsManager_Multisig", Ecosystem_TokenPoolsManagerRewardsBank = "Ecosystem_TokenPoolsManager_RewardsBank", + Ecosystem_LimitedTokenPoolsManager = "Ecosystem_LimitedTokenPoolsManager", + Ecosystem_LimitedTokenPoolsManagerMultisig = "Ecosystem_LimitedTokenPoolsManager_Multisig", + Ecosystem_LimitedTokenPoolsManagerRewardsBank = "Ecosystem_LimitedTokenPoolsManager_RewardsBank", Ecosystem_HBRToken = "Ecosystem_HBRToken", } @@ -130,6 +129,7 @@ export const MULTISIGS_ECOSYSTEM = { [ContractNames.Ecosystem_LiquidPoolStAMB]: ContractNames.Ecosystem_LiquidPoolMultisig, [ContractNames.Ecosystem_LiquidPoolStakingTiers]: ContractNames.Ecosystem_LiquidPoolMultisig, [ContractNames.Ecosystem_TokenPoolsManager]: ContractNames.Ecosystem_TokenPoolsManagerMultisig, + [ContractNames.Ecosystem_LimitedTokenPoolsManagerMultisig]: ContractNames.Ecosystem_LimitedTokenPoolsManagerMultisig, }; export const MULTISIGS = {...MULTISIGS_COMMON, ...MULTISIGS_ECOSYSTEM}; From f9394e04171d478bfebdb4194dea4f1e49b999d7 Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Tue, 8 Oct 2024 13:11:15 +0300 Subject: [PATCH 59/60] Deploy limited pools manager on devnet --- .openzeppelin/unknown-30746.json | 334 ++++++++++++++++++ deployments/30746.json | 132 +++---- package.json | 3 +- .../ecosystem/token_staking/create_hbr_amb.ts | 10 +- 4 files changed, 395 insertions(+), 84 deletions(-) diff --git a/.openzeppelin/unknown-30746.json b/.openzeppelin/unknown-30746.json index 0d3620ef..5073ee88 100644 --- a/.openzeppelin/unknown-30746.json +++ b/.openzeppelin/unknown-30746.json @@ -1875,6 +1875,340 @@ } } } + }, + "bc0d75db77e1e7d25640bcf4afb1f6a377798bae1949d197287647b310934a42": { + "address": "0xd23D3E4E8F52AD5E48247a13d988919a0f7F0247", + "txHash": "0xdfdb4e59b583d9726f90b6ab4f6cdd6bdf037eab76b3212508383c3bc5719e64", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "_roles", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:56" + }, + { + "label": "active", + "offset": 0, + "slot": "2", + "type": "t_bool", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:54" + }, + { + "label": "lockKeeper", + "offset": 1, + "slot": "2", + "type": "t_contract(LockKeeper)7997", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:56" + }, + { + "label": "rewardsBank", + "offset": 0, + "slot": "3", + "type": "t_contract(RewardsBank)10670", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:57" + }, + { + "label": "mainConfig", + "offset": 0, + "slot": "4", + "type": "t_struct(MainConfig)17722_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:59" + }, + { + "label": "limitsConfig", + "offset": 0, + "slot": "8", + "type": "t_struct(LimitsConfig)17745_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:60" + }, + { + "label": "info", + "offset": 0, + "slot": "19", + "type": "t_struct(Info)17756_storage", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:61" + }, + { + "label": "stakers", + "offset": 0, + "slot": "24", + "type": "t_mapping(t_address,t_struct(Staker)17769_storage)", + "contract": "LimitedTokenPool", + "src": "contracts/staking/token/LimitedTokenPool.sol:63" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(LockKeeper)7997": { + "label": "contract LockKeeper", + "numberOfBytes": "20" + }, + "t_contract(RewardsBank)10670": { + "label": "contract RewardsBank", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Staker)17769_storage)": { + "label": "mapping(address => struct LimitedTokenPool.Staker)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)3267_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Info)17756_storage": { + "label": "struct LimitedTokenPool.Info", + "members": [ + { + "label": "totalStake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "totalDeposit", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "totalRewards", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "lastInterestUpdate", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "totalRewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(LimitsConfig)17745_storage": { + "label": "struct LimitedTokenPool.LimitsConfig", + "members": [ + { + "label": "rewardTokenPrice", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "interest", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "interestRate", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "minDepositValue", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "minStakeValue", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "fastUnstakePenalty", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "unstakeLockPeriod", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "stakeLockPeriod", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "maxTotalStakeValue", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "maxStakePerUserValue", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "stakeLimitsMultiplier", + "type": "t_uint256", + "offset": 0, + "slot": "10" + } + ], + "numberOfBytes": "352" + }, + "t_struct(MainConfig)17722_storage": { + "label": "struct LimitedTokenPool.MainConfig", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "limitsMultiplierToken", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "profitableToken", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "rewardToken", + "type": "t_address", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(RoleData)3267_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Staker)17769_storage": { + "label": "struct LimitedTokenPool.Staker", + "members": [ + { + "label": "stake", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "deposit", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "rewardsDebt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "claimableRewards", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "lockedWithdrawal", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "stakedAt", + "type": "t_uint256", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/deployments/30746.json b/deployments/30746.json index 5399b855..9d8631fa 100644 --- a/deployments/30746.json +++ b/deployments/30746.json @@ -1662,8 +1662,42 @@ "fullyQualifiedName": "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy" } }, - "Ecosystem_TokenPoolsManager_Multisig": { - "address": "0x724AfcA5194639475094d6a67c4c47d52D6CeC63", + "Ecosystem_HBRToken": { + "address": "0x7b6B4cc8704EC9533F69E46682c9aD010F30F0C9", + "abi": [ + "constructor(address admin)", + "event Approval(address indexed owner, address indexed spender, uint256 value)", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "event Transfer(address indexed from, address indexed to, uint256 value)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function MINTER_ROLE() view returns (bytes32)", + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", + "function balanceOf(address account) view returns (uint256)", + "function burn(address account, uint256 amount)", + "function decimals() view returns (uint8)", + "function decreaseAllowance(address spender, uint256 subtractedValue) returns (bool)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function grantRole(bytes32 role, address account)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function increaseAllowance(address spender, uint256 addedValue) returns (bool)", + "function mint(address account, uint256 amount)", + "function name() view returns (string)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function symbol() view returns (string)", + "function totalSupply() view returns (uint256)", + "function transfer(address to, uint256 amount) returns (bool)", + "function transferFrom(address from, address to, uint256 amount) returns (bool)" + ], + "deployTx": "0xdda8f77783e2347249ba80546a99665569b58c1399a175d8fa9d3b1c0153485c", + "fullyQualifiedName": "contracts/staking/token/HBRToken.sol:HBRToken" + }, + "Ecosystem_LimitedTokenPoolsManager": { + "address": "0xB68Cb4770225B20E1967a2E2FEAf82da56568503", "abi": [ "constructor(address[] _signers, bool[] isInitiatorFlags, uint256 _threshold, address owner)", "event Confirmation(address indexed sender, uint256 indexed txId)", @@ -1699,11 +1733,11 @@ "function transferOwnership(address newOwner)", "function withdraw(address to, uint256 amount)" ], - "deployTx": "0x67d093a2dc5c8ca61dd8996ee8a41efb6b1c84c7ecd8f8523ad4647a67238ac8", + "deployTx": "0xf684813eabc73c9c55280cb13dd8e0a39896b970c6f961e001743ae19457b10e", "fullyQualifiedName": "contracts/multisig/Multisig.sol:Multisig" }, - "Ecosystem_TokenPoolsManager_RewardsBank": { - "address": "0xCCd30d8848ac1559a8215B556fB52C2dd12c7516", + "Ecosystem_LimitedTokenPoolsManager_RewardsBank": { + "address": "0x47111CEAc3a13E7e8C17eFeA0112b10e51D02efe", "abi": [ "constructor()", "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", @@ -1719,94 +1753,36 @@ "function withdrawAmb(address addressTo, uint256 amount)", "function withdrawErc20(address tokenAddress, address addressTo, uint256 amount)" ], - "deployTx": "0xe81a1e26055f7b0ae2127089697c7825d459fa22a3678f27a116555403dfccd9", + "deployTx": "0xc130fcca18a397e3c3374c9914289d7a6a9a0b4a0891e199a34165a45eb873af", "fullyQualifiedName": "contracts/funds/RewardsBank.sol:RewardsBank" }, "Ecosystem_TokenPoolsManager": { - "address": "0xe684E41da338363C2777c0097356752d6f24BDae", + "address": "0xF954533519A87Fa88da6B4CeF5d84Ea4a3aEdaDe", "abi": [ - "constructor(address bank_, address lockKeeper_, address singleSideBeacon_, address doubleSideBeacon_)", - "event DepositedPoolActivated(string name)", - "event DepositedPoolCreated(string name, address pool)", - "event DepositedPoolDeactivated(string name)", - "event PoolActivated(string name)", - "event PoolCreated(string name, address pool)", - "event PoolDeactivated(string name)", + "constructor(address bank_, address lockKeeper_, address _beacon)", + "event PoolActivated(address pool)", + "event PoolConfigured(address pool, tuple(uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) params)", + "event PoolCreated(address pool)", + "event PoolDeactivated(address pool)", "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", - "function activateLimitedTokenPool(string _pool)", - "function activateTokenPool(string _pool)", + "function activateTokenPool(address _pool)", "function bank() view returns (address)", - "function configureLimitedTokenPoolLimits(string name, tuple(uint256 minDepositValue, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 unstakeLockPeriod, uint256 stakeLockPeriod, uint256 maxTotalStakeValue, uint256 maxStakePerUserValue, uint256 stakeLimitsMultiplier) params)", - "function createLimitedTokenPool(tuple(string name, address limitsMultiplierToken, address profitableToken, address rewardToken, uint256 rewardTokenPrice, uint256 interest, uint256 interestRate) params) returns (address)", - "function createTokenPool(tuple(address token, string name, address rewardToken, uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) params) returns (address)", - "function deactivateLimitedTokenPool(string _pool)", - "function deactivateTokenPool(string _pool)", - "function depositedPools(string) view returns (address)", - "function depositedTokenPoolBeacon() view returns (address)", - "function getLimitedTokenPoolAdress(string name) view returns (address)", + "function beacon() view returns (address)", + "function configurePool(address pool, tuple(uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) limitsConfig)", + "function createPool(tuple(address token, string name, address rewardToken) mainConfig, tuple(uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) limitsConfig) returns (address)", + "function deactivateTokenPool(address _pool)", "function getRoleAdmin(bytes32 role) view returns (bytes32)", - "function getTokenPoolAddress(string name) view returns (address)", "function grantRole(bytes32 role, address account)", "function hasRole(bytes32 role, address account) view returns (bool)", - "function pools(string) view returns (address)", + "function pools(uint256) view returns (address)", "function renounceRole(bytes32 role, address account)", "function revokeRole(bytes32 role, address account)", - "function setFastUnstakePenalty(string _pool, uint256 penalty)", - "function setFastUnstakePenaltyL(string _pool, uint256 penalty)", - "function setInterest(string _pool, uint256 _interest, uint256 _interestRate)", - "function setInterestL(string _pool, uint256 _interest, uint256 _interestRate)", - "function setLockPeriod(string _pool, uint256 period)", - "function setMaxStakePerUserValueL(string _pool, uint256 value)", - "function setMaxTotalStakeValueL(string _pool, uint256 value)", - "function setMinDepositValueL(string _pool, uint256 value)", - "function setMinStakeValue(string _pool, uint256 value)", - "function setMinStakeValueL(string _pool, uint256 value)", - "function setRewardTokenPrice(string _pool, uint256 price)", - "function setRewardTokenPriceL(string _pool, uint256 price)", - "function setStakeLimitsMultiplierL(string _pool, uint256 value)", - "function setStakeLockPeriodL(string _pool, uint256 period)", - "function setUnstakeLockPeriodL(string _pool, uint256 period)", - "function supportsInterface(bytes4 interfaceId) view returns (bool)", - "function tokenPoolBeacon() view returns (address)" + "function supportsInterface(bytes4 interfaceId) view returns (bool)" ], - "deployTx": "0xa3d88b934c95d3df29c2dc19537a6449cc5ebce303eaf50a2cb6860775f43496", + "deployTx": "0x14eba9d4f3d2694bc5cf56f5ef854430ad984f925afceea9d95dd0943d2edbe5", "fullyQualifiedName": "contracts/staking/token/TokenPoolsManager.sol:TokenPoolsManager" - }, - "Ecosystem_HBRToken": { - "address": "0x7b6B4cc8704EC9533F69E46682c9aD010F30F0C9", - "abi": [ - "constructor(address admin)", - "event Approval(address indexed owner, address indexed spender, uint256 value)", - "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", - "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", - "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", - "event Transfer(address indexed from, address indexed to, uint256 value)", - "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", - "function MINTER_ROLE() view returns (bytes32)", - "function allowance(address owner, address spender) view returns (uint256)", - "function approve(address spender, uint256 amount) returns (bool)", - "function balanceOf(address account) view returns (uint256)", - "function burn(address account, uint256 amount)", - "function decimals() view returns (uint8)", - "function decreaseAllowance(address spender, uint256 subtractedValue) returns (bool)", - "function getRoleAdmin(bytes32 role) view returns (bytes32)", - "function grantRole(bytes32 role, address account)", - "function hasRole(bytes32 role, address account) view returns (bool)", - "function increaseAllowance(address spender, uint256 addedValue) returns (bool)", - "function mint(address account, uint256 amount)", - "function name() view returns (string)", - "function renounceRole(bytes32 role, address account)", - "function revokeRole(bytes32 role, address account)", - "function supportsInterface(bytes4 interfaceId) view returns (bool)", - "function symbol() view returns (string)", - "function totalSupply() view returns (uint256)", - "function transfer(address to, uint256 amount) returns (bool)", - "function transferFrom(address from, address to, uint256 amount) returns (bool)" - ], - "deployTx": "0xdda8f77783e2347249ba80546a99665569b58c1399a175d8fa9d3b1c0153485c", - "fullyQualifiedName": "contracts/staking/token/HBRToken.sol:HBRToken" } } \ No newline at end of file diff --git a/package.json b/package.json index ea8c4e34..01b48d4d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "deploy_astradex_tokensafe": "hardhat run scripts/ecosystem/astradex/deployTokensSafe.ts --network dev", "deploy_government": "hardhat run scripts/ecosystem/government/deploy.ts --network dev", "deploy_liquid_staking": "hardhat run scripts/ecosystem/liquid_staking/deploy.ts --network dev", - "deploy_token_staking": "hardhat run scripts/ecosystem/token_staking/deploy.ts --network dev", + "deploy_limited_token_staking": "hardhat run scripts/ecosystem/token_staking/deploy_limited_manager.ts --network dev", + "deploy_token_staking": "hardhat run scripts/ecosystem/token_staking/deploy_token_manager.ts --network dev", "deploy_hbr_token": "hardhat run scripts/ecosystem/token_staking/deploy_hbr.ts --network dev", "create_hbr_amb_pool": "hardhat run scripts/ecosystem/token_staking/create_hbr_amb.ts --network dev", diff --git a/scripts/ecosystem/token_staking/create_hbr_amb.ts b/scripts/ecosystem/token_staking/create_hbr_amb.ts index 62ab3412..98ac8251 100644 --- a/scripts/ecosystem/token_staking/create_hbr_amb.ts +++ b/scripts/ecosystem/token_staking/create_hbr_amb.ts @@ -23,9 +23,6 @@ async function main() { limitsMultiplierToken: hbrToken.address, profitableToken: ethers.constants.AddressZero, rewardToken: ethers.constants.AddressZero, - rewardTokenPrice: 1, - interest: 0.1 * BILLIION, - interestRate: 24 * 60 * 60, }; const createTx = await poolsManager.createLimitedTokenPool(mainConfig); @@ -33,14 +30,17 @@ async function main() { console.log("createReceipt", createReceipt); const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { + rewardTokenPrice: BILLIION, + interest: 0.1 * BILLIION, + interestRate: 24 * 60 * 60, minDepositValue: 1, minStakeValue: 1, fastUnstakePenalty: 0, unstakeLockPeriod: 24 * 60 * 60, stakeLockPeriod: 24 * 60 * 60, maxTotalStakeValue: 1 * BILLIION, - maxStakePerUserValue: 0.01 * BILLIION, - stakeLimitsMultiplier: 10, + maxStakePerUserValue: 0.1 * BILLIION, + stakeLimitsMultiplier: 10 * BILLIION, }; const configureLimitsTx = await poolsManager.configureLimitedTokenPoolLimits("HBR-AMB", limitsConfig); From ba9c7364f6edec77d80ab9da243b4278789242dd Mon Sep 17 00:00:00 2001 From: SigismundSchlomo Date: Tue, 8 Oct 2024 13:33:37 +0300 Subject: [PATCH 60/60] Fix deployment --- deployments/30746.json | 38 +++++++++---------- .../ecosystem/token_staking/create_hbr_amb.ts | 19 +++++----- .../token_staking/deploy_limited_manager.ts | 6 +-- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/deployments/30746.json b/deployments/30746.json index 9d8631fa..ad3a6d1a 100644 --- a/deployments/30746.json +++ b/deployments/30746.json @@ -1696,8 +1696,8 @@ "deployTx": "0xdda8f77783e2347249ba80546a99665569b58c1399a175d8fa9d3b1c0153485c", "fullyQualifiedName": "contracts/staking/token/HBRToken.sol:HBRToken" }, - "Ecosystem_LimitedTokenPoolsManager": { - "address": "0xB68Cb4770225B20E1967a2E2FEAf82da56568503", + "Ecosystem_LimitedTokenPoolsManager_Multisig": { + "address": "0x8DEE68BD82d3d69DF1005b361D2d9F16AB6E8FA5", "abi": [ "constructor(address[] _signers, bool[] isInitiatorFlags, uint256 _threshold, address owner)", "event Confirmation(address indexed sender, uint256 indexed txId)", @@ -1733,11 +1733,11 @@ "function transferOwnership(address newOwner)", "function withdraw(address to, uint256 amount)" ], - "deployTx": "0xf684813eabc73c9c55280cb13dd8e0a39896b970c6f961e001743ae19457b10e", + "deployTx": "0x3a87ab0d4493e14f81847957a8af1def71ea8fdaea344249f8688fa21b6ae445", "fullyQualifiedName": "contracts/multisig/Multisig.sol:Multisig" }, "Ecosystem_LimitedTokenPoolsManager_RewardsBank": { - "address": "0x47111CEAc3a13E7e8C17eFeA0112b10e51D02efe", + "address": "0x8D11A2398F4CB7d6b06196c5a6E7c75D4780E9bc", "abi": [ "constructor()", "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", @@ -1753,36 +1753,36 @@ "function withdrawAmb(address addressTo, uint256 amount)", "function withdrawErc20(address tokenAddress, address addressTo, uint256 amount)" ], - "deployTx": "0xc130fcca18a397e3c3374c9914289d7a6a9a0b4a0891e199a34165a45eb873af", + "deployTx": "0xfb329fe45d71da60c228a9609aed2c851b1e114df3cf08ed3262916b680ddc42", "fullyQualifiedName": "contracts/funds/RewardsBank.sol:RewardsBank" }, - "Ecosystem_TokenPoolsManager": { - "address": "0xF954533519A87Fa88da6B4CeF5d84Ea4a3aEdaDe", + "Ecosystem_LimitedTokenPoolsManager": { + "address": "0x2540Ca7ccAcD8452a411a3c4004f7b5338a74418", "abi": [ - "constructor(address bank_, address lockKeeper_, address _beacon)", - "event PoolActivated(address pool)", - "event PoolConfigured(address pool, tuple(uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) params)", - "event PoolCreated(address pool)", - "event PoolDeactivated(address pool)", + "constructor(address bank_, address lockKeeper_, address doubleSideBeacon_)", + "event LimitedPoolActivated(address pool)", + "event LimitedPoolConfigured(address pool, tuple(uint256 rewardTokenPrice, uint256 interest, uint256 interestRate, uint256 minDepositValue, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 unstakeLockPeriod, uint256 stakeLockPeriod, uint256 maxTotalStakeValue, uint256 maxStakePerUserValue, uint256 stakeLimitsMultiplier) params)", + "event LimitedPoolCreated(address pool)", + "event LimitedPoolDeactivated(address pool)", "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", - "function activateTokenPool(address _pool)", + "function activatePool(address _pool)", "function bank() view returns (address)", - "function beacon() view returns (address)", - "function configurePool(address pool, tuple(uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) limitsConfig)", - "function createPool(tuple(address token, string name, address rewardToken) mainConfig, tuple(uint256 rewardTokenPrice, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 interest, uint256 interestRate, uint256 lockPeriod) limitsConfig) returns (address)", - "function deactivateTokenPool(address _pool)", + "function configurePool(address _pool, tuple(uint256 rewardTokenPrice, uint256 interest, uint256 interestRate, uint256 minDepositValue, uint256 minStakeValue, uint256 fastUnstakePenalty, uint256 unstakeLockPeriod, uint256 stakeLockPeriod, uint256 maxTotalStakeValue, uint256 maxStakePerUserValue, uint256 stakeLimitsMultiplier) params)", + "function createPool(tuple(string name, address limitsMultiplierToken, address profitableToken, address rewardToken) params) returns (address)", + "function deactivatePool(address _pool)", "function getRoleAdmin(bytes32 role) view returns (bytes32)", "function grantRole(bytes32 role, address account)", "function hasRole(bytes32 role, address account) view returns (bool)", + "function limitedTokenPoolBeacon() view returns (address)", "function pools(uint256) view returns (address)", "function renounceRole(bytes32 role, address account)", "function revokeRole(bytes32 role, address account)", "function supportsInterface(bytes4 interfaceId) view returns (bool)" ], - "deployTx": "0x14eba9d4f3d2694bc5cf56f5ef854430ad984f925afceea9d95dd0943d2edbe5", - "fullyQualifiedName": "contracts/staking/token/TokenPoolsManager.sol:TokenPoolsManager" + "deployTx": "0x94e1f558116c4d7d27aad21616e1f696dc99670750d72612f917141340d3d52e", + "fullyQualifiedName": "contracts/staking/token/LimitedTokenPoolsManager.sol:LimitedTokenPoolsManager" } } \ No newline at end of file diff --git a/scripts/ecosystem/token_staking/create_hbr_amb.ts b/scripts/ecosystem/token_staking/create_hbr_amb.ts index 98ac8251..e9fbd02c 100644 --- a/scripts/ecosystem/token_staking/create_hbr_amb.ts +++ b/scripts/ecosystem/token_staking/create_hbr_amb.ts @@ -3,7 +3,8 @@ import { wrapProviderToError } from "../../../src/utils/AmbErrorProvider"; import { loadDeployment } from "@airdao/deployments/deploying"; import { - LimitedTokenPool + LimitedTokenPool, + LimitedTokenPoolsManager } from "../../../typechain-types"; const BILLIION = 1_000_000_000; @@ -16,7 +17,10 @@ async function main() { const hbrToken = loadDeployment("Ecosystem_HBRToken", chainId, deployer); - const poolsManager = loadDeployment("Ecosystem_TokenPoolsManager", chainId, deployer); + const poolsManager = loadDeployment("Ecosystem_LimitedTokenPoolsManager", chainId, deployer) as LimitedTokenPoolsManager; + + //const pools = await poolsManager.pools(0); + //console.log("pools", pools); const mainConfig: LimitedTokenPool.MainConfigStruct = { name: "HBR-AMB", @@ -25,9 +29,9 @@ async function main() { rewardToken: ethers.constants.AddressZero, }; - const createTx = await poolsManager.createLimitedTokenPool(mainConfig); - const createReceipt = await createTx.wait(); - console.log("createReceipt", createReceipt); + //const createTx = await poolsManager.createPool(mainConfig); + //const createReceipt = await createTx.wait(); + //console.log("createReceipt", createReceipt); const limitsConfig: LimitedTokenPool.LimitsConfigStruct = { rewardTokenPrice: BILLIION, @@ -43,12 +47,9 @@ async function main() { stakeLimitsMultiplier: 10 * BILLIION, }; - const configureLimitsTx = await poolsManager.configureLimitedTokenPoolLimits("HBR-AMB", limitsConfig); + const configureLimitsTx = await poolsManager.configurePool("0x93381ADEC72b8201fFe12E47e47f390f4132764f", limitsConfig); const configureLimitsReceipt = await configureLimitsTx.wait(); console.log("configureLimitsReceipt", configureLimitsReceipt); - - const poolAddress = await poolsManager.getLimitedTokenPoolAdress("HBR-AMB"); - console.log("poolAddress:", poolAddress); } diff --git a/scripts/ecosystem/token_staking/deploy_limited_manager.ts b/scripts/ecosystem/token_staking/deploy_limited_manager.ts index acdee291..afa20085 100644 --- a/scripts/ecosystem/token_staking/deploy_limited_manager.ts +++ b/scripts/ecosystem/token_staking/deploy_limited_manager.ts @@ -18,7 +18,7 @@ export async function main() { const [deployer] = await ethers.getSigners(); wrapProviderToError(deployer.provider!); - const multisig = await deployMultisig(ContractNames.Ecosystem_LimitedTokenPoolsManager, deployer); + const multisig = await deployMultisig(ContractNames.Ecosystem_LimitedTokenPoolsManagerMultisig, deployer); const rewardsBank = await deploy({ contractName: ContractNames.Ecosystem_LimitedTokenPoolsManagerRewardsBank, @@ -45,8 +45,8 @@ export async function main() { console.log("deploying TokenPoolsManager"); const poolsManager = await deploy({ - contractName: ContractNames.Ecosystem_TokenPoolsManager, - artifactName: "TokenPoolsManager", + contractName: ContractNames.Ecosystem_LimitedTokenPoolsManager, + artifactName: "LimitedTokenPoolsManager", deployArgs: [rewardsBank.address, lockKeeper.address, limitedTokenPoolBeacon.address], signer: deployer, loadIfAlreadyDeployed: true,