Skip to content

Commit

Permalink
fix: convertFees in StakedTokenBPT
Browse files Browse the repository at this point in the history
  • Loading branch information
naddison36 committed Apr 13, 2022
1 parent d0d50bd commit d675522
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 92 deletions.
26 changes: 12 additions & 14 deletions contracts/governance/staking/StakedToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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?
Expand All @@ -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?
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
Expand Down
56 changes: 29 additions & 27 deletions contracts/governance/staking/StakedTokenBPT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -58,36 +59,33 @@ 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,
address _rewardsToken,
address _questManager,
address _stakedToken,
uint256 _cooldownSeconds,
uint256 _unstakeWindow,
address[2] memory _bal,
bytes32 _poolId,
address _balancerPoolGauge
address _balancerGauge
)
StakedToken(
_nexus,
_rewardsToken,
_questManager,
_stakedToken,
_cooldownSeconds,
_unstakeWindow,
true
)
{
BAL = IERC20(_bal[0]);
balancerVault = IBVault(_bal[1]);
poolId = _poolId;
balancerPoolGauge = IBalancerPoolGauge(_balancerPoolGauge);
balancerGauge = IBalancerGauge(_balancerGauge);
}

/**
Expand All @@ -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);
}
}

Expand All @@ -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);
}
Expand All @@ -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");

Expand All @@ -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),
Expand All @@ -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);
Expand All @@ -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;
}

/***************************************
Expand Down Expand Up @@ -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));
}
}
6 changes: 2 additions & 4 deletions contracts/governance/staking/StakedTokenMTA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,23 @@ 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,
_rewardsToken,
_questManager,
_stakedToken,
_cooldownSeconds,
_unstakeWindow,
false
)
{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 1 addition & 3 deletions contracts/z_mocks/governance/MockStakedTokenWithPrice.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,14 @@ contract MockStakedTokenWithPrice is StakedToken, Initializable {
address _rewardsToken,
address _questManager,
address _stakedToken,
uint256 _cooldownSeconds,
uint256 _unstakeWindow
uint256 _cooldownSeconds
)
StakedToken(
_nexus,
_rewardsToken,
_questManager,
_stakedToken,
_cooldownSeconds,
_unstakeWindow,
true
)
{}
Expand Down

0 comments on commit d675522

Please sign in to comment.