From d675522192a78b0d428789afdb51be5dab145717 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 13 Apr 2022 11:59:17 +1000 Subject: [PATCH] fix: convertFees in StakedTokenBPT --- contracts/governance/staking/StakedToken.sol | 26 ++-- .../governance/staking/StakedTokenBPT.sol | 56 ++++---- .../governance/staking/StakedTokenMTA.sol | 6 +- ...lancerPoolGauge.sol => IBalancerGauge.sol} | 2 +- .../MockBPTGauge.sol} | 4 +- .../governance/MockStakedTokenWithPrice.sol | 4 +- tasks/utils/rewardsUtils.ts | 20 +-- tasks/utils/tokens.ts | 3 + .../staking/staked-token-bpt.spec.ts | 122 ++++++++++++++---- .../staking/staked-token-mta.spec.ts | 1 - test/governance/staking/staked-token.spec.ts | 11 +- 11 files changed, 163 insertions(+), 92 deletions(-) rename contracts/peripheral/Balancer/{IBalancerPoolGauge.sol => IBalancerGauge.sol} (94%) rename contracts/z_mocks/{shared/MockBalancerPoolGauge.sol => governance/MockBPTGauge.sol} (86%) diff --git a/contracts/governance/staking/StakedToken.sol b/contracts/governance/staking/StakedToken.sol index 1eab06f4..f606bed7 100644 --- a/contracts/governance/staking/StakedToken.sol +++ b/contracts/governance/staking/StakedToken.sol @@ -34,7 +34,7 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { /// @notice Seconds a user must wait after she initiates her cooldown before withdrawal is possible uint256 public immutable COOLDOWN_SECONDS; /// @notice Window in which it is possible to withdraw, following the cooldown period - uint256 public immutable UNSTAKE_WINDOW; + uint256 public constant UNSTAKE_WINDOW = 14 days; /// @notice A week uint256 private constant ONE_WEEK = 7 days; @@ -70,7 +70,6 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { * @param _questManager Centralised manager of quests * @param _stakedToken Core token that is staked and tracked (e.g. MTA) * @param _cooldownSeconds Seconds a user must wait after she initiates her cooldown before withdrawal is possible - * @param _unstakeWindow Window in which it is possible to withdraw, following the cooldown period * @param _hasPriceCoeff true if raw staked amount is multiplied by price coeff to get staked amount. eg BPT Staked Token */ constructor( @@ -79,12 +78,10 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { address _questManager, address _stakedToken, uint256 _cooldownSeconds, - uint256 _unstakeWindow, bool _hasPriceCoeff ) GamifiedVotingToken(_nexus, _rewardsToken, _questManager, _hasPriceCoeff) { STAKED_TOKEN = IERC20(_stakedToken); COOLDOWN_SECONDS = _cooldownSeconds; - UNSTAKE_WINDOW = _unstakeWindow; } /** @@ -235,7 +232,7 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { /** * @dev Withdraw raw tokens from the system, following an elapsed cooldown period. * Note - May be subject to a transfer fee, depending on the users weightedTimestamp - * @param _amount Units of raw token to withdraw + * @param _amount Units of raw staking token to withdraw. eg MTA or mBPT * @param _recipient Address of beneficiary who will receive the raw tokens * @param _amountIncludesFee Is the `_amount` specified inclusive of any applicable redemption fee? * @param _exitCooldown Should we take this opportunity to exit the cooldown period? @@ -252,7 +249,7 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { /** * @dev Withdraw raw tokens from the system, following an elapsed cooldown period. * Note - May be subject to a transfer fee, depending on the users weightedTimestamp - * @param _amount Units of raw token to withdraw + * @param _amount Units of raw staking token to withdraw. eg MTA or mBPT * @param _recipient Address of beneficiary who will receive the raw tokens * @param _amountIncludesFee Is the `_amount` specified inclusive of any applicable redemption fee? * @param _exitCooldown Should we take this opportunity to exit the cooldown period? @@ -270,7 +267,7 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { // 1. If recollateralisation has occured, the contract is finished and we can skip all checks _burnRaw(_msgSender(), _amount, false, true); // 2. Return a proportionate amount of tokens, based on the collateralisation ratio - _transferStakedTokens(_recipient, (_amount * safetyData.collateralisationRatio) / 1e18); + _withdrawStakedTokens(_recipient, (_amount * safetyData.collateralisationRatio) / 1e18); emit Withdraw(_msgSender(), _recipient, _amount); } else { // 1. If no recollateralisation has occured, the user must be within their UNSTAKE_WINDOW period in order to withdraw @@ -291,7 +288,7 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { // 3. Apply redemption fee // e.g. (55e18 / 5e18) - 2e18 = 9e18 / 100 = 9e16 uint256 feeRate = calcRedemptionFeeRate(balance.weightedTimestamp); - // fee = amount * 1e18 / feeRate + // fee = amount * feeRate / 1e18 // totalAmount = amount + fee uint256 totalWithdraw = _amountIncludesFee ? _amount @@ -308,20 +305,21 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { // 5. Settle the withdrawal by burning the voting tokens _burnRaw(_msgSender(), totalWithdraw, exitCooldown, false); - // Log any redemption fee to the rewards contract + // Log any redemption fee to the rewards contract if MTA or + // the staking token if mBPT. _notifyAdditionalReward(totalWithdraw - userWithdrawal); - // Finally transfer staked tokens back to recipient - _transferStakedTokens(_recipient, userWithdrawal); + // Finally transfer staked tokens back to recipient + _withdrawStakedTokens(_recipient, userWithdrawal); emit Withdraw(_msgSender(), _recipient, _amount); } } /** - * @dev Transfers an `amount` of staked tokens to the `recipient`. eg MTA or mBPT. + * @dev Transfers an `amount` of staked tokens to the withdraw `recipient`. eg MTA or mBPT. * Can be overridden if the tokens are held elsewhere. eg in the Balancer Pool Gauge. */ - function _transferStakedTokens( + function _withdrawStakedTokens( address _recipient, uint256 amount ) internal virtual { @@ -384,7 +382,7 @@ contract StakedToken is GamifiedVotingToken, InitializableReentrancyGuard { safetyData.collateralisationRatio = 1e18 - safetyData.slashingPercentage; // 2. Take slashing percentage uint256 balance = _balanceOfStakedTokens(); - _transferStakedTokens(_recollateraliser(), (balance * safetyData.slashingPercentage) / 1e18); + _withdrawStakedTokens(_recollateraliser(), (balance * safetyData.slashingPercentage) / 1e18); // 3. No functions should work anymore because the colRatio has changed emit Recollateralised(); } diff --git a/contracts/governance/staking/StakedTokenBPT.sol b/contracts/governance/staking/StakedTokenBPT.sol index 03a474d1..c6920642 100644 --- a/contracts/governance/staking/StakedTokenBPT.sol +++ b/contracts/governance/staking/StakedTokenBPT.sol @@ -6,7 +6,7 @@ import { StakedToken } from "./StakedToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IBVault, ExitPoolRequest } from "./interfaces/IBVault.sol"; -import { IBalancerPoolGauge } from "../../peripheral/Balancer/IBalancerPoolGauge.sol"; +import { IBalancerGauge } from "../../peripheral/Balancer/IBalancerGauge.sol"; /** * @title StakedTokenBPT @@ -22,10 +22,11 @@ contract StakedTokenBPT is StakedToken { /// @notice Balancer vault IBVault public immutable balancerVault; - /// @notice Balancer poolId + /// @notice Balancer pool Id bytes32 public immutable poolId; - IBalancerPoolGauge public immutable balancerPoolGauge; + /// @notice Balancer Pool Token Gauge. eg mBPT Gauge (mBPT-gauge) + IBalancerGauge public immutable balancerGauge; /// @notice contract that can redistribute the $BAL /// @dev Deprecated as the $BAL recipient is now set in the BPT Gauge. @@ -58,10 +59,9 @@ contract StakedTokenBPT is StakedToken { * @param _questManager Centralised manager of quests * @param _stakedToken Core token that is staked and tracked e.g. mStable MTA/WETH Staking BPT (mBPT) * @param _cooldownSeconds Seconds a user must wait after she initiates her cooldown before withdrawal is possible - * @param _unstakeWindow Window in which it is possible to withdraw, following the cooldown period * @param _bal Balancer addresses, [0] = $BAL addr, [1] = BAL vault * @param _poolId Balancer Pool identifier - * @param _balancerPoolGauge Address of the Balancer Pool Gauge. eg mBPT Gauge (mBPT-gauge) + * @param _balancerGauge Address of the Balancer Pool Token Gauge. eg mBPT Gauge (mBPT-gauge) */ constructor( address _nexus, @@ -69,10 +69,9 @@ contract StakedTokenBPT is StakedToken { address _questManager, address _stakedToken, uint256 _cooldownSeconds, - uint256 _unstakeWindow, address[2] memory _bal, bytes32 _poolId, - address _balancerPoolGauge + address _balancerGauge ) StakedToken( _nexus, @@ -80,14 +79,13 @@ contract StakedTokenBPT is StakedToken { _questManager, _stakedToken, _cooldownSeconds, - _unstakeWindow, true ) { BAL = IERC20(_bal[0]); balancerVault = IBVault(_bal[1]); poolId = _poolId; - balancerPoolGauge = IBalancerPoolGauge(_balancerPoolGauge); + balancerGauge = IBalancerGauge(_balancerGauge); } /** @@ -108,12 +106,12 @@ contract StakedTokenBPT is StakedToken { } // Staking Token contract approves the Balancer Pool Gauge to transfer the staking token. eg mBPT - STAKED_TOKEN.safeApprove(address(balancerPoolGauge), type(uint256).max); + STAKED_TOKEN.safeApprove(address(balancerGauge), type(uint256).max); uint256 stakingBal = STAKED_TOKEN.balanceOf(address(this)); if (stakingBal > 0) { - balancerPoolGauge.deposit(stakingBal); + balancerGauge.deposit(stakingBal); } } @@ -125,7 +123,7 @@ contract StakedTokenBPT is StakedToken { * @dev Sets the recipient for any potential $BAL earnings */ function setBalRecipient(address _newRecipient) external onlyGovernor { - balancerPoolGauge.set_rewards_receiver(_newRecipient); + balancerGauge.set_rewards_receiver(_newRecipient); emit BalRecipientChanged(_newRecipient); } @@ -142,9 +140,10 @@ contract StakedTokenBPT is StakedToken { require(pendingBPT > 1, "no fees"); pendingBPTFees = 1; - // 1. Sell the BPT - uint256 stakingBalBefore = STAKED_TOKEN.balanceOf(address(this)); + // 1. Sell the mBPT + uint256 stakingBalBefore = balancerGauge.balanceOf(address(this)); uint256 mtaBalBefore = REWARDS_TOKEN.balanceOf(address(this)); + (address[] memory tokens, , ) = balancerVault.getPoolTokens(poolId); require(tokens[0] == address(REWARDS_TOKEN), "not MTA"); @@ -156,7 +155,11 @@ contract StakedTokenBPT is StakedToken { minOut[0] = (pendingBPT * priceCoefficient) / 11000; } - // 1.2. Exits to here, from here. Assumes token is in position 0 + // 1.2 Withdraw pending mBPT fees from the mBPT Gauge back to this mBPT staking contract + balancerGauge.withdraw(pendingBPT - 1); + + // 1.3. Exits rewards (MTA) to this staking contract for mBPT from this staking contract. + // Assumes rewards token (MTA) is in position 0 balancerVault.exitPool( poolId, address(this), @@ -165,13 +168,13 @@ contract StakedTokenBPT is StakedToken { ); // 2. Verify and update state - uint256 stakingBalAfter = STAKED_TOKEN.balanceOf(address(this)); + uint256 stakingBalAfter = balancerGauge.balanceOf(address(this)); require( stakingBalAfter == (stakingBalBefore - pendingBPT + 1), "< min BPT" ); - // 3. Inform HeadlessRewards about the new rewards + // 3. Inform HeadlessRewards about the new MTA rewards uint256 received = REWARDS_TOKEN.balanceOf(address(this)) - mtaBalBefore; require(received >= minOut[0], "< min MTA"); super._notifyAdditionalReward(received); @@ -180,14 +183,13 @@ contract StakedTokenBPT is StakedToken { } /** - * @dev Called by the child contract to notify of any additional rewards that have accrued. - * Trusts that this is called honestly. - * @param _additionalReward Units of additional RewardToken to add at the next notification + * @dev Called by `StakedToken._withdraw` to add early withdrawal fee charged in the staking token mBPT. + * @param _fees Units of staking token mBPT. */ - function _notifyAdditionalReward(uint256 _additionalReward) internal override { - require(_additionalReward < 1e24, "> mil"); + function _notifyAdditionalReward(uint256 _fees) internal override { + require(_fees < 1e24, "> mil"); - pendingBPTFees += _additionalReward; + pendingBPTFees += _fees; } /*************************************** @@ -261,21 +263,21 @@ contract StakedTokenBPT is StakedToken { ) internal override { STAKED_TOKEN.safeTransferFrom(_msgSender(), address(this), _amount); - balancerPoolGauge.deposit(_amount); + balancerGauge.deposit(_amount); _settleStake(_amount, _delegatee, _exitCooldown); } - function _transferStakedTokens( + function _withdrawStakedTokens( address _recipient, uint256 userWithdrawal ) internal override { - balancerPoolGauge.withdraw(userWithdrawal); + balancerGauge.withdraw(userWithdrawal); STAKED_TOKEN.safeTransfer(_recipient, userWithdrawal); } function _balanceOfStakedTokens() internal override view returns (uint256 stakedTokens) { - stakedTokens = balancerPoolGauge.balanceOf(address(this)); + stakedTokens = balancerGauge.balanceOf(address(this)); } } diff --git a/contracts/governance/staking/StakedTokenMTA.sol b/contracts/governance/staking/StakedTokenMTA.sol index 58362657..ebf97cd9 100644 --- a/contracts/governance/staking/StakedTokenMTA.sol +++ b/contracts/governance/staking/StakedTokenMTA.sol @@ -18,17 +18,16 @@ contract StakedTokenMTA is StakedToken, Initializable { /** * @param _nexus System nexus * @param _rewardsToken Token that is being distributed as a reward. eg MTA + * @param _questManager Centralised manager of quests * @param _stakedToken Core token that is staked and tracked (e.g. MTA) * @param _cooldownSeconds Seconds a user must wait after she initiates her cooldown before withdrawal is possible - * @param _unstakeWindow Window in which it is possible to withdraw, following the cooldown period */ constructor( address _nexus, address _rewardsToken, address _questManager, address _stakedToken, - uint256 _cooldownSeconds, - uint256 _unstakeWindow + uint256 _cooldownSeconds ) StakedToken( _nexus, @@ -36,7 +35,6 @@ contract StakedTokenMTA is StakedToken, Initializable { _questManager, _stakedToken, _cooldownSeconds, - _unstakeWindow, false ) {} diff --git a/contracts/peripheral/Balancer/IBalancerPoolGauge.sol b/contracts/peripheral/Balancer/IBalancerGauge.sol similarity index 94% rename from contracts/peripheral/Balancer/IBalancerPoolGauge.sol rename to contracts/peripheral/Balancer/IBalancerGauge.sol index f4d59a26..8f4ba41b 100644 --- a/contracts/peripheral/Balancer/IBalancerPoolGauge.sol +++ b/contracts/peripheral/Balancer/IBalancerGauge.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.6; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -interface IBalancerPoolGauge is IERC20 { +interface IBalancerGauge is IERC20 { /** * @notice Deposit `_value` LP tokens. eg mBPT. * @param _value Number of tokens to deposit diff --git a/contracts/z_mocks/shared/MockBalancerPoolGauge.sol b/contracts/z_mocks/governance/MockBPTGauge.sol similarity index 86% rename from contracts/z_mocks/shared/MockBalancerPoolGauge.sol rename to contracts/z_mocks/governance/MockBPTGauge.sol index 3f6eb824..f5d4df06 100644 --- a/contracts/z_mocks/shared/MockBalancerPoolGauge.sol +++ b/contracts/z_mocks/governance/MockBPTGauge.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.6; -import { IBalancerPoolGauge } from "../../peripheral/Balancer/IBalancerPoolGauge.sol"; +import { IBalancerGauge } from "../../peripheral/Balancer/IBalancerGauge.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -contract MockBalancerPoolGauge is IBalancerPoolGauge, ERC20 { +contract MockBPTGauge is IBalancerGauge, ERC20 { IERC20 public immutable stakedToken; mapping (address => address) public rewards_receiver; diff --git a/contracts/z_mocks/governance/MockStakedTokenWithPrice.sol b/contracts/z_mocks/governance/MockStakedTokenWithPrice.sol index 7c7231d0..eb9a2031 100644 --- a/contracts/z_mocks/governance/MockStakedTokenWithPrice.sol +++ b/contracts/z_mocks/governance/MockStakedTokenWithPrice.sol @@ -21,8 +21,7 @@ contract MockStakedTokenWithPrice is StakedToken, Initializable { address _rewardsToken, address _questManager, address _stakedToken, - uint256 _cooldownSeconds, - uint256 _unstakeWindow + uint256 _cooldownSeconds ) StakedToken( _nexus, @@ -30,7 +29,6 @@ contract MockStakedTokenWithPrice is StakedToken, Initializable { _questManager, _stakedToken, _cooldownSeconds, - _unstakeWindow, true ) {} diff --git a/tasks/utils/rewardsUtils.ts b/tasks/utils/rewardsUtils.ts index 07d7e845..74b968b5 100644 --- a/tasks/utils/rewardsUtils.ts +++ b/tasks/utils/rewardsUtils.ts @@ -118,18 +118,11 @@ export const deployStakingToken = async ( const stakedTokenLibraryAddresses = { "contracts/rewards/staking/PlatformTokenVendorFactory.sol:PlatformTokenVendorFactory": platformTokenVendorFactoryAddress, } - let constructorArguments: [string,string,string,string,BigNumberish, BigNumberish, string[]?,string?] + let constructorArguments: [string, string, string, string, BigNumberish, [string, string]?, string?, string?] let stakedTokenImpl: Contract let data: string if (rewardsTokenAddress === stakedTokenAddress) { - constructorArguments = [ - nexusAddress, - rewardsTokenAddress, - questManagerAddress, - rewardsTokenAddress, - stakedTokenData.cooldown, - stakedTokenData.unstakeWindow, - ] + constructorArguments = [nexusAddress, rewardsTokenAddress, questManagerAddress, rewardsTokenAddress, stakedTokenData.cooldown] stakedTokenImpl = await deployContract( new StakedTokenMTA__factory(stakedTokenLibraryAddresses, deployer.signer), @@ -146,7 +139,7 @@ export const deployStakingToken = async ( const balPoolId = resolveAddress("BalancerStakingPoolId", chain) const balancerVaultAddress = resolveAddress("BalancerVault", chain) - const balancerRecipientAddress = resolveAddress("BalancerRecipient", chain) + const balancerGaugeAddress = resolveAddress("mBPT", chain, "gauge") constructorArguments = [ nexusAddress, @@ -154,9 +147,9 @@ export const deployStakingToken = async ( questManagerAddress, stakedTokenAddress, stakedTokenData.cooldown, - stakedTokenData.unstakeWindow, [balAddress, balancerVaultAddress], balPoolId, + balancerGaugeAddress, ] console.log(`Staked Token BPT contract size ${StakedTokenBPT__factory.bytecode.length / 2} bytes`) @@ -167,12 +160,11 @@ export const deployStakingToken = async ( constructorArguments, ) - const priceCoeff = 42550 + const priceCoeff = 49631 data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ formatBytes32String(stakedTokenData.name), formatBytes32String(stakedTokenData.symbol), rewardsDistributorAddress, - balancerRecipientAddress, priceCoeff, ]) } @@ -194,6 +186,8 @@ export const deployStakingToken = async ( ]) } + console.log(`Governor needs to call setBalRecipient`) + return { stakedToken: proxy?.address, questManager: questManagerAddress, diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index e3bd8060..7ba7b800 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -29,6 +29,7 @@ export interface Token { bridgeForwarder?: string // Mainnet contract that forwards MTA rewards from the Emissions Controller to the L2 Bridge bridgeRecipient?: string // L2 contract that receives bridge MTA rewards from the L2 Bridge priceGetter?: string // Contract for price of asset, used for NonPeggedFeederPool + gauge?: string // Curve or Balancer gauge for rewards } export function isToken(asset: unknown): asset is Token { @@ -46,6 +47,7 @@ export const assetAddressTypes = [ "platformTokenVendor", "bridgeForwarder", "bridgeRecipient", + "gauge", ] as const export type AssetAddressTypes = typeof assetAddressTypes[number] @@ -498,6 +500,7 @@ export const mBPT: Token = { decimals: 18, quantityFormatter: "USD", vault: "0xeFbe22085D9f29863Cfb77EEd16d3cC0D927b011", + gauge: "0xbeC2d02008Dc64A6AD519471048CF3D3aF5ca0C5", } export const RmBPT: Token = { diff --git a/test/governance/staking/staked-token-bpt.spec.ts b/test/governance/staking/staked-token-bpt.spec.ts index a0ce9054..943532be 100644 --- a/test/governance/staking/staked-token-bpt.spec.ts +++ b/test/governance/staking/staked-token-bpt.spec.ts @@ -18,21 +18,21 @@ import { MockEmissionController__factory, MockBPT, MockBPT__factory, + MockBPTGauge__factory, + MockBPTGauge, MockBVault, MockBVault__factory, StakedTokenBPT__factory, StakedTokenBPT, } from "types" import { DEAD_ADDRESS } from "index" -import { ONE_DAY, ONE_WEEK } from "@utils/constants" +import { ONE_WEEK } from "@utils/constants" import { assertBNClose } from "@utils/assertions" import { simpleToExactAmount, BN } from "@utils/math" import { expect } from "chai" import { getTimestamp, increaseTime } from "@utils/time" import { formatBytes32String } from "ethers/lib/utils" import { BalConfig, UserStakingData } from "types/stakedToken" -import { MockBalancerPoolGauge__factory } from "types/generated/factories/MockBalancerPoolGauge__factory" -import { MockBalancerPoolGauge } from "types/generated/MockBalancerPoolGauge" interface Deployment { stakedToken: StakedTokenBPT @@ -45,7 +45,7 @@ interface BPTDeployment { bpt: MockBPT bal: MockERC20 underlying: MockERC20[] - gauge: MockBalancerPoolGauge + gauge: MockBPTGauge } describe("Staked Token BPT", () => { @@ -63,7 +63,7 @@ describe("Staked Token BPT", () => { const token2 = await new MockERC20__factory(sa.default.signer).deploy("Test Token 2", "TST2", 18, sa.default.address, 10000000) const mockBal = await new MockERC20__factory(sa.default.signer).deploy("Mock BAL", "mkBAL", 18, sa.default.address, 10000000) const bptLocal = await new MockBPT__factory(sa.default.signer).deploy("Balance Pool Token", "mBPT") - const mockBptGauge = await new MockBalancerPoolGauge__factory(sa.default.signer).deploy(bptLocal.address) + const mockBptGauge = await new MockBPTGauge__factory(sa.default.signer).deploy(bptLocal.address) const vault = await new MockBVault__factory(sa.default.signer).deploy() await mockMTA.approve(vault.address, simpleToExactAmount(100000)) await token2.approve(vault.address, simpleToExactAmount(100000)) @@ -108,7 +108,6 @@ describe("Staked Token BPT", () => { questManagerProxy.address, bptLocal.bpt.address, ONE_WEEK, - ONE_DAY.mul(2), ) data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ formatBytes32String("Staked Rewards"), @@ -126,7 +125,6 @@ describe("Staked Token BPT", () => { questManagerProxy.address, bptLocal.bpt.address, ONE_WEEK, - ONE_DAY.mul(2), [bptLocal.bal.address, bptLocal.vault.address], await bptLocal.vault.poolIds(bptLocal.bpt.address), bptLocal.gauge.address, @@ -166,8 +164,6 @@ describe("Staked Token BPT", () => { const priceCoefficient = await stakedToken.priceCoefficient() const lastPriceUpdateTime = await stakedToken.lastPriceUpdateTime() return { - // balRecipient, - // keeper, pendingBPTFees, priceCoefficient, lastPriceUpdateTime, @@ -201,8 +197,6 @@ describe("Staked Token BPT", () => { const accounts = await ethers.getSigners() const mAssetMachine = await new MassetMachine().initAccounts(accounts) sa = mAssetMachine.sa - - console.log(`StakedTokenBPT contract size ${StakedTokenBPT__factory.bytecode.length / 2}`) }) // '''..................................................................''' @@ -215,17 +209,100 @@ describe("Staked Token BPT", () => { }) it("post initialize", async () => { const data = await snapBalData() - expect(await stakedToken.BAL()).eq(bpt.bal.address) - expect(await stakedToken.balancerVault()).eq(bpt.vault.address) - expect(await stakedToken.poolId()).eq(await bpt.vault.poolIds(bpt.bpt.address)) - // expect(data.balRecipient).eq(sa.fundManager.address) - // expect(data.keeper).eq(ZERO_ADDRESS) + expect(await stakedToken.BAL(), "BAL token").eq(bpt.bal.address) + expect(await stakedToken.balancerVault(), "Balancer Vault").eq(bpt.vault.address) + expect(await stakedToken.poolId(), "Balancer Pool ID").eq(await bpt.vault.poolIds(bpt.bpt.address)) + expect(await stakedToken.balancerGauge(), "BPT Gauge").eq(bpt.gauge.address) expect(data.pendingBPTFees).eq(0) expect(data.priceCoefficient).eq(44000) expect(data.lastPriceUpdateTime).eq(0) }) }) + // '''..................................................................''' + // '''................... Staking ......................''' + // '''..................................................................''' + + context("stake", () => { + const stakerStartingBptBal = simpleToExactAmount(10000) + const stakeAmount = simpleToExactAmount(100) + before(async () => { + ;({ stakedToken, questManager, bpt } = await redeployStakedToken()) + await bpt.bpt.approve(stakedToken.address, stakeAmount) + }) + it("stake mBPT with delegation", async () => { + expect(await stakedToken.balanceOf(sa.default.address), "staker's stkBPT bal before").to.eq(0) + expect(await stakedToken.getVotes(sa.default.address), "staker's votes before").to.eq(0) + expect(await stakedToken.getVotes(sa.dummy1.address), "delegatee's votes before").to.eq(0) + expect(await stakedToken.totalSupply(), "stkBPT's total supply before").to.eq(0) + expect(await bpt.bpt.balanceOf(sa.default.address), "staker's mBPT bal before").to.eq(stakerStartingBptBal) + expect(await bpt.bpt.balanceOf(bpt.gauge.address), "gauge's mBPT bal before").to.eq(0) + + await stakedToken.connect(sa.default.signer)["stake(uint256,address)"](stakeAmount, sa.dummy1.address) + + // Price coefficient is 44,000 and is scaled to 10,000 = 4.4 + const stakedBptAmount = stakeAmount.mul(44000).div(10000) + expect(await stakedToken.balanceOf(sa.default.address), "staker's stkBPT bal after").to.eq(stakedBptAmount) + expect(await stakedToken.getVotes(sa.default.address), "staker's votes after").to.eq(0) + expect(await stakedToken.getVotes(sa.dummy1.address), "delegatee's votes after").to.eq(stakedBptAmount) + expect(await stakedToken.totalSupply(), "stkBPT's total supply after").to.eq(stakedBptAmount) + + expect(await bpt.bpt.balanceOf(sa.default.address), "staker's mBPT bal after").to.eq(stakerStartingBptBal.sub(stakeAmount)) + expect(await bpt.bpt.balanceOf(stakedToken.address), "stkBPT's mBPT bal after").to.eq(0) + expect(await bpt.bpt.balanceOf(bpt.gauge.address), "gauge's mBPT bal after").to.eq(stakeAmount) + + expect(await bpt.gauge.balanceOf(sa.default.address), "staker's gauge bal after").to.eq(0) + expect(await bpt.gauge.balanceOf(stakedToken.address), "stkBPT's gauge bal after").to.eq(stakeAmount) + }) + }) + + // '''..................................................................''' + // '''................... Withdraw ......................''' + // '''..................................................................''' + + context("withdraw", () => { + const stakerStartingBptBal = simpleToExactAmount(10000) + const stakeAmount = simpleToExactAmount(100) + const withdrawAmount = simpleToExactAmount(80) + // Redemption fee starts at 7.5% and drops using a curve after 3 weeks + const expectedFees = withdrawAmount.sub(withdrawAmount.mul(1000).div(1075)) + before(async () => { + ;({ stakedToken, questManager, bpt } = await redeployStakedToken()) + await bpt.bpt.approve(stakedToken.address, stakeAmount) + await stakedToken.connect(sa.default.signer)["stake(uint256,address)"](stakeAmount, sa.dummy1.address) + await stakedToken.startCooldown(stakeAmount) + await increaseTime(ONE_WEEK.add(1)) + }) + it("withdraw mBPT to recipient", async () => { + expect(await bpt.bpt.balanceOf(sa.dummy2.address), "recipient's mBPT bal before").to.eq(0) + + const tx = await stakedToken.connect(sa.default.signer).withdraw(withdrawAmount, sa.dummy2.address, true, true) + + await expect(tx).to.emit(stakedToken, "Withdraw") + + // Price coefficient is 44,000 and is scaled to 10,000 = 4.4 + const stakedBptAmount = stakeAmount.sub(withdrawAmount).mul(44000).div(10000) + expect(await stakedToken.balanceOf(sa.default.address), "staker's stkBPT bal after").to.eq(stakedBptAmount) + expect(await stakedToken.balanceOf(sa.dummy2.address), "recipient's stkBPT bal after").to.eq(0) + expect(await stakedToken.getVotes(sa.default.address), "staker's votes after").to.eq(0) + expect(await stakedToken.getVotes(sa.dummy1.address), "delegatee's votes after").to.eq(stakedBptAmount) + expect(await stakedToken.getVotes(sa.dummy2.address), "recipient's votes after").to.eq(0) + expect(await stakedToken.totalSupply(), "stkBPT's total supply after").to.eq(stakedBptAmount) + + expect(await bpt.bpt.balanceOf(sa.default.address), "staker's mBPT bal after").to.eq(stakerStartingBptBal.sub(stakeAmount)) + expect(await bpt.bpt.balanceOf(sa.dummy2.address), "recipient's mBPT bal after").to.eq(withdrawAmount.sub(expectedFees)) + expect(await bpt.bpt.balanceOf(stakedToken.address), "stkBPT's mBPT bal after").to.eq(0) + expect(await bpt.bpt.balanceOf(bpt.gauge.address), "gauge's mBPT bal after").to.eq( + stakeAmount.sub(withdrawAmount).add(expectedFees), + ) + + expect(await bpt.gauge.balanceOf(sa.default.address), "staker's gauge bal after").to.eq(0) + expect(await bpt.gauge.balanceOf(stakedToken.address), "stkBPT's gauge bal after").to.eq( + stakeAmount.sub(withdrawAmount).add(expectedFees), + ) + }) + }) + // '''..................................................................''' // '''................... BAL TOKENS ......................''' // '''..................................................................''' @@ -240,7 +317,6 @@ describe("Staked Token BPT", () => { await expect(stakedToken.setBalRecipient(sa.fundManager.address)).to.be.revertedWith("Only governor can execute") const tx = stakedToken.connect(sa.governor.signer).setBalRecipient(sa.fundManager.address) await expect(tx).to.emit(stakedToken, "BalRecipientChanged").withArgs(sa.fundManager.address) - // expect(await stakedToken.balRecipient()).to.eq(sa.fundManager.address) }) }) @@ -264,11 +340,13 @@ describe("Staked Token BPT", () => { expectedMTA = expectedFees.mul(data.balData.priceCoefficient).div(12000) }) it("should collect 7.5% as fees", async () => { - expect(await stakedToken.pendingAdditionalReward()).eq(0) - expect(data.balData.pendingBPTFees).eq(expectedFees) + expect(await stakedToken.pendingAdditionalReward(), "MTA rewards").eq(0) + expect(data.balData.pendingBPTFees, "mBPT fees").eq(expectedFees) + expect(await bpt.bpt.balanceOf(bpt.gauge.address), "gauge's mBPT bal").to.eq(expectedFees) + expect(await bpt.gauge.balanceOf(stakedToken.address), "stkBPT's gauge bal").to.eq(expectedFees) }) it("should convert fees back into $MTA", async () => { - const bptBalBefore = await bpt.bpt.balanceOf(stakedToken.address) + const bptBalBefore = await bpt.bpt.balanceOf(bpt.gauge.address) const mtaBalBefore = await rewardToken.balanceOf(stakedToken.address) const tx = stakedToken.convertFees() // it should emit the event @@ -280,7 +358,7 @@ describe("Staked Token BPT", () => { expect(await stakedToken.pendingAdditionalReward()).gt(expectedMTA) // should burn bpt and receive mta - const bptBalAfter = await bpt.bpt.balanceOf(stakedToken.address) + const bptBalAfter = await bpt.bpt.balanceOf(bpt.gauge.address) const mtaBalAfter = await rewardToken.balanceOf(stakedToken.address) expect(mtaBalAfter.sub(mtaBalBefore)).gt(expectedMTA) expect(mtaBalAfter).eq(await stakedToken.pendingAdditionalReward()) @@ -292,7 +370,7 @@ describe("Staked Token BPT", () => { expect(await stakedToken.pendingAdditionalReward()).eq(1) }) it("should fail if there is nothing to collect", async () => { - await expect(stakedToken.convertFees()).to.be.revertedWith("Must have something to convert") + await expect(stakedToken.convertFees()).to.be.revertedWith("no fees") }) }) diff --git a/test/governance/staking/staked-token-mta.spec.ts b/test/governance/staking/staked-token-mta.spec.ts index 5907dfe5..76fd9e2f 100644 --- a/test/governance/staking/staked-token-mta.spec.ts +++ b/test/governance/staking/staked-token-mta.spec.ts @@ -78,7 +78,6 @@ describe("Staked Token MTA rewards", () => { questManagerProxy.address, rewardToken.address, ONE_WEEK, - ONE_DAY.mul(2), ) data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ formatBytes32String("Staked Rewards"), diff --git a/test/governance/staking/staked-token.spec.ts b/test/governance/staking/staked-token.spec.ts index fd360bf2..6289c36b 100644 --- a/test/governance/staking/staked-token.spec.ts +++ b/test/governance/staking/staked-token.spec.ts @@ -102,7 +102,6 @@ describe("Staked Token", () => { questManagerProxy.address, rewardToken.address, ONE_WEEK, - ONE_DAY.mul(2), false, ) data = stakedTokenImpl.interface.encodeFunctionData("__StakedToken_init", [ @@ -177,7 +176,7 @@ describe("Staked Token", () => { expect(await stakedToken.STAKED_TOKEN(), "staked token").to.eq(rewardToken.address) expect(await stakedToken.REWARDS_TOKEN(), "reward token").to.eq(rewardToken.address) expect(await stakedToken.COOLDOWN_SECONDS(), "cooldown").to.eq(ONE_WEEK) - expect(await stakedToken.UNSTAKE_WINDOW(), "unstake window").to.eq(ONE_DAY.mul(2)) + expect(await stakedToken.UNSTAKE_WINDOW(), "unstake window").to.eq(ONE_WEEK.mul(2)) expect(await stakedToken.questManager(), "quest manager").to.eq(questManager.address) expect(await stakedToken.hasPriceCoeff(), "price coeff").to.eq(false) @@ -1198,6 +1197,7 @@ describe("Staked Token", () => { expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) it("in unstake window", async () => { + // 1 week cooldown await increaseTime(ONE_DAY.mul(8)) const tx = await stakedToken.endCooldown() @@ -1212,7 +1212,8 @@ describe("Staked Token", () => { expect(stakerDataAfter.votes, "staked votes after").to.eq(stakedAmount) }) it("after unstake window", async () => { - await increaseTime(ONE_DAY.mul(12)) + // 1 week cooldown and 2 weeks unstake window + await increaseTime(ONE_DAY.mul(22)) const tx = await stakedToken.endCooldown() await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) @@ -1433,7 +1434,7 @@ describe("Staked Token", () => { }) it("after the unstake window", async () => { await stakedToken.startCooldown(stakedAmount) - await increaseTime(ONE_DAY.mul(9).add(60)) + await increaseTime(ONE_DAY.mul(22)) await expect(stakedToken.withdraw(withdrawAmount, sa.default.address, false, false)).to.revertedWith( "UNSTAKE_WINDOW_FINISHED", ) @@ -1680,7 +1681,7 @@ describe("Staked Token", () => { { stakedSeconds: ONE_DAY, expected: 75, desc: "1 day" }, { stakedSeconds: ONE_WEEK, expected: 75, desc: "1 week" }, { stakedSeconds: ONE_WEEK.mul(2), expected: 75, desc: "2 weeks" }, - { stakedSeconds: ONE_WEEK.mul(32).div(10), expected: 71.82458365, desc: "3.1 weeks" }, + { stakedSeconds: ONE_WEEK.mul(32).div(10), expected: 71.82458365, desc: "3.2 weeks" }, { stakedSeconds: ONE_WEEK.mul(10), expected: 29.77225575, desc: "10 weeks" }, { stakedSeconds: ONE_WEEK.mul(12), expected: 25, desc: "12 weeks" }, { stakedSeconds: ONE_WEEK.mul(47), expected: 0.26455763, desc: "47 weeks" },