diff --git a/packages/core/contracts/financial-templates/common/FundingRateApplier.sol b/packages/core/contracts/financial-templates/common/FundingRateApplier.sol index f6a2ed7852..bf6cb39f65 100644 --- a/packages/core/contracts/financial-templates/common/FundingRateApplier.sol +++ b/packages/core/contracts/financial-templates/common/FundingRateApplier.sol @@ -149,15 +149,13 @@ abstract contract FundingRateApplier is FeePayer { returns (FixedPoint.Unsigned memory totalBond) { require(fundingRate.proposalTime == 0, "Proposal in progress"); + _validateFundingRate(rate); - // Timestamp must be after the last funding rate update time, within the last 30 minutes, and it cannot be more - // than 90 seconds ahead of the block timestamp. + // Timestamp must be after the last funding rate update time, within the last 30 minutes. uint256 currentTime = getCurrentTime(); uint256 updateTime = fundingRate.updateTime; require( - timestamp > updateTime && - timestamp >= currentTime.sub(_getConfig().proposalTimePastLimit) && - timestamp <= currentTime.add(_getConfig().proposalTimeFutureLimit), + timestamp > updateTime && timestamp >= currentTime.sub(_getConfig().proposalTimePastLimit), "Invalid proposal time" ); @@ -169,6 +167,7 @@ abstract contract FundingRateApplier is FeePayer { // Set up optimistic oracle. bytes32 identifier = fundingRate.identifier; bytes memory ancillaryData = _getAncillaryData(); + // Note: requestPrice will revert if `timestamp` is less than the current block timestamp. optimisticOracle.requestPrice(identifier, timestamp, ancillaryData, collateralCurrency, 0); totalBond = FixedPoint.Unsigned( optimisticOracle.setBond( @@ -261,6 +260,22 @@ abstract contract FundingRateApplier is FeePayer { return fundingRate.rate; } + // Constraining the range of funding rates limits the PfC for any dishonest proposer and enhances the + // perpetual's security. For example, let's examine the case where the max and min funding rates + // are equivalent to +/- 500%/year. This 1000% funding rate range allows a 8.6% profit from corruption for a + // proposer who can deter honest proposers for 74 hours: + // 1000%/year / 360 days / 24 hours * 74 hours max attack time = ~ 8.6%. + // How would attack work? Imagine that the market is very volatile currently and that the "true" funding + // rate for the next 74 hours is -500%, but a dishonest proposer successfully proposes a rate of +500% + // (after a two hour liveness) and disputes honest proposers for the next 72 hours. This results in a funding + // rate error of 1000% for 74 hours, until the DVM can set the funding rate back to its correct value. + function _validateFundingRate(FixedPoint.Signed memory rate) internal view { + require( + rate.isLessThanOrEqual(_getConfig().maxFundingRate) && + rate.isGreaterThanOrEqual(_getConfig().minFundingRate) + ); + } + // Fetches a funding rate from the Store, determines the period over which to compute an effective fee, // and multiplies the current multiplier by the effective fee. // A funding rate < 1 will reduce the multiplier, and a funding rate of > 1 will increase the multiplier. diff --git a/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStore.sol b/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStore.sol index 14453ce0a6..7dba978539 100644 --- a/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStore.sol +++ b/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStore.sol @@ -40,7 +40,8 @@ contract ConfigStore is ConfigStoreInterface, Testable, Lockable, Ownable { uint256 rewardRate, uint256 proposerBond, uint256 timelockLiveness, - uint256 proposalTimeFutureLimit, + int256 maxFundingRate, + int256 minFundingRate, uint256 proposalTimePastLimit, uint256 proposalPassedTimestamp ); @@ -48,7 +49,8 @@ contract ConfigStore is ConfigStoreInterface, Testable, Lockable, Ownable { uint256 rewardRate, uint256 proposerBond, uint256 timelockLiveness, - uint256 proposalTimeFutureLimit, + int256 maxFundingRate, + int256 minFundingRate, uint256 proposalTimePastLimit ); @@ -112,7 +114,8 @@ contract ConfigStore is ConfigStoreInterface, Testable, Lockable, Ownable { newConfig.rewardRatePerSecond.rawValue, newConfig.proposerBondPct.rawValue, newConfig.timelockLiveness, - newConfig.proposalTimeFutureLimit, + newConfig.maxFundingRate.rawValue, + newConfig.minFundingRate.rawValue, newConfig.proposalTimePastLimit, pendingPassedTimestamp ); @@ -136,7 +139,8 @@ contract ConfigStore is ConfigStoreInterface, Testable, Lockable, Ownable { currentConfig.rewardRatePerSecond.rawValue, currentConfig.proposerBondPct.rawValue, currentConfig.timelockLiveness, - currentConfig.proposalTimeFutureLimit, + currentConfig.maxFundingRate.rawValue, + currentConfig.minFundingRate.rawValue, currentConfig.proposalTimePastLimit ); } @@ -153,24 +157,33 @@ contract ConfigStore is ConfigStoreInterface, Testable, Lockable, Ownable { // Use this method to constrain values with which you can set ConfigSettings. function _validateConfig(ConfigStoreInterface.ConfigSettings memory config) internal pure { - // Its hard to ascertain what are reasonable limits, into the future and the past, for proposal timestamps. - // So, we do not set any limits. + // We don't set limits on proposal timestamps because there are already natural limits: + // - Future: price requests to the OptimisticOracle must be in the past---we can't add further constraints. + // - Past: proposal times must always be after the last update time, and a reasonable past limit would be 30 + // mins, meaning that no proposal timestamp can be more than 30 minutes behind the current time. - // Make sure timelockLiveness is not too long, otherwise contract can might not be able to fix itself + // Make sure timelockLiveness is not too long, otherwise contract might not be able to fix itself // before a vulnerability drains its collateral. require(config.timelockLiveness <= 7 days && config.timelockLiveness >= 1 days, "Invalid timelockLiveness"); - // Upper limits for the reward and bond rates are estimated based on offline discussions, - // and it is expected that these hard-coded limits can change in future deployments. - // For a discussion thread, go [here](https://github.com/UMAprotocol/protocol/pull/2223#discussion_r530692149). - - // Proposer bond of 0.04% is based on a maximum expected funding rate error of 200%/year. - FixedPoint.Unsigned memory maxProposerBond = FixedPoint.fromUnscaledUint(4).div(1e4); - require(config.proposerBondPct.isLessThan(maxProposerBond), "Invalid proposerBondPct"); - - // Reward rate should be less than 100% a year => 100% / 360 days / 24 hours / 60 mins / 60 secs + // The reward rate should be modified as needed to incentivize honest proposers appropriately. + // Additionally, the rate should be less than 100% a year => 100% / 360 days / 24 hours / 60 mins / 60 secs // = 0.0000033 FixedPoint.Unsigned memory maxRewardRatePerSecond = FixedPoint.fromUnscaledUint(33).div(1e7); require(config.rewardRatePerSecond.isLessThan(maxRewardRatePerSecond), "Invalid rewardRatePerSecond"); + + // We don't set a limit on the proposer bond because it is a defense against dishonest proposers. If a proposer + // were to successfully propose a very high or low funding rate, then their PfC would be very high. The proposer + // could theoretically keep their "evil" funding rate alive indefinitely by continuously disputing honest + // proposers, so we would want to be able to set the proposal bond (equal to the dispute bond) higher than their + // PfC for each proposal liveness window. The downside of not limiting this is that the config store owner + // can set it arbitrarily high and preclude a new funding rate from ever coming in. We suggest setting the proposal + // bond based on the configuration's funding rate range like in this discussion: + // https://github.com/UMAprotocol/protocol/issues/2039#issuecomment-719734383 + + // We also don't set a limit on the funding rate max/min because we might need to allow very high magnitude + // funding rates in extraordinarily volatile market situations. Note, that even though we do not bound + // the max/min, we still recommend that the deployer of this contract set the funding rate max/min values + // to bound the PfC of a dishonest proposer. A reasonable range might be the equivalent of [+200%/year, -200%/year]. } } diff --git a/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStoreInterface.sol b/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStoreInterface.sol index 443362b6e5..f442a70857 100644 --- a/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStoreInterface.sol +++ b/packages/core/contracts/financial-templates/perpetual-multiparty/ConfigStoreInterface.sol @@ -13,8 +13,10 @@ interface ConfigStoreInterface { FixedPoint.Unsigned rewardRatePerSecond; // Bond % (of given contract's PfC) that must be staked by proposers. Percentage of 1, e.g. 0.0005 is 0.05%. FixedPoint.Unsigned proposerBondPct; - // Funding rate proposal timestamp cannot be more than this amount of seconds in the future . - uint256 proposalTimeFutureLimit; + // Maximum funding rate % per second that can be proposed. + FixedPoint.Signed maxFundingRate; + // Minimum funding rate % per second that can be proposed. + FixedPoint.Signed minFundingRate; // Funding rate proposal timestamp cannot be more than this amount of seconds in the past from the latest // update time. uint256 proposalTimePastLimit; diff --git a/packages/core/contracts/financial-templates/perpetual-multiparty/PerpetualLiquidatable.sol b/packages/core/contracts/financial-templates/perpetual-multiparty/PerpetualLiquidatable.sol index 5dac019c46..efac808835 100644 --- a/packages/core/contracts/financial-templates/perpetual-multiparty/PerpetualLiquidatable.sol +++ b/packages/core/contracts/financial-templates/perpetual-multiparty/PerpetualLiquidatable.sol @@ -241,7 +241,7 @@ contract PerpetualLiquidatable is PerpetualPositionManager { PositionData storage positionToLiquidate = _getPositionData(sponsor); tokensLiquidated = FixedPoint.min(maxTokensToLiquidate, positionToLiquidate.tokensOutstanding); - require(tokensLiquidated.isGreaterThan(0), "Liquidating 0 tokens"); + require(tokensLiquidated.isGreaterThan(0)); // Starting values for the Position being liquidated. If withdrawal request amount is > position's collateral, // then set this to 0, otherwise set it to (startCollateral - withdrawal request amount). diff --git a/packages/core/test/financial-templates/common/FundingRateApplier.js b/packages/core/test/financial-templates/common/FundingRateApplier.js index f8e8604503..5ea8ce40cf 100644 --- a/packages/core/test/financial-templates/common/FundingRateApplier.js +++ b/packages/core/test/financial-templates/common/FundingRateApplier.js @@ -37,6 +37,8 @@ contract("FundingRateApplier", function(accounts) { const identifier = utf8ToHex("Test Identifier"); const initialUserBalance = toWei("100"); const defaultProposal = toWei("0.0000001"); // 1 percent every 100_000 seconds. + const maxFundingRate = toWei("0.00001"); + const minFundingRate = toWei("-0.00001"); const tokenScaling = toWei("1"); const proposalTimePastLimit = 1800; // 30 mins. const delay = 10000; // 10_000 seconds. @@ -100,7 +102,8 @@ contract("FundingRateApplier", function(accounts) { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: rewardRate }, proposerBondPct: { rawValue: bondPercentage }, - proposalTimeFutureLimit: 90, // Note: this doesn't affect the contract due to the stricter limits imposed by the optimistic oracle. + maxFundingRate: { rawValue: maxFundingRate }, + minFundingRate: { rawValue: minFundingRate }, proposalTimePastLimit: proposalTimePastLimit // 30 mins }, timer.address @@ -200,8 +203,15 @@ contract("FundingRateApplier", function(accounts) { assert.equal((await fundingRateApplier.fundingRate()).rate.rawValue.toString(), "0"); }); + it("Funding rate proposal must be within limits", async function() { + // Max/min funding rate per second is [< +1e-5, > -1e-5]. + const currentTime = (await fundingRateApplier.getCurrentTime()).toNumber(); + assert(await didContractThrow(fundingRateApplier.proposeNewRate({ rawValue: toWei("0.00002") }, currentTime))); + assert(await didContractThrow(fundingRateApplier.proposeNewRate({ rawValue: toWei("-0.00002") }, currentTime))); + }); + it("Proposal time checks", async () => { - const newRate = { rawValue: toWei("-0.0001") }; + const newRate = { rawValue: toWei("-0.000001") }; // Cannot be at or before the last update time. assert(await didContractThrow(fundingRateApplier.proposeNewRate(newRate, startTime))); diff --git a/packages/core/test/financial-templates/perpetual-multiparty/ConfigStore.js b/packages/core/test/financial-templates/perpetual-multiparty/ConfigStore.js index 38386ad8c2..5cabd2079b 100644 --- a/packages/core/test/financial-templates/perpetual-multiparty/ConfigStore.js +++ b/packages/core/test/financial-templates/perpetual-multiparty/ConfigStore.js @@ -29,14 +29,16 @@ contract("ConfigStore", function(accounts) { timelockLiveness: 86401, // 1 day + 1 second rewardRatePerSecond: { rawValue: toWei("0.000001") }, proposerBondPct: { rawValue: toWei("0.0001") }, - proposalTimeFutureLimit: 90, + maxFundingRate: { rawValue: toWei("0.00001") }, + minFundingRate: { rawValue: toWei("-0.00001") }, proposalTimePastLimit: 1800 // 30 mins }; let defaultConfig = { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: "0" }, proposerBondPct: { rawValue: "0" }, - proposalTimeFutureLimit: 0, + maxFundingRate: { rawValue: toWei("0") }, + minFundingRate: { rawValue: toWei("0") }, proposalTimePastLimit: 0 }; @@ -48,7 +50,8 @@ contract("ConfigStore", function(accounts) { _inputConfig.rewardRatePerSecond.rawValue.toString() ); assert.equal(currentConfig.proposerBondPct.rawValue.toString(), _inputConfig.proposerBondPct.rawValue.toString()); - assert.equal(currentConfig.proposalTimeFutureLimit.toString(), _inputConfig.proposalTimeFutureLimit.toString()); + assert.equal(currentConfig.maxFundingRate.rawValue.toString(), _inputConfig.maxFundingRate.rawValue.toString()); + assert.equal(currentConfig.minFundingRate.rawValue.toString(), _inputConfig.minFundingRate.rawValue.toString()); assert.equal(currentConfig.proposalTimePastLimit.toString(), _inputConfig.proposalTimePastLimit.toString()); } @@ -60,7 +63,8 @@ contract("ConfigStore", function(accounts) { _inputConfig.rewardRatePerSecond.rawValue.toString() ); assert.equal(pendingConfig.proposerBondPct.rawValue.toString(), _inputConfig.proposerBondPct.rawValue.toString()); - assert.equal(pendingConfig.proposalTimeFutureLimit.toString(), _inputConfig.proposalTimeFutureLimit.toString()); + assert.equal(pendingConfig.maxFundingRate.rawValue.toString(), _inputConfig.maxFundingRate.rawValue.toString()); + assert.equal(pendingConfig.minFundingRate.rawValue.toString(), _inputConfig.minFundingRate.rawValue.toString()); assert.equal(pendingConfig.proposalTimePastLimit.toString(), _inputConfig.proposalTimePastLimit.toString()); } @@ -79,7 +83,8 @@ contract("ConfigStore", function(accounts) { assert.equal(config.timelockLiveness.toString(), testConfig.timelockLiveness.toString()); assert.equal(config.rewardRatePerSecond.rawValue.toString(), testConfig.rewardRatePerSecond.rawValue); assert.equal(config.proposerBondPct.rawValue.toString(), testConfig.proposerBondPct.rawValue); - assert.equal(config.proposalTimeFutureLimit.toString(), testConfig.proposalTimeFutureLimit.toString()); + assert.equal(config.maxFundingRate.rawValue.toString(), testConfig.maxFundingRate.rawValue.toString()); + assert.equal(config.minFundingRate.rawValue.toString(), testConfig.minFundingRate.rawValue.toString()); assert.equal(config.proposalTimePastLimit.toString(), testConfig.proposalTimePastLimit.toString()); await storeHasNoPendingConfig(configStore); }); @@ -97,13 +102,6 @@ contract("ConfigStore", function(accounts) { rewardRatePerSecond: { rawValue: toWei("0.00000331") } }; assert(await didContractThrow(ConfigStore.new(invalidConfig, timer.address))); - - // Invalid proposal bond - invalidConfig = { - ...testConfig, - proposerBondPct: { rawValue: toWei("0.00041") } - }; - assert(await didContractThrow(ConfigStore.new(invalidConfig, timer.address))); }); }); describe("Proposing a new configuration", function() { @@ -124,7 +122,8 @@ contract("ConfigStore", function(accounts) { ev.proposerBond.toString() === testConfig.proposerBondPct.rawValue && ev.timelockLiveness.toString() === testConfig.timelockLiveness.toString() && ev.proposalPassedTimestamp.toString() === proposeTime.add(toBN(defaultConfig.timelockLiveness)).toString() && - ev.proposalTimeFutureLimit.toString() === testConfig.proposalTimeFutureLimit.toString() && + ev.maxFundingRate.toString() === testConfig.maxFundingRate.rawValue && + ev.minFundingRate.toString() === testConfig.minFundingRate.rawValue && ev.proposalTimePastLimit.toString() === testConfig.proposalTimePastLimit.toString() ); }); @@ -138,7 +137,8 @@ contract("ConfigStore", function(accounts) { ev.rewardRate.toString() === testConfig.rewardRatePerSecond.rawValue && ev.proposerBond.toString() === testConfig.proposerBondPct.rawValue && ev.timelockLiveness.toString() === testConfig.timelockLiveness.toString() && - ev.proposalTimeFutureLimit.toString() === testConfig.proposalTimeFutureLimit.toString() && + ev.maxFundingRate.toString() === testConfig.maxFundingRate.rawValue && + ev.minFundingRate.toString() === testConfig.minFundingRate.rawValue && ev.proposalTimePastLimit.toString() === testConfig.proposalTimePastLimit.toString() ); }); @@ -159,7 +159,8 @@ contract("ConfigStore", function(accounts) { ev.proposerBond.toString() === testConfig.proposerBondPct.rawValue && ev.timelockLiveness.toString() === testConfig.timelockLiveness.toString() && ev.proposalPassedTimestamp.toString() === proposeTime.add(toBN(defaultConfig.timelockLiveness)).toString() && - ev.proposalTimeFutureLimit.toString() === testConfig.proposalTimeFutureLimit.toString() && + ev.maxFundingRate.toString() === testConfig.maxFundingRate.rawValue && + ev.minFundingRate.toString() === testConfig.minFundingRate.rawValue && ev.proposalTimePastLimit.toString() === testConfig.proposalTimePastLimit.toString() ); }); @@ -199,7 +200,8 @@ contract("ConfigStore", function(accounts) { ev.timelockLiveness.toString() === test2Config.timelockLiveness.toString() && ev.proposalPassedTimestamp.toString() === overwriteProposalTime.add(toBN(defaultConfig.timelockLiveness)).toString() && - ev.proposalTimeFutureLimit.toString() === test2Config.proposalTimeFutureLimit.toString() && + ev.maxFundingRate.toString() === test2Config.maxFundingRate.rawValue && + ev.minFundingRate.toString() === test2Config.minFundingRate.rawValue && ev.proposalTimePastLimit.toString() === test2Config.proposalTimePastLimit.toString() ); }); @@ -231,7 +233,8 @@ contract("ConfigStore", function(accounts) { ev.rewardRate.toString() === test2Config.rewardRatePerSecond.rawValue && ev.proposerBond.toString() === test2Config.proposerBondPct.rawValue && ev.timelockLiveness.toString() === test2Config.timelockLiveness.toString() && - ev.proposalTimeFutureLimit.toString() === test2Config.proposalTimeFutureLimit.toString() && + ev.maxFundingRate.toString() === test2Config.maxFundingRate.rawValue && + ev.minFundingRate.toString() === test2Config.minFundingRate.rawValue && ev.proposalTimePastLimit.toString() === test2Config.proposalTimePastLimit.toString() ); }); diff --git a/packages/core/test/financial-templates/perpetual-multiparty/Perpetual.js b/packages/core/test/financial-templates/perpetual-multiparty/Perpetual.js index aaab13c8d8..5f5ce2c19c 100644 --- a/packages/core/test/financial-templates/perpetual-multiparty/Perpetual.js +++ b/packages/core/test/financial-templates/perpetual-multiparty/Perpetual.js @@ -26,7 +26,8 @@ contract("Perpetual", function(accounts) { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: "0" }, proposerBondPct: { rawValue: "0" }, - proposalTimeFutureLimit: 0, + maxFundingRate: { rawValue: "0" }, + minFundingRate: { rawValue: "0" }, proposalTimePastLimit: 0 }, timer.address diff --git a/packages/core/test/financial-templates/perpetual-multiparty/PerpetualCreator.js b/packages/core/test/financial-templates/perpetual-multiparty/PerpetualCreator.js index 9c1a4ee5af..f0dd33e8fb 100644 --- a/packages/core/test/financial-templates/perpetual-multiparty/PerpetualCreator.js +++ b/packages/core/test/financial-templates/perpetual-multiparty/PerpetualCreator.js @@ -26,11 +26,13 @@ contract("PerpetualCreator", function(accounts) { let collateralTokenWhitelist; // Re-used variables let constructorParams; + let testConfig = { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: toWei("0.000001") }, proposerBondPct: { rawValue: toWei("0.0001") }, - proposalTimeFutureLimit: 90, + maxFundingRate: { rawValue: toWei("0.00001") }, + minFundingRate: { rawValue: toWei("-0.00001") }, proposalTimePastLimit: 1800 }; @@ -321,4 +323,23 @@ contract("PerpetualCreator", function(accounts) { let configStore = await ConfigStore.at(configStoreAddress); assert.equal(await configStore.owner(), contractCreator); }); + it("Funding rate bounds are set correctly", async function() { + let createdAddressResult = await perpetualCreator.createPerpetual(constructorParams, testConfig, { + from: contractCreator + }); + + let perpetualAddress; + truffleAssert.eventEmitted(createdAddressResult, "CreatedPerpetual", ev => { + perpetualAddress = ev.perpetualAddress; + return ev.perpetualAddress != 0 && ev.deployerAddress == contractCreator; + }); + let perpetual = await Perpetual.at(perpetualAddress); + + const currentTime = (await perpetual.getCurrentTime()).toNumber(); + assert( + await didContractThrow( + perpetual.proposeNewRate({ rawValue: toWei("0.00002") }, currentTime, { from: contractCreator }) + ) + ); + }); }); diff --git a/packages/core/test/financial-templates/perpetual-multiparty/PerpetualLiquidatable.js b/packages/core/test/financial-templates/perpetual-multiparty/PerpetualLiquidatable.js index 30c744b047..b40dc0d82d 100644 --- a/packages/core/test/financial-templates/perpetual-multiparty/PerpetualLiquidatable.js +++ b/packages/core/test/financial-templates/perpetual-multiparty/PerpetualLiquidatable.js @@ -55,6 +55,8 @@ contract("PerpetualLiquidatable", function(accounts) { .muln(3); // In seconds const startTime = "15798990420"; const minSponsorTokens = toBN(toWei("1")); + const maxFundingRate = toWei("0.00001"); + const minFundingRate = toWei("-0.00001"); // Synthetic Token Position contract params const withdrawalLiveness = toBN(60) @@ -140,7 +142,8 @@ contract("PerpetualLiquidatable", function(accounts) { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: "0" }, proposerBondPct: { rawValue: "0" }, - proposalTimeFutureLimit: 0, + maxFundingRate: { rawValue: maxFundingRate }, + minFundingRate: { rawValue: minFundingRate }, proposalTimePastLimit: 0 }, timer.address diff --git a/packages/core/test/financial-templates/perpetual-multiparty/PerpetualPositionManager.js b/packages/core/test/financial-templates/perpetual-multiparty/PerpetualPositionManager.js index 88ebb8a1b7..2af52c8087 100644 --- a/packages/core/test/financial-templates/perpetual-multiparty/PerpetualPositionManager.js +++ b/packages/core/test/financial-templates/perpetual-multiparty/PerpetualPositionManager.js @@ -50,6 +50,8 @@ contract("PerpetualPositionManager", function(accounts) { const priceFeedIdentifier = utf8ToHex("TEST_IDENTIIFER"); const fundingRateRewardRate = toWei("0.000001"); const fundingRateFeedIdentifier = utf8ToHex("TEST_FUNDING_IDENTIFIER"); // example identifier for funding rate. + const maxFundingRate = toWei("0.00001"); + const minFundingRate = toWei("-0.00001"); const minSponsorTokens = "5"; // Conveniently asserts expected collateral and token balances, assuming that @@ -123,7 +125,8 @@ contract("PerpetualPositionManager", function(accounts) { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: "0" }, proposerBondPct: { rawValue: "0" }, - proposalTimeFutureLimit: 0, + maxFundingRate: { rawValue: maxFundingRate }, + minFundingRate: { rawValue: minFundingRate }, proposalTimePastLimit: 0 }, timer.address @@ -178,7 +181,8 @@ contract("PerpetualPositionManager", function(accounts) { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: fundingRateRewardRate }, proposerBondPct: { rawValue: "0" }, - proposalTimeFutureLimit: 0, + maxFundingRate: { rawValue: maxFundingRate }, + minFundingRate: { rawValue: minFundingRate }, proposalTimePastLimit: 0 }); @@ -807,7 +811,8 @@ contract("PerpetualPositionManager", function(accounts) { timelockLiveness: 86400, // 1 day rewardRatePerSecond: { rawValue: fundingRateRewardRate }, proposerBondPct: { rawValue: "0" }, - proposalTimeFutureLimit: 0, + maxFundingRate: { rawValue: maxFundingRate }, + minFundingRate: { rawValue: minFundingRate }, proposalTimePastLimit: 0 }); await setFundingRateAndAdvanceTime("0"); @@ -821,7 +826,7 @@ contract("PerpetualPositionManager", function(accounts) { await setFundingRateAndAdvanceTime("0"); await positionManager.applyFundingRate(); - // Clock has been advanced during the proposal by 10k seconds. The fee rate is one one-thousandth of a percent. + // Clock has been advanced during the proposal by 10k seconds. The reward rate is one one-thousandth of a percent. // This means the expected hit to the cumulativeFeeMultiplier is 1%. assert.equal((await positionManager.cumulativeFeeMultiplier()).toString(), toWei("0.99")); assert.equal((await positionManager.pfc()).toString(), toWei("1.98")); @@ -1057,76 +1062,75 @@ contract("PerpetualPositionManager", function(accounts) { assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.05")); - // Set the funding rate to a negative funding rate of -0.00001 in the store and apply it for 10k seconds. New funding rate - // should be 1.05 * (1 - -0.00001 * 10k) = 0.945 - await setFundingRateAndAdvanceTime(toWei("-0.00001")); + // Set the funding rate to a negative funding rate of -0.000001 in the store and apply it for 10k seconds. New funding rate + // should be 1.05 * (1 - -0.000001 * 10k) = 1.0395 + await setFundingRateAndAdvanceTime(toWei("-0.000001")); // Requesting withdraw should not update funding multipler await positionManager.requestWithdrawal({ rawValue: toWei("10") }, { from: sponsor }); assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.05")); // Apply the update await positionManager.applyFundingRate(); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.945")); + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.0395")); - // Setting the funding rate to zero (no payments made, synth trading at parity) should no change the cumulativeFundingRateMultiplier. + // Setting the funding rate to zero (no payments made, synth trading at parity) should not change the cumulativeFundingRateMultiplier. await setFundingRateAndAdvanceTime(toWei("0")); await positionManager.withdrawPassedRequest({ from: sponsor }); // call another function on the contract. - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.945")); + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.0395")); await setFundingRateAndAdvanceTime(toWei("0.000001")); // depositTo await positionManager.depositTo(sponsor, { rawValue: toWei("1") }, { from: other }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.95445")); // 0.945 * (1 + (1.01 - 1) * 1) = 0.95445 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.049895")); // 1.0395 * 1.01 = 1.049895 // deposit await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); await positionManager.deposit({ rawValue: toWei("1") }, { from: sponsor }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.9639945")); // 0.95445 * (1 + (1.01 - 1) * 1) = 0.9639945 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.06039395")); // 1.049895 * 1.01 = 1.06039395 // withdraw. To do a "fast" withdraw need to have the position above the GCR. await positionManager.create({ rawValue: toWei("200") }, { rawValue: toWei("100") }, { from: other }); // position above GCR await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); await positionManager.withdraw({ rawValue: toWei("1") }, { from: other }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.973634445")); // 0.9639945 * (1 + (1.0001 - 1) * 1) = 0.973634445 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.0709978895")); // 1.06039395 * 1.01 = 1.0709978895 // cancelWithdrawal await positionManager.requestWithdrawal({ rawValue: toWei("1") }, { from: other }); await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); await positionManager.cancelWithdrawal({ from: other }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.973634445")); // NO CHANGE -- withdraw requests do not affect. + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.0709978895")); // NO CHANGE -- withdraw requests do not affect. // Apply the funding rate to see the change. await positionManager.applyFundingRate(); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.98337078945")); // 0.973634445 * (1 + (1.0001 - 1) * 1) = 0.98337078945 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.081707868395")); // 1.0709978895 * 1.01 = 1.081707868395 // redeem await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); await positionManager.redeem({ rawValue: toWei("1") }, { from: sponsor }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("0.9932044973445")); // 0.98337078945 * (1 + (1.0001 - 1) * 1) = 0.9932044973445 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.09252494707895")); // 1.081707868395 * 1.01 = 1.09252494707895 // repay await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); await positionManager.repay({ rawValue: toWei("1") }, { from: sponsor }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.003136542317945")); // 0.9932044973445 * (1 + (1.0001 - 1) * 1) = 1.003136542317945 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.1034501965497395")); // 1.09252494707895 * 1.01 = 1.1034501965497395 // can directly call applyFundingRate await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); await positionManager.applyFundingRate({ from: other }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.01316790774112445")); // 1.003136542317945 * (1 + (1.0001 - 1) * 1) = 1.01316790774112445 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.114484698515236895")); // 1.1034501965497395 * 1.01 = 1.114484698515236895 // emergencyShutdown await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); - const shutdownTimestamp = Number(await positionManager.getCurrentTime()); - await positionManager.setCurrentTime(shutdownTimestamp); await financialContractsAdmin.callEmergencyShutdown(positionManager.address); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.023299586818535694")); // 1.01316790774112445 * (1 + (1.0001 - 1) * 1) = 1.023299586818535694(5) truncated + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.125629545500389263")); // 1.114484698515236895 * 1.01 = 1.125629545500389263(954) truncated // settleEmergencyShutdown SHOULD NOT update the cumulativeFundingRateMultiplier as emergency shutdown locks all state variables. + const shutdownTimestamp = Number(await positionManager.getCurrentTime()); await timer.setCurrentTime((await timer.getCurrentTime()).add(toBN(10000)).toString()); await mockOracle.pushPrice(priceFeedIdentifier, shutdownTimestamp, toWei("1.1")); await positionManager.settleEmergencyShutdown({ from: tokenHolder }); - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.023299586818535694")); // same as previous assert + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.125629545500389263")); // same as previous assert }); it("cumulativeFundingRateMultiplier is correctly applied to emergency shutdown settlement price", async function() { @@ -1140,8 +1144,8 @@ contract("PerpetualPositionManager", function(accounts) { const tokenHolderTokens = toWei("50"); await tokenCurrency.transfer(tokenHolder, tokenHolderTokens, { from: sponsor }); - // Add a funding rate to the fundingRateStore. let's say a value of 0.005% per second. This advances time by 10k seconds. - await setFundingRateAndAdvanceTime(toWei("0.00005")); + // Add a funding rate to the fundingRateStore. let's say a value of 0.0005% per second. This advances time by 10k seconds. + await setFundingRateAndAdvanceTime(toWei("0.000005")); // Some time passes and the UMA token holders decide that Emergency shutdown needs to occur. const shutdownTimestamp = Number(await positionManager.getCurrentTime()); @@ -1151,7 +1155,7 @@ contract("PerpetualPositionManager", function(accounts) { await financialContractsAdmin.callEmergencyShutdown(positionManager.address); // Cumulative funding rate multiplier should have been updated accordingly. - assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.5")); // 1 * (1 + (1.0005 - 1000) * 1) = 1.5 + assert.equal((await positionManager.fundingRate()).cumulativeMultiplier.toString(), toWei("1.05")); // 1 * (1 + (0.000005 * 10000)) = 1.05 // UMA token holders now vote to resolve of the price request to enable the emergency shutdown to continue. // Say they resolve to a price of 1.1 USD per synthetic token. @@ -1160,8 +1164,8 @@ contract("PerpetualPositionManager", function(accounts) { // Token holders (`sponsor` and `tokenHolder`) should now be able to withdraw post emergency shutdown. // From the token holder's perspective, they are entitled to the value of their tokens, notated in the underlying. // Their token debt value is effectively multiplied by the cumulativeFundingRateMultiplier to give the funding rate - // adjusted value of their debt. They have 50 tokens settled at a price of 1.1 should yield with a funding multiplier of 1.5 - // TRV = 50 * 1.1 * 1.5 = 82.5 + // adjusted value of their debt. They have 50 tokens settled at a price of 1.1 should yield with a funding multiplier of 1.05 + // TRV = 50 * 1.1 * 1.05 = 57.75 const tokenHolderInitialCollateral = await collateral.balanceOf(tokenHolder); const tokenHolderInitialSynthetic = await tokenCurrency.balanceOf(tokenHolder); assert.equal(tokenHolderInitialSynthetic, tokenHolderTokens); @@ -1174,7 +1178,7 @@ contract("PerpetualPositionManager", function(accounts) { assert.equal((await positionManager.emergencyShutdownPrice()).toString(), toWei("1.1")); const tokenHolderFinalCollateral = await collateral.balanceOf(tokenHolder); const tokenHolderFinalSynthetic = await tokenCurrency.balanceOf(tokenHolder); - const expectedTokenHolderFinalCollateral = toWei("82.5"); + const expectedTokenHolderFinalCollateral = toWei("57.75"); assert.equal( tokenHolderFinalCollateral.sub(tokenHolderInitialCollateral).toString(), expectedTokenHolderFinalCollateral.toString() @@ -1202,10 +1206,10 @@ contract("PerpetualPositionManager", function(accounts) { // For the sponsor, they are entitled to the underlying value of their remaining synthetic tokens scaled by the // funding rate multiplier + the excess collateral in their position at time of settlement. The sponsor had 150 units - // of collateral in their position and the final TRV of their synthetic debt is 100 * 1.1 * 1.5 (debt * price * funding rate multiplier). + // of collateral in their position and the final TRV of their synthetic debt is 100 * 1.1 * 1.05 (debt * price * funding rate multiplier). // Their redeemed amount for this excess collateral is the difference between the two. The sponsor also has 50 synthetic // tokens that they did not sell which will be redeemed.This makes their expected redemption: - // = 200 - (100 - 50) * 1.1 * 1.5 = 117.5 + // = 200 - (100 - 50) * 1.1 * 1.05 = 142.25 const sponsorInitialCollateral = await collateral.balanceOf(sponsor); const sponsorInitialSynthetic = await tokenCurrency.balanceOf(sponsor); @@ -1221,10 +1225,10 @@ contract("PerpetualPositionManager", function(accounts) { // The token Sponsor should gain the value of their synthetics in underlying // + their excess collateral from the over collateralization in their position - // Excess collateral = 200 - 100 * 1.1 * 1.5 = 35 - const expectedSponsorCollateralUnderlying = toBN(toWei("35")); - // Value of remaining synthetic tokens = 50 * 1.1 * 1.5 = 82.5 - const expectedSponsorCollateralSynthetic = toBN(toWei("82.5")); + // Excess collateral = 200 - 100 * 1.1 * 1.05 = 84.5 + const expectedSponsorCollateralUnderlying = toBN(toWei("84.5")); + // Value of remaining synthetic tokens = 50 * 1.1 * 1.05 = 57.75 + const expectedSponsorCollateralSynthetic = toBN(toWei("57.75")); const expectedTotalSponsorCollateralReturned = expectedSponsorCollateralUnderlying.add( expectedSponsorCollateralSynthetic );