diff --git a/contracts/p1/StRSR.sol b/contracts/p1/StRSR.sol index 906b8dccd7..99683b97c1 100644 --- a/contracts/p1/StRSR.sol +++ b/contracts/p1/StRSR.sol @@ -16,7 +16,7 @@ import "./mixins/Component.sol"; // solhint-disable max-states-count -/* +/** * @title StRSRP1 * @notice StRSR is an ERC20 token contract that allows people to stake their RSR as * over-collateralization behind an RToken. As compensation stakers receive a share of revenues @@ -64,6 +64,7 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab uint256 private totalStakes; // Total of all stakes {qStRSR} uint256 private stakeRSR; // Amount of RSR backing all stakes {qRSR} uint192 private stakeRate; // The exchange rate between stakes and RSR. D18{qStRSR/qRSR} + // DEPRECATED in 3.4.0 in favor of totalStakes / stakeRSR uint192 private constant MAX_STAKE_RATE = 1e9 * FIX_ONE; // 1e9 D18{qStRSR/qRSR} @@ -111,11 +112,7 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // ==== Invariants ==== // [total-stakes]: totalStakes == sum(bal[acct] for acct in bal) - // [max-stake-rate]: 0 < stakeRate <= MAX_STAKE_RATE - // [stake-rate]: if totalStakes == 0, then stakeRSR == 0 and stakeRate == FIX_ONE - // else, stakeRSR * stakeRate >= totalStakes * 1e18 - // (ie, stakeRSR covers totalStakes at stakeRate) - // + // [max-stake-rate]: 0 < divuu(totalStakes, stakeRSR) <= MAX_STAKE_RATE // [total-drafts]: totalDrafts == sum(draftSum(draft[acct]) for acct in draft) // [max-draft-rate]: 0 < draftRate <= MAX_DRAFT_RATE // [draft-rate]: if totalDrafts == 0, then draftRSR == 0 and draftRate == FIX_ONE @@ -218,9 +215,8 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // // effects: // stakeRSR' = stakeRSR + rsrAmount - // totalStakes' = stakeRSR' * stakeRate / 1e18 (as required by invariant) + // totalStakes' = stakeRSR' * totalStakes / stakeRSR (as required by invariant) // bal'[caller] = bal[caller] + (totalStakes' - totalStakes) - // stakeRate' = stakeRate (this could go without saying, but it's important!) // // actions: // rsr.transferFrom(account, this, rsrAmount) @@ -247,9 +243,7 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // effects: // totalStakes' = totalStakes - stakeAmount // bal'[caller] = bal[caller] - stakeAmount - // stakeRSR' = ceil(totalStakes' * 1e18 / stakeRate) - // stakeRate' = stakeRate (no change) - // + // stakeRSR' = ceil(totalStakes' * stakeRSR / totalStakes) // draftRSR' + stakeRSR' = draftRSR + stakeRSR // draftRate' = draftRate (no change) // totalDrafts' = floor(draftRSR' + draftRate' / 1e18) @@ -266,13 +260,17 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab _payoutRewards(); // ==== Compute changes to stakes and RSR accounting - // rsrAmount: how many RSR to move from the stake pool to the draft pool - // pick rsrAmount as big as we can such that (newTotalStakes <= newStakeRSR * stakeRate) + uint256 prevTotalStakes = totalStakes; _burn(account, stakeAmount); - // newStakeRSR: {qRSR} = D18 * {qStRSR} / D18{qStRSR/qRSR} - uint256 newStakeRSR = (FIX_ONE_256 * totalStakes + (stakeRate - 1)) / stakeRate; - uint256 rsrAmount = stakeRSR - newStakeRSR; + // rsrAmount: how many RSR to move from the stake pool to the draft pool + // pick rsrAmount so that (newStakeRSR >= stakeRSR * totalStakes / prevTotalStakes) + + // {qRSR} = {qRSR} * {qStRSR} / {qStRSR} + uint256 newStakeRSR = prevTotalStakes != 0 + ? (stakeRSR * totalStakes + (prevTotalStakes - 1)) / prevTotalStakes + : stakeAmount; + uint256 rsrAmount = newStakeRSR < stakeRSR ? stakeRSR - newStakeRSR : 0; stakeRSR = newStakeRSR; // Create draft @@ -321,8 +319,11 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab firstRemainingDraft[draftEra][account] = endId; // ==== Compute RSR amount + // rsrAmount: how many RSR to withdraw from the draft pool + // pick rsrAmount as big as we can such that (newDraftRSR >= newTotalDrafts / draftRate) + uint256 newTotalDrafts = totalDrafts - draftAmount; - // newDraftRSR: {qRSR} = {qDrafts} * D18 / D18{qDrafts/qRSR} + // {qRSR} = {qDrafts} * D18 / D18{qDrafts/qRSR} uint256 newDraftRSR = (newTotalDrafts * FIX_ONE_256 + (draftRate - 1)) / draftRate; uint256 rsrAmount = draftRSR - newDraftRSR; @@ -368,8 +369,11 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab firstRemainingDraft[draftEra][account] = endId; // ==== Compute RSR amount + // rsrAmount: how many RSR to move from the draft pool to the stake pool + // pick rsrAmount as big as we can such that (newDraftRSR >= newTotalDrafts / draftRate) + uint256 newTotalDrafts = totalDrafts - draftAmount; - // newDraftRSR: {qRSR} = {qDrafts} * D18 / D18{qDrafts/qRSR} + // {qRSR} = {qDrafts} * D18 / D18{qDrafts/qRSR} uint256 newDraftRSR = (newTotalDrafts * FIX_ONE_256 + (draftRate - 1)) / draftRate; uint256 rsrAmount = draftRSR - newDraftRSR; @@ -401,7 +405,6 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // effects, in two phases. Phase 1: (from x to x') // stakeRSR' = floor(stakeRSR * keepRatio) // totalStakes' = totalStakes - // stakeRate' = ceil(totalStakes' * 1e18 / stakeRSR') // // draftRSR' = floor(draftRSR * keepRatio) // totalDrafts' = totalDrafts @@ -413,7 +416,7 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // draftRSR'' = (draftRSR' <= MAX_DRAFT_RATE) ? draftRSR' : 0 // if draftRSR'' = 0, then totalDrafts'' = 0 and draftRate'' = FIX_ONE // stakeRSR'' = (stakeRSR' <= MAX_STAKE_RATE) ? stakeRSR' : 0 - // if stakeRSR'' = 0, then totalStakes'' = 0 and stakeRate'' = FIX_ONE + // if stakeRSR'' = 0, then totalStakes'' = 0 // // actions: // as (this), rsr.transfer(backingManager, seized) @@ -442,12 +445,14 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab stakeRSR -= stakeRSRToTake; seizedRSR = stakeRSRToTake; - // update stakeRate, possibly beginning a new stake era - if (stakeRSR != 0) { - // Downcast is safe: totalStakes is 1e38 at most so expression maximum value is 1e56 - stakeRate = uint192((FIX_ONE_256 * totalStakes + (stakeRSR - 1)) / stakeRSR); - } - if (stakeRSR == 0 || stakeRate > MAX_STAKE_RATE) { + // Removed in 3.4.0 + // // update stakeRate, possibly beginning a new stake era + // if (stakeRSR != 0) { + // // Downcast is safe: totalStakes is 1e38 at most so expression maximum value is 1e56 + // stakeRate = uint192((FIX_ONE_256 * totalStakes + (stakeRSR - 1)) / stakeRSR); + // } + + if (stakeRSR == 0 || (FIX_ONE * totalStakes) / stakeRSR > MAX_STAKE_RATE) { seizedRSR += stakeRSR; beginEra(); } @@ -489,8 +494,9 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab /// and the risk of it occurring is low enough that it is not worth the effort to mitigate. function resetStakes() external { _requireGovernanceOnly(); + uint256 _stakeRate = stakeRSR != 0 ? (FIX_ONE * totalStakes) / stakeRSR : FIX_ONE; require( - stakeRate <= MIN_SAFE_STAKE_RATE || stakeRate >= MAX_SAFE_STAKE_RATE, + _stakeRate <= MIN_SAFE_STAKE_RATE || _stakeRate >= MAX_SAFE_STAKE_RATE, "rate still safe" ); @@ -500,8 +506,9 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab /// @return D18{qRSR/qStRSR} The exchange rate between RSR and StRSR function exchangeRate() public view returns (uint192) { - // D18{qRSR/qStRSR} = D18 * D18 / D18{qStRSR/qRSR} - return (FIX_SCALE_SQ + (stakeRate / 2)) / stakeRate; // ROUND method + // downcast is safe: expression is at most 1e18 * 1e29 / 1 = 1e47 + // D18{qRSR/qStRSR} = D18 * {qRSR} / {qStRSR} + return uint192(totalStakes != 0 ? (FIX_ONE * stakeRSR) / totalStakes : FIX_ONE); } /// Return the maximum value of endId such that withdraw(endId) can immediately work @@ -566,7 +573,6 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab /// @dev do this by effecting stakeRSR and payoutLastPaid as appropriate, given the current /// value of rsrRewards() /// @dev perhaps astonishingly, this _isn't_ a refresher - // let // N = numPeriods; the number of whole rewardPeriods since the last payout // payout = rsrRewards() * (1 - (1 - rewardRatio)^N) (see [strsr-payout-formula]) @@ -574,8 +580,6 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // effects: // stakeRSR' = stakeRSR + payout // rsrRewards'() = rsrRewards() - payout (implicit in the code, but true) - // stakeRate' = ceil(totalStakes' * 1e18 / stakeRSR') (because [stake-rate]) - // unless totalStakes == 0 or stakeRSR == 0, in which case stakeRate' = FIX_ONE // totalStakes' = totalStakes // // [strsr-payout-formula]: @@ -613,15 +617,6 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab payoutLastPaid += numPeriods; rsrRewardsAtLastPayout = rsrRewards(); - // stakeRate else case: D18{qStRSR/qRSR} = {qStRSR} * D18 / {qRSR} - // downcast is safe: it's at most 1e38 * 1e18 = 1e56 - // untestable: - // the second half of the OR comparison is untestable because of the invariant: - // if totalStakes == 0, then stakeRSR == 0 - stakeRate = (stakeRSR == 0 || totalStakes == 0) - ? FIX_ONE - : uint192((totalStakes * FIX_ONE_256 + (stakeRSR - 1)) / stakeRSR); - emit RewardsPaid(payout); emit ExchangeRateSet(initRate, exchangeRate()); } @@ -644,7 +639,7 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // draftAmount: how many drafts to create and assign to the user // pick draftAmount as big as we can such that (newTotalDrafts <= newDraftRSR * draftRate) draftRSR += rsrAmount; - // newTotalDrafts: {qDrafts} = D18{qDrafts/qRSR} * {qRSR} / D18 + // {qDrafts} = D18{qDrafts/qRSR} * {qRSR} / D18 uint256 newTotalDrafts = (draftRate * draftRSR) / FIX_ONE; uint256 draftAmount = newTotalDrafts - totalDrafts; totalDrafts = newTotalDrafts; @@ -725,8 +720,8 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // stakeAmount: how many stRSR the user shall receive. // pick stakeAmount as big as we can such that (newTotalStakes <= newStakeRSR * stakeRate) uint256 newStakeRSR = stakeRSR + rsrAmount; - // newTotalStakes: {qStRSR} = D18{qStRSR/qRSR} * {qRSR} / D18 - uint256 newTotalStakes = (stakeRate * newStakeRSR) / FIX_ONE; + // {qStRSR} = {qStRSR} * {qRSR} / {qRSR} + uint256 newTotalStakes = stakeRSR != 0 ? (totalStakes * newStakeRSR) / stakeRSR : rsrAmount; uint256 stakeAmount = newTotalStakes - totalStakes; // Transfer RSR from account to this contract