diff --git a/.solhint.json b/.solhint.json index 8939f9c..519f4a1 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,7 +2,7 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { - "code-complexity": ["error", 10], + "code-complexity": ["error", 11], "compiler-version": ["error", "^0.6.0"], "constructor-syntax": "error", "max-line-length": ["error", 120], diff --git a/contracts/PowerIndexRouter.sol b/contracts/PowerIndexRouter.sol index fedfb28..7019781 100644 --- a/contracts/PowerIndexRouter.sol +++ b/contracts/PowerIndexRouter.sol @@ -55,7 +55,7 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { IPowerPoke public powerPoke; uint256 public reserveRatio; uint256 public claimRewardsInterval; - uint256 public lastRebalancedAt; + uint256 public lastRebalancedByPokerAt; uint256 public reserveRatioLowerBound; uint256 public reserveRatioUpperBound; // 1 ether == 100% @@ -95,7 +95,9 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { uint256 minInterval; uint256 maxInterval; uint256 piTokenUnderlyingBalance; + uint256 subFromExpectedStakeAmount; bool atLeastOneForceRebalance; + bool skipCanPokeCheck; } modifier onlyEOA() { @@ -294,6 +296,11 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { require(success, string(result)); } + function piTokenCallback(address, uint256 _withdrawAmount) external payable virtual override { + PokeFromState memory state = PokeFromState(0, 0, 0, _withdrawAmount, false, true); + _rebalance(state, false, false); + } + /** * @notice Call poke by Reporter. * @param _reporterId Reporter ID. @@ -360,13 +367,35 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { * @param _isSlasher Calling by Slasher. */ function _pokeFrom(bool _claimAndDistributeRewards, bool _isSlasher) internal { - PokeFromState memory state = PokeFromState(0, 0, 0, false); + PokeFromState memory state = PokeFromState(0, 0, 0, 0, false, false); (state.minInterval, state.maxInterval) = _getMinMaxReportInterval(); - state.piTokenUnderlyingBalance = piToken.getUnderlyingBalance(); - (uint256[] memory stakedBalanceList, uint256 totalStakedBalance) = _getUnderlyingStakedList(); + _rebalance(state, _claimAndDistributeRewards, _isSlasher); + + require( + _canPoke(_isSlasher, state.atLeastOneForceRebalance, state.minInterval, state.maxInterval), + "INTERVAL_NOT_REACHED_OR_NOT_FORCE" + ); + + lastRebalancedByPokerAt = block.timestamp; + } + + function _rebalance( + PokeFromState memory s, + bool _claimAndDistributeRewards, + bool _isSlasher + ) internal { + if (connectors.length == 1 && reserveRatio == 0 && !_claimAndDistributeRewards) { + if (s.subFromExpectedStakeAmount > 0) { + _rebalancePoke(connectors[0], StakeStatus.EXCESS, s.subFromExpectedStakeAmount); + } else { + _rebalancePoke(connectors[0], StakeStatus.SHORTAGE, piToken.getUnderlyingBalance()); + } + return; + } - state.atLeastOneForceRebalance = false; + s.piTokenUnderlyingBalance = piToken.getUnderlyingBalance(); + (uint256[] memory stakedBalanceList, uint256 totalStakedBalance) = _getUnderlyingStakedList(); RebalanceConfig[] memory configs = new RebalanceConfig[](connectors.length); @@ -377,19 +406,20 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { } (StakeStatus status, uint256 diff, bool shouldClaim, bool forceRebalance) = getStakeAndClaimStatus( - state.piTokenUnderlyingBalance, + s.piTokenUnderlyingBalance, totalStakedBalance, stakedBalanceList[i], + s.subFromExpectedStakeAmount, _claimAndDistributeRewards, connectors[i] ); if (forceRebalance) { - state.atLeastOneForceRebalance = true; + s.atLeastOneForceRebalance = true; } if (status == StakeStatus.EXCESS) { // Calling rebalance immediately if interval conditions reached - if (_canPoke(_isSlasher, forceRebalance, state.minInterval, state.maxInterval)) { + if (s.skipCanPokeCheck || _canPoke(_isSlasher, forceRebalance, s.minInterval, s.maxInterval)) { _rebalancePokeByConf(RebalanceConfig(false, status, diff, shouldClaim, forceRebalance, i)); } } else { @@ -398,23 +428,16 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { } } - require( - _canPoke(_isSlasher, state.atLeastOneForceRebalance, state.minInterval, state.maxInterval), - "INTERVAL_NOT_REACHED_OR_NOT_FORCE" - ); - // Second cycle: connectors with EQUILIBRIUM and SHORTAGE balance status on staking for (uint256 i = 0; i < connectors.length; i++) { if (!configs[i].shouldPushFunds) { continue; } // Calling rebalance if interval conditions reached - if (_canPoke(_isSlasher, configs[i].forceRebalance, state.minInterval, state.maxInterval)) { + if (s.skipCanPokeCheck || _canPoke(_isSlasher, configs[i].forceRebalance, s.minInterval, s.maxInterval)) { _rebalancePokeByConf(configs[i]); } } - - lastRebalancedAt = block.timestamp; } /** @@ -431,8 +454,8 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { } return _isSlasher - ? (lastRebalancedAt + _maxInterval < block.timestamp) - : (lastRebalancedAt + _minInterval < block.timestamp); + ? (lastRebalancedByPokerAt + _maxInterval < block.timestamp) + : (lastRebalancedByPokerAt + _minInterval < block.timestamp); } /** @@ -564,13 +587,14 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { bool forceRebalance ) { - return getStakeStatus(piToken.getUnderlyingBalance(), getUnderlyingStaked(), _stakedBalance, _share); + return getStakeStatus(piToken.getUnderlyingBalance(), getUnderlyingStaked(), _stakedBalance, 0, _share); } function getStakeAndClaimStatus( uint256 _leftOnPiTokenBalance, uint256 _totalStakedBalance, uint256 _stakedBalance, + uint256 _subFromExpectedStakeAmount, bool _claimAndDistributeRewards, Connector memory _c ) @@ -587,6 +611,7 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { _leftOnPiTokenBalance, _totalStakedBalance, _stakedBalance, + _subFromExpectedStakeAmount, _c.share ); shouldClaim = _claimAndDistributeRewards && claimRewardsIntervalReached(_c.lastClaimRewardsAt); @@ -606,6 +631,7 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { uint256 _leftOnPiTokenBalance, uint256 _totalStakedBalance, uint256 _stakedBalance, + uint256 _subFromExpectedStakeAmount, uint256 _share ) public @@ -622,7 +648,8 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { _leftOnPiTokenBalance, _totalStakedBalance, _stakedBalance, - _share + _share, + _subFromExpectedStakeAmount ); if (status == StakeStatus.EQUILIBRIUM) { @@ -754,7 +781,8 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { uint256 _leftOnPiToken, uint256 _totalStakedBalance, uint256 _stakedBalance, - uint256 _share + uint256 _share, + uint256 _subFromExpectedStakeAmount ) public view @@ -767,6 +795,11 @@ contract PowerIndexRouter is PowerIndexRouterInterface, PowerIndexNaiveRouter { require(_reserveRatioPct <= HUNDRED_PCT, "RR_GREATER_THAN_100_PCT"); expectedStakeAmount = getExpectedStakeAmount(_reserveRatioPct, _leftOnPiToken, _totalStakedBalance, _share); + uint256 subFromExpectedStakeAmountByShare = _subFromExpectedStakeAmount.mul(_share).div(1 ether); + if (expectedStakeAmount >= subFromExpectedStakeAmountByShare) { + expectedStakeAmount -= subFromExpectedStakeAmountByShare; + } + if (expectedStakeAmount > _stakedBalance) { status = StakeStatus.SHORTAGE; diff = expectedStakeAmount.sub(_stakedBalance); diff --git a/contracts/WrappedPiErc20.sol b/contracts/WrappedPiErc20.sol index 426d71a..b475907 100644 --- a/contracts/WrappedPiErc20.sol +++ b/contracts/WrappedPiErc20.sol @@ -157,7 +157,10 @@ contract WrappedPiErc20 is ERC20, ReentrancyGuard, WrappedPiErc20Interface { uint256 withdrawAmount = getUnderlyingEquivalentForPi(_burnAmount); require(withdrawAmount > 0, "ZERO_UNDERLYING_TO_WITHDRAW"); - PowerIndexNaiveRouterInterface(router).piTokenCallback{ value: msg.value }(msg.sender, withdrawAmount); + + if (routerCallbackEnabled) { + PowerIndexNaiveRouterInterface(router).piTokenCallback{ value: msg.value }(msg.sender, withdrawAmount); + } _burn(msg.sender, _burnAmount); underlying.safeTransfer(msg.sender, withdrawAmount); diff --git a/contracts/connectors/TornPowerIndexConnector.sol b/contracts/connectors/TornPowerIndexConnector.sol index 53b6b83..b0cddaf 100644 --- a/contracts/connectors/TornPowerIndexConnector.sol +++ b/contracts/connectors/TornPowerIndexConnector.sol @@ -46,7 +46,6 @@ contract TornPowerIndexConnector is AbstractConnector { if (receivedReward > 0) { uint256 rewardsToReinvest; (rewardsToReinvest, stakeData) = _distributeReward(_distributeData, PI_TOKEN, UNDERLYING, receivedReward); - _approveToStaking(rewardsToReinvest); _stakeImpl(rewardsToReinvest); return stakeData; } diff --git a/hardhat.config.js b/hardhat.config.js index 67d9003..f700acc 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -5,6 +5,7 @@ require('solidity-coverage'); require('hardhat-contract-sizer'); require('hardhat-gas-reporter'); require('./tasks/deployTornVault'); +require('./tasks/redeployTornRouter'); const fs = require('fs'); const homeDir = require('os').homedir(); diff --git a/package.json b/package.json index f7bd0d1..ce94dad 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "build": "yarn run compile && yarn run typechain", "clean": "hardhat clean", "compile": "hardhat compile", - "compile-release": "COMPILE_TARGET=release hardhat compile", + "compile-release": "rm -rf ./artifacts && rm -rf ./cache && COMPILE_TARGET=release hardhat compile", "coverage": "hardhat coverage --show-stack-traces --solcoverjs ./.solcover.js --network coverage --temp artifacts --testfiles \"./test/**/*.js\"", "lint:sol": "solhint --config ./.solhint.json \"contracts/**/*.sol\"", "lint:js": "eslint --config .eslintrc.json --ignore-path ./.eslintignore --ext .js .", diff --git a/tasks/deployTornVault.js b/tasks/deployTornVault.js index 595a21c..2237996 100644 --- a/tasks/deployTornVault.js +++ b/tasks/deployTornVault.js @@ -1,10 +1,10 @@ require('@nomiclabs/hardhat-truffle5'); require('@nomiclabs/hardhat-ethers'); -task('deploy-torn-vault', 'Deploy VestedLpMining').setAction(async (__, {ethers, network}) => { +task('deploy-torn-vault', 'Deploy TornVault').setAction(async (__, {ethers, network}) => { const {ether, fromEther, impersonateAccount, gwei, increaseTime, advanceBlocks} = require('../test/helpers'); const WrappedPiErc20 = await artifacts.require('WrappedPiErc20'); - const IERC20 = await artifacts.require(process.env.FLAT ? 'flatten/PowerIndexRouter.sol:IERC20' : '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20'); + const IERC20 = await artifacts.require('WrappedPiErc20'); const PowerIndexRouter = await artifacts.require('PowerIndexRouter'); const TornPowerIndexConnector = await artifacts.require('TornPowerIndexConnector'); @@ -25,19 +25,19 @@ task('deploy-torn-vault', 'Deploy VestedLpMining').setAction(async (__, {ethers, const tornAddress = '0x77777feddddffc19ff86db637967013e6c6a116c'; const startBalance = fromEther(await web3.eth.getBalance(deployer)); const piTorn = await WrappedPiErc20.new(tornAddress, deployer, 'PowerPool Torn Vault', 'ppTORN'); - console.log('piTorn', piTorn.address, 'name', await piTorn.name(), 'symbol', await piTorn.symbol()); + console.log('piTorn', piTorn.address, 'name', await piTorn.contract.methods.name().call(), 'symbol', await piTorn.contract.methods.symbol().call()); const tornRouter = await PowerIndexRouter.new( piTorn.address, { poolRestrictions: '0x698967cA2fB85A6D9a7D2BeD4D2F6D32Bbc5fCdc', powerPoke: '0x04D7aA22ef7181eE3142F5063e026Af1BbBE5B96', - reserveRatio: ether(0.1), - reserveRatioLowerBound: ether(0.01), - reserveRatioUpperBound: ether(0.2), - claimRewardsInterval: '604800', + reserveRatio: '0', + reserveRatioLowerBound: '0', + reserveRatioUpperBound: '0', + claimRewardsInterval: '86400', performanceFeeReceiver: '0xd132973eaebbd6d7ca7b88e9170f2cca058de430', - performanceFee: ether(0.003) + performanceFee: '0' } ); console.log('tornRouter', tornRouter.address); @@ -55,10 +55,11 @@ task('deploy-torn-vault', 'Deploy VestedLpMining').setAction(async (__, {ethers, connectorIndex: 0, }, ]); + await piTorn.enableRouterCallback(true); await piTorn.changeRouter(tornRouter.address); console.log('tornConnector done'); // console.log('getUnderlyingReserve', await tornRouter.getUnderlyingReserve()); - console.log('tornConnector.getTornPriceRatio', await tornConnector.getTornPriceRatio().then(r => r.toString())); + // console.log('tornConnector.getTornPriceRatio', await tornConnector.contract.methods.getTornPriceRatio().call().then(r => r.toString())); // console.log('router.getUnderlyingStaked', await tornRouter.getUnderlyingStaked()); // console.log('calculateLockedProfit', await tornRouter.calculateLockedProfit()); // console.log('getUnderlyingAvailable', await tornRouter.getUnderlyingAvailable()); diff --git a/tasks/redeployTornRouter.js b/tasks/redeployTornRouter.js new file mode 100644 index 0000000..a9cd7ef --- /dev/null +++ b/tasks/redeployTornRouter.js @@ -0,0 +1,138 @@ +require('@nomiclabs/hardhat-truffle5'); +require('@nomiclabs/hardhat-ethers'); + +task('redeploy-torn-router', 'Redeploy TornRouter').setAction(async (__, {ethers, network}) => { + const {ether, fromEther, impersonateAccount, increaseTime, advanceBlocks} = require('../test/helpers'); + const WrappedPiErc20 = await artifacts.require('WrappedPiErc20'); + const IERC20 = await artifacts.require('WrappedPiErc20'); + const PowerIndexRouter = await artifacts.require('PowerIndexRouter'); + const TornPowerIndexConnector = await artifacts.require('TornPowerIndexConnector'); + + if (process.env.FORK) { + await ethers.provider.send('hardhat_reset', [{forking: {jsonRpcUrl: process.env.FORK}}]); + } + + const { web3 } = WrappedPiErc20; + + const [deployer] = await web3.eth.getAccounts(); + console.log('deployer', deployer); + const OWNER = '0xB258302C3f209491d604165549079680708581Cc'; + const piTornAddress = '0xa1ebc8bde2f1f87fe24f384497b6bd9ce3b14345'; + const TORN_STAKING = '0x2fc93484614a34f26f7970cbb94615ba109bb4bf'; + const TORN_GOVERNANCE = '0x5efda50f22d34f262c29268506c5fa42cb56a1ce'; + const tornAddress = '0x77777feddddffc19ff86db637967013e6c6a116c'; + + const startBalance = fromEther(await web3.eth.getBalance(deployer)); + const tornRouter = await PowerIndexRouter.at('0xDAf584C15722cdc7E78214Fb1A4832dA6638D655'); + console.log('tornRouter', tornRouter.address); + const piTorn = await WrappedPiErc20.at(piTornAddress); + const tornConnector = await TornPowerIndexConnector.new(TORN_STAKING, tornAddress, piTorn.address, TORN_GOVERNANCE); + console.log('tornConnector', tornConnector.address); + + console.log('tornConnector done'); + + const endBalance = fromEther(await web3.eth.getBalance(deployer)); + console.log('balance spent', startBalance - endBalance); + if (network.name !== 'mainnetfork') { + return; + } + + await impersonateAccount(ethers, OWNER); + await tornRouter.initRouterByConnector('0', '0x', {from: OWNER}); + await tornRouter.setConnectorList([ + { + connector: tornConnector.address, + share: ether(1), + callBeforeAfterPoke: false, + newConnector: false, + connectorIndex: 0, + }, + ], {from: OWNER}); + + const ITornGovernance = await artifacts.require('ITornGovernance'); + const ITornStaking = await artifacts.require('ITornStaking'); + + const torn = await IERC20.at('0x77777feddddffc19ff86db637967013e6c6a116c'); + + const tornHolder = '0xf977814e90da44bfa03b6295a0616a897441acec'; + const pokerReporter = '0xabdf215fce6c5b0c1b40b9f2068204a9e7c49627'; + await impersonateAccount(ethers, tornHolder); + const amount = ether( 33400); + console.log('1 wrapper balance', fromEther(await torn.balanceOf(piTorn.address))); + await torn.approve(piTorn.address, amount, {from: tornHolder}); + await piTorn.deposit(amount, {from: tornHolder}); + console.log('2 wrapper balance', fromEther(await torn.balanceOf(piTorn.address))); + + const powerPokeOpts = web3.eth.abi.encodeParameter( + { PowerPokeRewardOpts: {to: 'address', compensateInETH: 'bool'} }, + {to: pokerReporter, compensateInETH: true}, + ); + + await impersonateAccount(ethers, pokerReporter); + + // await tornRouter.pokeFromReporter('1', false, powerPokeOpts, {from: pokerReporter}); + + console.log('3 wrapper balance', fromEther(await torn.balanceOf(piTorn.address))); + const governance = await ITornGovernance.at(TORN_GOVERNANCE); + const staking = await ITornStaking.at(TORN_STAKING); + console.log('lockedBalance', fromEther(await governance.lockedBalance(piTorn.address))); + console.log('checkReward 1', fromEther(await staking.checkReward(piTorn.address))); + + const TEN_HOURS = 60 * 60 * 10; + const GAS_TO_REINVEST = '100000'; + await impersonateAccount(ethers, TORN_GOVERNANCE); + + await tornRouter.setClaimParams('0', await getClaimParams(TEN_HOURS), {from: OWNER}); + + await increaseTime(TEN_HOURS); + await advanceBlocks(1); + await staking.addBurnRewards(ether(1700), {from: TORN_GOVERNANCE}); + console.log('checkReward 2', fromEther(await staking.checkReward(piTorn.address))); + await printForecast(TEN_HOURS); + await checkClaimAvailability(TEN_HOURS); + + await increaseTime(TEN_HOURS); + await advanceBlocks(1); + await staking.addBurnRewards(ether(2700), {from: TORN_GOVERNANCE}); + console.log('checkReward 3', fromEther(await staking.checkReward(piTorn.address))); + await printForecast(TEN_HOURS); + await checkClaimAvailability(TEN_HOURS); + + const res = await tornRouter.pokeFromReporter('1', true, powerPokeOpts, {from: pokerReporter}); + console.log('res.receipt.gasUsed', res.receipt.gasUsed); + + console.log('lockedBalance', fromEther(await governance.lockedBalance(piTorn.address))); + + await torn.approve(piTorn.address, amount, {from: tornHolder}); + await piTorn.deposit(amount, {from: tornHolder}); + + console.log('lockedBalance after deposit', fromEther(await governance.lockedBalance(piTorn.address))); + + function getClaimParams(duration) { + return tornConnector.packClaimParams(duration, GAS_TO_REINVEST); + } + async function checkClaimAvailability(duration) { + const connector = await tornRouter.connectors('0'); + const claimParams = await getClaimParams(duration); + const res = await tornConnector.isClaimAvailable(claimParams, connector.lastClaimRewardsAt, connector.lastChangeStakeAt); + const tornNeedToReinvest = await tornConnector.getTornUsedToReinvest(GAS_TO_REINVEST, parseInt(process.env.GAS_PRICE) * 10 ** 9); + console.log('tornNeedToReinvest', fromEther(tornNeedToReinvest)); + console.log('isClaimAvailable for', parseInt(duration) / (60 * 60), 'hours:', res); + return res; + } + + async function printForecast(investDuration) { + const block = await web3.eth.getBlock('latest'); + const connector = await tornRouter.connectors('0'); + let {lastClaimRewardsAt, lastChangeStakeAt} = connector; + lastClaimRewardsAt = parseInt(lastClaimRewardsAt.toString(10)); + lastChangeStakeAt = parseInt(lastChangeStakeAt.toString(10)); + const lastRewardsAt = lastClaimRewardsAt > lastChangeStakeAt ? lastClaimRewardsAt : lastChangeStakeAt; + + console.log('forecast after', (block.timestamp - lastRewardsAt) / (60 * 60), 'hours:', fromEther(await tornConnector.getPendingAndForecastReward( + lastClaimRewardsAt, + lastChangeStakeAt, + investDuration + ).then(r => r.forecastByPending)), 'with invest duration', investDuration / (60 * 60), 'hours'); + } +}); diff --git a/test/WrappedPiErc20.unit.test.js b/test/WrappedPiErc20.unit.test.js index bf75264..f154f08 100644 --- a/test/WrappedPiErc20.unit.test.js +++ b/test/WrappedPiErc20.unit.test.js @@ -664,6 +664,8 @@ describe('WrappedPiErc20 Unit Tests', () => { const ethFee = ether(0.001); + await router.enableRouterCallback(piCake.address, true); + await router.setPiTokenEthFee(ethFee, { from: deployer }); assert.equal(await piCake.ethFee(), ethFee); diff --git a/test/implementations/TornConnector.unit.js b/test/implementations/TornConnector.unit.js index 5971ab4..798d394 100644 --- a/test/implementations/TornConnector.unit.js +++ b/test/implementations/TornConnector.unit.js @@ -1,8 +1,13 @@ const { time, constants, expectRevert } = require('@openzeppelin/test-helpers'); -const { ether, fromEther } = require('./../helpers'); +const { ether, fromEther, latestBlockTimestamp } = require('./../helpers'); const { buildBasicRouterConfig } = require('./../helpers/builders'); +const { + Eip2612PermitUtils, + Web3ProviderConnector, fromRpcSig, +} = require('@1inch/permit-signed-approvals-utils'); + const assert = require('chai').assert; -const MockERC20 = artifacts.require('MockERC20'); +const MockERC20 = artifacts.require('MockERC20Permit'); const TornPowerIndexConnector = artifacts.require('MockTornPowerIndexConnector'); const PowerIndexRouter = artifacts.require('PowerIndexRouter'); const WrappedPiErc20 = artifacts.require('WrappedPiErc20'); @@ -16,10 +21,12 @@ MockERC20.numberFormat = 'String'; TornPowerIndexConnector.numberFormat = 'String'; WrappedPiErc20.numberFormat = 'String'; PowerIndexRouter.numberFormat = 'String'; +TornGovernance.numberFormat = 'String'; const { web3 } = MockERC20; const REPORTER_ID = 42; +const chainId = 31337; describe('PancakeMasterChefRouter Tests', () => { let deployer, bob, alice, piGov, stub, pvp, pool1, pool2; @@ -147,18 +154,18 @@ describe('PancakeMasterChefRouter Tests', () => { await piTorn.deposit(ether('10000'), { from: alice }); // bob - await torn.transfer(bob, ether('42000')); + await torn.transfer(bob, ether('100000')); await torn.approve(governance.address, ether('42000'), { from: bob }); await governance.lockWithApproval(ether('42000'), { from: bob }); + }); + it('should claim rewards and reinvest', async () => { await myRouter.pokeFromReporter(REPORTER_ID, true, '0x'); assert.equal(await governance.lockedBalance(piTorn.address), ether(8000)); assert.equal(await torn.balanceOf(governance.address), ether(50000)); assert.equal(await torn.balanceOf(piTorn.address), ether(2000)); - }); - it('should claim rewards and reinvest', async () => { const reinvestDuration = 60 * 60; const claimParams = await connector.packClaimParams(reinvestDuration, GAS_TO_REINVEST); await myRouter.setClaimParams('0', claimParams, {from: piGov}); @@ -204,8 +211,11 @@ describe('PancakeMasterChefRouter Tests', () => { assert.equal(fromEther(await rewards.lockedProfit, ether('2.7197990668')) > 3300, true); }); - describe('on poke', async () => { + beforeEach(async () => { + await myRouter.pokeFromReporter(REPORTER_ID, true, '0x'); + }); + it('should do nothing when nothing has changed', async () => { await expectRevert(myRouter.pokeFromReporter(REPORTER_ID, false, '0x'), 'NOTHING_TO_DO'); }); @@ -241,5 +251,247 @@ describe('PancakeMasterChefRouter Tests', () => { assert.equal(await myRouter.getUnderlyingTotal(), ether('12360')); }); }); + + describe('on deposit/withdraw', async () => { + it('should increase reserve if required', async () => { + let nonce = {}; + + await myRouter.enableRouterCallback(piTorn.address, true, {from: piGov}); + await myRouter.setReserveConfig('0', '0', '0', '1000', {from: piGov}); + + assert.equal(await torn.balanceOf(piTorn.address), ether('10000')); + assert.equal(await governance.lockedBalance(piTorn.address), '0'); + + let vrs = await getPermitVrs(ether(10000), alice); + let res = await piTorn.depositWithPermit(ether(10000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: alice}); + // console.log('1 deposit by alice gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(20000)); + + await piTorn.withdraw(ether('5000'), { from: alice }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(15000)); + + vrs = await getPermitVrs(ether(2000), alice); + await piTorn.depositWithPermit(ether(2000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: alice}); + // console.log('2 deposit by alice gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(17000)); + + vrs = await getPermitVrs(ether(2000), bob); + await piTorn.depositWithPermit(ether(2000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: bob}); + // console.log('1 deposit by bob gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(19000)); + + vrs = await getPermitVrs(ether(1000), bob); + await piTorn.depositWithPermit(ether(1000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: bob}); + // console.log('2 deposit by bob gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(20000)); + + await staking.addBurnRewards(ether('50000')); + + assert.equal(await torn.balanceOf(bob), ether('55000')); + assert.equal(await piTorn.balanceOf(bob), ether('3000')); + await expectRevert(piTorn.withdraw(ether('4000'), { from: bob }), 'ERC20: burn amount exceeds balance'); + await piTorn.withdraw(ether('2000'), { from: bob }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(bob), ether('57000')); + assert.equal(await piTorn.balanceOf(bob), ether('1000')); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(18000)); + + res = await myRouter.pokeFromReporter(REPORTER_ID, true, '0x'); + const distributeRewards = TornPowerIndexConnector.decodeLogs(res.receipt.rawLogs).filter( + l => l.event === 'DistributeReward', + )[0]; + assert.equal(distributeRewards.args.totalReward, ether('5000')); + + assert.equal(await torn.balanceOf(governance.address), ether(64250)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(22250)); + assert.equal(await myRouter.getUnderlyingStaked(), ether(22250)); + assert.equal(await myRouter.getUnderlyingReserve(), ether('0')); + assert.equal(await myRouter.getUnderlyingAvailable(), ether(18000)); + assert.equal(await myRouter.getUnderlyingTotal(), ether('22250')); + assert.equal(await myRouter.calculateLockedProfit(), ether('4250')); + assert.equal(await torn.balanceOf(piTorn.address), ether('0')); + + await piTorn.withdraw(ether('100'), { from: bob }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(bob), ether('57100')); + assert.equal(await piTorn.balanceOf(bob), ether('900.001086099314865775')); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(22150)); + + await time.increase(time.duration.hours(10)); + await piTorn.withdraw(ether('100'), { from: bob }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(bob), ether('57200')); + assert.equal(await piTorn.balanceOf(bob), ether('819.188440112410509722')); + assert.equal(await myRouter.calculateLockedProfit(), ether(0)); + assert.equal(await myRouter.getUnderlyingAvailable(), ether('22050')); + assert.equal(await myRouter.getUnderlyingTotal(), ether('22050')); + + vrs = await getPermitVrs(ether(100), bob); + await piTorn.depositWithPermit(ether(100), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: bob}); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(bob), ether('57100')); + assert.equal(await piTorn.balanceOf(bob), ether('900.001086099314865775')); + assert.equal(await torn.balanceOf(piTorn.address), '0'); + assert.equal(await governance.lockedBalance(piTorn.address), ether(22150)); + + async function getPermitVrs(value, owner) { + const deadline = (await latestBlockTimestamp()) + 10; + if(!nonce[owner]) { + nonce[owner] = 0; + } + const permitParams = { + spender: piTorn.address, + nonce: nonce[owner], + owner, + value, + deadline, + }; + nonce[owner]++; + + const connector = new Web3ProviderConnector(web3); + const eip2612PermitUtils = new Eip2612PermitUtils(connector); + const signature = await eip2612PermitUtils.buildPermitSignature( + permitParams, + chainId, + 'Torn', + torn.address, + '1' + ); + return { + ...fromRpcSig(signature), + deadline + }; + } + }); + }); + + describe('on deposit/withdraw', async () => { + it('should increase reserve if required', async () => { + let nonce = {}; + + await myRouter.enableRouterCallback(piTorn.address, true, {from: piGov}); + await myRouter.setReserveConfig(ether('0.1'), ether('0.1'), ether('0.1'), '1000', {from: piGov}); + + assert.equal(await torn.balanceOf(piTorn.address), ether('10000')); + assert.equal(await governance.lockedBalance(piTorn.address), '0'); + + let vrs = await getPermitVrs(ether(10000), alice); + await piTorn.depositWithPermit(ether(10000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: alice}); + // console.log('1 deposit by alice gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), ether(2000)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(18000)); + assert.equal(await torn.balanceOf(alice), '0'); + + await piTorn.withdraw(ether('5000'), { from: alice }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(alice), ether('5000')); + assert.equal(await torn.balanceOf(piTorn.address), ether(2000)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(13000)); + + vrs = await getPermitVrs(ether(2000), alice); + await piTorn.depositWithPermit(ether(2000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: alice}); + // console.log('2 deposit by alice gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), ether(1700)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(15300)); + + vrs = await getPermitVrs(ether(2000), bob); + await piTorn.depositWithPermit(ether(2000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: bob}); + // console.log('1 deposit by bob gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), ether(1900)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(17100)); + + vrs = await getPermitVrs(ether(1000), bob); + await piTorn.depositWithPermit(ether(1000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: bob}); + // console.log('2 deposit by bob gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), ether(2000)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(18000)); + + await staking.addBurnRewards(ether('50000')); + + assert.equal(await torn.balanceOf(bob), ether('55000')); + assert.equal(await piTorn.balanceOf(bob), ether('3000')); + await expectRevert(piTorn.withdraw(ether('4000'), { from: bob }), 'ERC20: burn amount exceeds balance'); + await piTorn.withdraw(ether('2000'), { from: bob }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(bob), ether('57000')); + assert.equal(await piTorn.balanceOf(bob), ether('1000')); + assert.equal(await torn.balanceOf(piTorn.address), ether(2000)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(16000)); + + const res = await myRouter.pokeFromReporter(REPORTER_ID, true, '0x'); + const distributeRewards = TornPowerIndexConnector.decodeLogs(res.receipt.rawLogs).filter( + l => l.event === 'DistributeReward', + )[0]; + assert.equal(distributeRewards.args.totalReward, ether('4500')); + + assert.equal(await torn.balanceOf(governance.address), ether(62025)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(20025)); + assert.equal(await myRouter.getUnderlyingStaked(), ether(20025)); + assert.equal(await myRouter.getUnderlyingReserve(), ether(1800)); + assert.equal(await myRouter.getUnderlyingAvailable(), ether(18000)); + assert.equal(await myRouter.getUnderlyingTotal(), ether('21825')); + assert.equal(await myRouter.calculateLockedProfit(), ether('3825')); + assert.equal(await torn.balanceOf(piTorn.address), ether(1800)); + + await piTorn.withdraw(ether('100'), { from: bob }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(bob), ether('57100')); + assert.equal(await piTorn.balanceOf(bob), ether('900.000977490445030900')); + assert.equal(await torn.balanceOf(piTorn.address), ether(2182.5)); + assert.equal(await governance.lockedBalance(piTorn.address), ether(19542.5)); + + await time.increase(time.duration.hours(10)); + await piTorn.withdraw(ether('100'), { from: bob }); + // console.log('withdraw gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(bob), ether('57200')); + assert.equal(await piTorn.balanceOf(bob), ether('817.607417179787056075')); + assert.equal(await myRouter.calculateLockedProfit(), ether(0)); + assert.equal(await myRouter.getUnderlyingAvailable(), ether('21625')); + assert.equal(await myRouter.getUnderlyingTotal(), ether('21625')); + + vrs = await getPermitVrs(ether(1000), bob); + await piTorn.depositWithPermit(ether(1000), vrs.deadline, vrs.v, vrs.r, vrs.s, {from: bob}); + // console.log('2 deposit by bob gasUsed', res.receipt.gasUsed); + assert.equal(await torn.balanceOf(piTorn.address), ether('2262.5')); + assert.equal(await governance.lockedBalance(piTorn.address), ether('20362.5')); + + async function getPermitVrs(value, owner) { + const deadline = (await latestBlockTimestamp()) + 10; + if(!nonce[owner]) { + nonce[owner] = 0; + } + const permitParams = { + spender: piTorn.address, + nonce: nonce[owner], + owner, + value, + deadline, + }; + nonce[owner]++; + + const connector = new Web3ProviderConnector(web3); + const eip2612PermitUtils = new Eip2612PermitUtils(connector); + const signature = await eip2612PermitUtils.buildPermitSignature( + permitParams, + chainId, + 'Torn', + torn.address, + '1' + ); + return { + ...fromRpcSig(signature), + deadline + }; + } + }); + }); }); });