diff --git a/contracts/interfaces/IRToken.sol b/contracts/interfaces/IRToken.sol index faa09a6b5f..2c4c965980 100644 --- a/contracts/interfaces/IRToken.sol +++ b/contracts/interfaces/IRToken.sol @@ -144,6 +144,11 @@ interface TestIRToken is IRToken { function setRedemptionThrottleParams(ThrottleLib.Params calldata) external; + function setThrottleParams( + ThrottleLib.Params calldata issuanceParams, + ThrottleLib.Params calldata redemptionParams + ) external; + function issuanceThrottleParams() external view returns (ThrottleLib.Params memory); function redemptionThrottleParams() external view returns (ThrottleLib.Params memory); diff --git a/contracts/p0/RToken.sol b/contracts/p0/RToken.sol index 640fde3afe..bcc7c9f283 100644 --- a/contracts/p0/RToken.sol +++ b/contracts/p0/RToken.sol @@ -28,6 +28,7 @@ contract RTokenP0 is ComponentP0, ERC20PermitUpgradeable, IRToken { uint192 public constant MAX_THROTTLE_PCT_AMT = 1e18; // {qRTok} uint192 public constant MIN_EXCHANGE_RATE = 1e9; // D18{BU/rTok} uint192 public constant MAX_EXCHANGE_RATE = 1e27; // D18{BU/rTok} + uint192 public constant MIN_THROTTLE_DELTA = 25e16; // {1} 25% /// Weakly immutable: expected to be an IPFS link but could be the mandate itself string public mandate; @@ -54,8 +55,7 @@ contract RTokenP0 is ComponentP0, ERC20PermitUpgradeable, IRToken { __ERC20Permit_init(name_); mandate = mandate_; - setIssuanceThrottleParams(issuanceThrottleParams_); - setRedemptionThrottleParams(redemptionThrottleParams_); + setThrottleParams(issuanceThrottleParams_, redemptionThrottleParams_); issuanceThrottle.lastTimestamp = uint48(block.timestamp); redemptionThrottle.lastTimestamp = uint48(block.timestamp); @@ -340,25 +340,76 @@ contract RTokenP0 is ComponentP0, ERC20PermitUpgradeable, IRToken { /// @custom:governance function setIssuanceThrottleParams(ThrottleLib.Params calldata params) public governance { + _setIssuanceThrottleParams(params); + require( + isRedemptionThrottleGreaterByDelta(params, redemptionThrottle.params), + "redemption throttle too low" + ); + } + + /// @custom:governance + function setRedemptionThrottleParams(ThrottleLib.Params calldata params) public governance { + _setRedemptionThrottleParams(params); + require( + isRedemptionThrottleGreaterByDelta(issuanceThrottle.params, params), + "redemption throttle too low" + ); + } + + function setThrottleParams( + ThrottleLib.Params calldata issuanceParams, + ThrottleLib.Params calldata redemptionParams + ) public governance { + _setIssuanceThrottleParams(issuanceParams); + _setRedemptionThrottleParams(redemptionParams); + require( + isRedemptionThrottleGreaterByDelta(issuanceParams, redemptionParams), + "redemption throttle too low" + ); + } + + // === Private === + + function _setIssuanceThrottleParams(ThrottleLib.Params calldata params) private { require(params.amtRate >= MIN_THROTTLE_RATE_AMT, "issuance amtRate too small"); require(params.amtRate <= MAX_THROTTLE_RATE_AMT, "issuance amtRate too big"); require(params.pctRate <= MAX_THROTTLE_PCT_AMT, "issuance pctRate too big"); issuanceThrottle.useAvailable(totalSupply(), 0); + emit IssuanceThrottleSet(issuanceThrottle.params, params); issuanceThrottle.params = params; } /// @custom:governance - function setRedemptionThrottleParams(ThrottleLib.Params calldata params) public governance { + function _setRedemptionThrottleParams(ThrottleLib.Params calldata params) private { require(params.amtRate >= MIN_THROTTLE_RATE_AMT, "redemption amtRate too small"); require(params.amtRate <= MAX_THROTTLE_RATE_AMT, "redemption amtRate too big"); require(params.pctRate <= MAX_THROTTLE_PCT_AMT, "redemption pctRate too big"); redemptionThrottle.useAvailable(totalSupply(), 0); + emit RedemptionThrottleSet(redemptionThrottle.params, params); redemptionThrottle.params = params; } - // === Private === + /// @notice Checks if the redemption throttle is greater than the issuance throttle by the + /// required delta + /// @dev Compares both amtRate and pctRate individually to ensure each meets the minimum + /// delta requirement + /// @param issuance The issuance throttle parameters to compare against + /// @param redemption The redemption throttle parameters to check + /// @return bool True if redemption throttle is greater by at least MIN_THROTTLE_DELTA, + /// false otherwise + function isRedemptionThrottleGreaterByDelta( + ThrottleLib.Params memory issuance, + ThrottleLib.Params memory redemption + ) private pure returns (bool) { + uint256 requiredAmtRate = issuance.amtRate + + ((issuance.amtRate * MIN_THROTTLE_DELTA) / FIX_ONE); + uint256 requiredPctRate = issuance.pctRate + + ((issuance.pctRate * MIN_THROTTLE_DELTA) / FIX_ONE); + + return redemption.amtRate >= requiredAmtRate && redemption.pctRate >= requiredPctRate; + } /// Mint an amount of RToken equivalent to amtBaskets and scale basketsNeeded up /// @param recipient The address to receive the RTokens diff --git a/contracts/p1/RToken.sol b/contracts/p1/RToken.sol index 6e2dfa80a6..1c81c3b411 100644 --- a/contracts/p1/RToken.sol +++ b/contracts/p1/RToken.sol @@ -24,9 +24,10 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { uint192 public constant MAX_THROTTLE_PCT_AMT = 1e18; // {qRTok} uint192 public constant MIN_EXCHANGE_RATE = 1e9; // D18{BU/rTok} uint192 public constant MAX_EXCHANGE_RATE = 1e27; // D18{BU/rTok} + uint192 public constant MIN_THROTTLE_DELTA = 25e16; // {1} 25% /// The mandate describes what goals its governors should try to achieve. By succinctly - /// explaining the RToken’s purpose and what the RToken is intended to do, it provides common + /// explaining the RToken's purpose and what the RToken is intended to do, it provides common /// ground for the governors to decide upon priorities and how to weigh tradeoffs. /// /// Example Mandates: @@ -79,8 +80,7 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { furnace = main_.furnace(); mandate = mandate_; - setIssuanceThrottleParams(issuanceThrottleParams_); - setRedemptionThrottleParams(redemptionThrottleParams_); + setThrottleParams(issuanceThrottleParams_, redemptionThrottleParams_); issuanceThrottle.lastTimestamp = uint48(block.timestamp); redemptionThrottle.lastTimestamp = uint48(block.timestamp); @@ -461,6 +461,38 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { /// @custom:governance function setIssuanceThrottleParams(ThrottleLib.Params calldata params) public governance { + _setIssuanceThrottleParams(params); + require( + isRedemptionThrottleGreaterByDelta(params, redemptionThrottle.params), + "redemption throttle too low" + ); + } + + /// @custom:governance + function setRedemptionThrottleParams(ThrottleLib.Params calldata params) public governance { + _setRedemptionThrottleParams(params); + require( + isRedemptionThrottleGreaterByDelta(issuanceThrottle.params, params), + "redemption throttle too low" + ); + } + + /// @custom:governance + function setThrottleParams( + ThrottleLib.Params calldata issuanceParams, + ThrottleLib.Params calldata redemptionParams + ) public governance { + _setIssuanceThrottleParams(issuanceParams); + _setRedemptionThrottleParams(redemptionParams); + require( + isRedemptionThrottleGreaterByDelta(issuanceParams, redemptionParams), + "redemption throttle too low" + ); + } + + // === Private Helpers === + + function _setIssuanceThrottleParams(ThrottleLib.Params calldata params) private { require(params.amtRate >= MIN_THROTTLE_RATE_AMT, "issuance amtRate too small"); require(params.amtRate <= MAX_THROTTLE_RATE_AMT, "issuance amtRate too big"); require(params.pctRate <= MAX_THROTTLE_PCT_AMT, "issuance pctRate too big"); @@ -471,7 +503,7 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { } /// @custom:governance - function setRedemptionThrottleParams(ThrottleLib.Params calldata params) public governance { + function _setRedemptionThrottleParams(ThrottleLib.Params calldata params) private { require(params.amtRate >= MIN_THROTTLE_RATE_AMT, "redemption amtRate too small"); require(params.amtRate <= MAX_THROTTLE_RATE_AMT, "redemption amtRate too big"); require(params.pctRate <= MAX_THROTTLE_PCT_AMT, "redemption pctRate too big"); @@ -481,7 +513,25 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { redemptionThrottle.params = params; } - // ==== Private ==== + /// @notice Checks if the redemption throttle is greater than the issuance throttle by the + /// required delta + /// @dev Compares both amtRate and pctRate individually to ensure each meets the minimum + /// delta requirement + /// @param issuance The issuance throttle parameters to compare against + /// @param redemption The redemption throttle parameters to check + /// @return bool True if redemption throttle is greater by at least MIN_THROTTLE_DELTA, + /// false otherwise + function isRedemptionThrottleGreaterByDelta( + ThrottleLib.Params memory issuance, + ThrottleLib.Params memory redemption + ) private pure returns (bool) { + uint256 requiredAmtRate = issuance.amtRate + + ((issuance.amtRate * MIN_THROTTLE_DELTA) / FIX_ONE); + uint256 requiredPctRate = issuance.pctRate + + ((issuance.pctRate * MIN_THROTTLE_DELTA) / FIX_ONE); + + return redemption.amtRate >= requiredAmtRate && redemption.pctRate >= requiredPctRate; + } /// Mint an amount of RToken equivalent to amtBaskets and scale basketsNeeded up /// @param recipient The address to receive the RTokens diff --git a/test/Broker.test.ts b/test/Broker.test.ts index 6eafdd5cda..de0354cbe0 100644 --- a/test/Broker.test.ts +++ b/test/Broker.test.ts @@ -1449,10 +1449,11 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { const MAX_SELL_TOKEN_SCALED = toBNDecimals(MAX_ERC20_SUPPLY, Number(sellTokDecimals)) // Max out throttles - const issuanceThrottleParams = { amtRate: MAX_ERC20_SUPPLY, pctRate: 0 } + const issuanceThrottleParams = { amtRate: MAX_ERC20_SUPPLY.mul(80).div(100), pctRate: 0 } const redemptionThrottleParams = { amtRate: MAX_ERC20_SUPPLY, pctRate: 0 } - await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) await advanceTime(3600) // Mint coll tokens to addr1 diff --git a/test/Facade.test.ts b/test/Facade.test.ts index 9cb0d42fe1..5bce4b61e0 100644 --- a/test/Facade.test.ts +++ b/test/Facade.test.ts @@ -1339,10 +1339,6 @@ describe('Facade + FacadeMonitor contracts', () => { it('should return redemption available', async () => { const issueAmount = bn('100000e18') - // Decrease redemption allowed amount - const redeemThrottleParams = { amtRate: issueAmount.div(2), pctRate: fp('0.1') } // 50K - await rToken.connect(owner).setRedemptionThrottleParams(redeemThrottleParams) - // Check with no supply expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) expect(await rToken.redemptionAvailable()).to.equal(bn(0)) @@ -1351,8 +1347,13 @@ describe('Facade + FacadeMonitor contracts', () => { // Issue some RTokens await rToken.connect(addr1).issue(issueAmount) - // check throttles - redemption still fully available - expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('0.9')) + // Decrease redemption allowed amount + const issuanceThrottleParams = { amtRate: issueAmount.div(4), pctRate: fp('0.05') } // 25K + const redeemThrottleParams = { amtRate: issueAmount.div(2), pctRate: fp('0.1') } // 50K + await rToken.connect(owner).setThrottleParams(issuanceThrottleParams, redeemThrottleParams) + + // check throttles - issuance & redemption still fully available (because lower) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) // Redeem RTokens (50% of throttle) @@ -1416,7 +1417,10 @@ describe('Facade + FacadeMonitor contracts', () => { // Set issuance throttle to percent only const issuanceThrottleParams = { amtRate: fp('1'), pctRate: fp('0.1') } // 10% - await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) + const redemptionThrottleParams = { amtRate: fp('2'), pctRate: fp('0.2') } // 10% + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) // Advance time significantly await advanceTime(1000000000) @@ -1427,7 +1431,7 @@ describe('Facade + FacadeMonitor contracts', () => { expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) // Check redemption throttle unchanged - expect(await rToken.redemptionAvailable()).to.equal(config.redemptionThrottle.amtRate) + expect(await rToken.redemptionAvailable()).to.equal(supplyThrottle.mul(2)) expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) // Issuance #3 - Should be allowed, does not exceed supply restriction @@ -1437,18 +1441,15 @@ describe('Facade + FacadeMonitor contracts', () => { // Check issuance throttle updated - Previous issuances recharged expect(await rToken.issuanceAvailable()).to.equal(supplyThrottle.sub(issueAmount3)) - // Hourly Limit: 210K (10% of total supply of 2.1 M) // Available: 100 K / 201K (~ 0.47619) expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.be.closeTo( fp('0.476'), fp('0.001') ) - // Check redemption throttle unchanged - expect(await rToken.redemptionAvailable()).to.equal(config.redemptionThrottle.amtRate) + // expect(await rToken.redemptionAvailable()).to.equal(config.redemptionThrottle.amtRate) expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) - // Check all issuances are confirmed expect(await rToken.balanceOf(addr1.address)).to.equal( issueAmount1.add(issueAmount2).add(issueAmount3) @@ -1456,7 +1457,6 @@ describe('Facade + FacadeMonitor contracts', () => { // Advance time, issuance will recharge a bit await advanceTime(100) - // Now 50% of hourly limit available (~105.8K / 210 K) expect(await rToken.issuanceAvailable()).to.be.closeTo(fp('105800'), fp('100')) expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.be.closeTo( @@ -1483,12 +1483,8 @@ describe('Facade + FacadeMonitor contracts', () => { expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) // Check redemptions - // Set redemption throttle to percent only - const redemptionThrottleParams = { amtRate: fp('1'), pctRate: fp('0.1') } // 10% - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) - const totalSupply = await rToken.totalSupply() - expect(await rToken.redemptionAvailable()).to.equal(totalSupply.div(10)) // 10% + expect(await rToken.redemptionAvailable()).to.equal(totalSupply.div(5)) // 20% expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) // Redeem half of the available throttle @@ -1496,7 +1492,7 @@ describe('Facade + FacadeMonitor contracts', () => { // About 52% now used of redemption throttle expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.be.closeTo( - fp('0.52'), + fp('0.79'), fp('0.01') ) diff --git a/test/Furnace.test.ts b/test/Furnace.test.ts index 536a4066f6..7ecdc7742b 100644 --- a/test/Furnace.test.ts +++ b/test/Furnace.test.ts @@ -506,9 +506,12 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { await token3.connect(addr1).approve(rToken.address, max256) // Set up larger throttles - const throttle = { amtRate: bal.lt(fp('1')) ? fp('1') : bal, pctRate: 0 } - await rToken.connect(owner).setIssuanceThrottleParams(throttle) - await rToken.connect(owner).setRedemptionThrottleParams(throttle) + const issuanceThrottle = { amtRate: bal.lt(fp('1')) ? fp('1') : bal, pctRate: 0 } + const redemptionThrottle = { + amtRate: bal.lt(fp('1')) ? fp('1').mul(125).div(100) : bal.mul(125).div(100), + pctRate: 0, + } + await rToken.connect(owner).setThrottleParams(issuanceThrottle, redemptionThrottle) await advanceTime(3600) // Issue and send tokens to furnace diff --git a/test/RToken.test.ts b/test/RToken.test.ts index e895433213..188b403dbb 100644 --- a/test/RToken.test.ts +++ b/test/RToken.test.ts @@ -83,6 +83,13 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { let backingManager: TestIBackingManager let basketHandler: TestIBasketHandler + async function issueNTimes(n: number) { + for (let i = 0; i < n; i++) { + await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate) + await advanceTime(3600) + } + } + beforeEach(async () => { ;[owner, addr1, addr2, other] = await ethers.getSigners() @@ -185,7 +192,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should allow to update issuance throttle if Owner and perform validations', async () => { - const issuanceThrottleParams = { amtRate: fp('1'), pctRate: fp('0.1') } + const issuanceThrottleParams = { amtRate: fp('1'), pctRate: fp('0.01') } await expect( rToken.connect(addr1).setIssuanceThrottleParams(issuanceThrottleParams) ).to.be.revertedWith('governance only') @@ -196,7 +203,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(params[1]).to.equal(issuanceThrottleParams.pctRate) issuanceThrottleParams.amtRate = fp('2') - issuanceThrottleParams.pctRate = fp('1') + issuanceThrottleParams.pctRate = fp('0.02') await expect(rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams)) params = await rToken.issuanceThrottleParams() expect(params[0]).to.equal(issuanceThrottleParams.amtRate) @@ -224,7 +231,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { it('Should account for accrued value when updating issuance throttle parameters', async () => { await advanceTime(12 * 5 * 60) // 60 minutes, charge fully - const issuanceThrottleParams = { amtRate: fp('60'), pctRate: fp('0.1') } + const issuanceThrottleParams = { amtRate: fp('60'), pctRate: fp('0.01') } await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) const params = await rToken.issuanceThrottleParams() @@ -243,7 +250,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should allow to update redemption throttle if Owner and perform validations', async () => { - const redemptionThrottleParams = { amtRate: fp('1'), pctRate: fp('0.1') } + const redemptionThrottleParams = { amtRate: fp('10e6'), pctRate: fp('0.1') } await expect( rToken.connect(addr1).setRedemptionThrottleParams(redemptionThrottleParams) ).to.be.revertedWith('governance only') @@ -253,11 +260,13 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(params[0]).to.equal(redemptionThrottleParams.amtRate) expect(params[1]).to.equal(redemptionThrottleParams.pctRate) - redemptionThrottleParams.amtRate = fp('2') - redemptionThrottleParams.pctRate = fp('1') + redemptionThrottleParams.amtRate = fp('2e7') + redemptionThrottleParams.pctRate = fp('0.5') await expect(rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams)) params = await rToken.redemptionThrottleParams() + expect(params[0]).to.equal(redemptionThrottleParams.amtRate) + expect(params[1]).to.equal(redemptionThrottleParams.pctRate) // Cannot update with too small amtRate @@ -283,25 +292,28 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { it('Should account for accrued value when updating redemption throttle parameters', async () => { await advanceTime(12 * 5 * 60) // 60 minutes, charge fully const issuanceThrottleParams = { amtRate: fp('100'), pctRate: fp('0.1') } - const redemptionThrottleParams = { amtRate: fp('60'), pctRate: fp('0.1') } + const redemptionThrottleParams = { amtRate: fp('150'), pctRate: fp('0.2') } - await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) const params = await rToken.redemptionThrottleParams() expect(params[0]).to.equal(redemptionThrottleParams.amtRate) expect(params[1]).to.equal(redemptionThrottleParams.pctRate) await Promise.all(tokens.map((t) => t.connect(addr1).approve(rToken.address, initialBal))) await rToken.connect(addr1).issue(fp('100')) - expect(await rToken.redemptionAvailable()).to.equal(fp('60')) + await advanceTime(12 * 5 * 60) // 60 minutes, charge fully + await rToken.connect(addr1).issue(fp('100')) + expect(await rToken.redemptionAvailable()).to.equal(fp('150')) await rToken.connect(addr1).redeem(fp('30')) - expect(await rToken.redemptionAvailable()).to.equal(fp('30')) + expect(await rToken.redemptionAvailable()).to.equal(fp('120')) await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12 * 5 * 10) // 10 minutes - redemptionThrottleParams.amtRate = fp('100') + redemptionThrottleParams.amtRate = fp('180') await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) - expect(await rToken.redemptionAvailable()).to.equal(fp('40')) + expect(await rToken.redemptionAvailable()).to.equal(fp('145')) // 120 + 25 }) it('Should return a price of 0 if the assets become unregistered', async () => { @@ -397,13 +409,17 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should not allow overflow issuance -- regression test for C4 truncation bug', async function () { + const issuanceThrottleAmt = MAX_THROTTLE_AMT_RATE.mul(75).div(100) // Max out issuance throttle await rToken .connect(owner) - .setIssuanceThrottleParams({ amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 }) + .setThrottleParams( + { amtRate: issuanceThrottleAmt, pctRate: 0 }, + { amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 } + ) // Try to issue - await expect(rToken.connect(addr1).issue(MAX_THROTTLE_AMT_RATE.add(1))).to.be.revertedWith( + await expect(rToken.connect(addr1).issue(issuanceThrottleAmt.add(1))).to.be.revertedWith( 'supply change throttled' ) @@ -412,15 +428,15 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.basketsNeeded()).to.equal(0) // Issue under limit, ensure correct number of baskets is set and we do not overflow - await mintCollaterals(owner, [addr1], MAX_THROTTLE_AMT_RATE, basket) + await mintCollaterals(owner, [addr1], issuanceThrottleAmt, basket) await Promise.all( - tokens.map((t) => t.connect(addr1).approve(rToken.address, MAX_THROTTLE_AMT_RATE)) + tokens.map((t) => t.connect(addr1).approve(rToken.address, issuanceThrottleAmt)) ) // advance time await advanceTime(12 * 5 * 60) // 60 minutes, charge fully - await rToken.connect(addr1).issue(MAX_THROTTLE_AMT_RATE) - expect(await rToken.totalSupply()).to.equal(MAX_THROTTLE_AMT_RATE) - expect(await rToken.basketsNeeded()).to.equal(MAX_THROTTLE_AMT_RATE) + await rToken.connect(addr1).issue(issuanceThrottleAmt) + expect(await rToken.totalSupply()).to.equal(issuanceThrottleAmt) + expect(await rToken.basketsNeeded()).to.equal(issuanceThrottleAmt) }) it('Should not overflow BU exchange rate below 1e-9 on issue', async () => { @@ -537,7 +553,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // Check all available issuance consumed expect(await rToken.issuanceAvailable()).to.equal(bn(0)) // All can be redeemed - expect(await rToken.redemptionAvailable()).to.equal(config.redemptionThrottle.amtRate) + expect(await rToken.redemptionAvailable()).to.equal(issueAmount) }) it('Should revert if single issuance exceeds maximum allowed', async function () { @@ -941,16 +957,21 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { it('Should update issuance throttle correctly with redemptions - edge case', async function () { // set fixed redemption amount + const issuanceThrottleParams = JSON.parse(JSON.stringify(config.issuanceThrottle)) + issuanceThrottleParams.amtRate = fp('5e5') + issuanceThrottleParams.pctRate = bn(0) const redemptionThrottleParams = JSON.parse(JSON.stringify(config.redemptionThrottle)) redemptionThrottleParams.amtRate = fp('1e6') redemptionThrottleParams.pctRate = bn(0) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) // Provide approvals await Promise.all(tokens.map((t) => t.connect(addr1).approve(rToken.address, initialBal))) // Issuance throttle is fully charged - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) expect(await rToken.redemptionAvailable()).to.equal(bn(0)) // Set automine to false for multiple transactions in one block @@ -1261,7 +1282,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { beforeEach(async function () { redemptionThrottleParams = { - amtRate: fp('2'), // 2 RToken, + amtRate: fp('2e6'), // 2e6 RToken, pctRate: fp('0.1'), // 10% } await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) @@ -1274,7 +1295,13 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should calculate redemption limit correctly', async function () { - redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) + await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate.sub(issueAmount)) + await advanceTime(3600) + await issueNTimes(21) + redeemAmount = config.issuanceThrottle.amtRate + .mul(22) + .mul(redemptionThrottleParams.pctRate) + .div(fp('1')) expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) }) @@ -1303,12 +1330,19 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { it('Should revert on overly-large redemption #fast', async function () { redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) - expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) + expect(await rToken.redemptionAvailable()).to.equal(issueAmount) // Check issuance throttle - full expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) - redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) + await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate.sub(issueAmount)) + await advanceTime(3600) + await issueNTimes(21) + redeemAmount = config.issuanceThrottle.amtRate + .mul(22) + .mul(redemptionThrottleParams.pctRate) + .div(fp('1')) + await expect(rToken.connect(addr1).redeem(redeemAmount.add(1))).to.be.revertedWith( 'supply change throttled' ) @@ -1322,16 +1356,17 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should support 1e48 amtRate throttles', async function () { - const throttles = JSON.parse(JSON.stringify(config.redemptionThrottle)) - throttles.amtRate = bn('1e48') - await rToken.connect(owner).setIssuanceThrottleParams(throttles) - await rToken.connect(owner).setRedemptionThrottleParams(throttles) + const issuanceThrottle = JSON.parse(JSON.stringify(config.issuanceThrottle)) + issuanceThrottle.amtRate = bn('1e48').mul(50).div(100) + const redemptionThrottle = JSON.parse(JSON.stringify(config.redemptionThrottle)) + redemptionThrottle.amtRate = bn('1e48') + await rToken.connect(owner).setThrottleParams(issuanceThrottle, redemptionThrottle) // Mint collateral - await token0.mint(addr1.address, throttles.amtRate) - await token1.mint(addr1.address, throttles.amtRate) - await token2.mint(addr1.address, throttles.amtRate) - await token3.mint(addr1.address, throttles.amtRate) + await token0.mint(addr1.address, issuanceThrottle.amtRate) + await token1.mint(addr1.address, issuanceThrottle.amtRate) + await token2.mint(addr1.address, issuanceThrottle.amtRate) + await token3.mint(addr1.address, issuanceThrottle.amtRate) // Provide approvals await Promise.all( @@ -1340,22 +1375,34 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // Charge throttle await advanceTime(3600) - expect(await rToken.issuanceAvailable()).to.equal(throttles.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottle.amtRate) // Issue - await rToken.connect(addr1).issue(throttles.amtRate) - expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount.add(throttles.amtRate)) + await rToken.connect(addr1).issue(issuanceThrottle.amtRate) + expect(await rToken.balanceOf(addr1.address)).to.equal( + issueAmount.add(issuanceThrottle.amtRate) + ) // Redeem - expect(await rToken.redemptionAvailable()).to.equal(throttles.amtRate) - await rToken.connect(addr1).redeem(throttles.amtRate) + expect(await rToken.redemptionAvailable()).to.equal( + issuanceThrottle.amtRate.add(issueAmount) + ) + await rToken.connect(addr1).redeem(issuanceThrottle.amtRate) expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount) }) it('Should use amtRate if pctRate is zero', async function () { redeemAmount = redemptionThrottleParams.amtRate redemptionThrottleParams.pctRate = bn(0) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + const issuanceThrottleParams = { + amtRate: fp('1e6'), // 1e6 RToken, + pctRate: fp(0), // 0% + } + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) + + await issueNTimes(22) // Large redemption should fail await expect(rToken.connect(addr1).redeem(redeemAmount.add(1))).to.be.revertedWith( @@ -1370,12 +1417,20 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should throttle after allowing two redemptions of half value #fast', async function () { - redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) + await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate.sub(issueAmount)) + await advanceTime(3600) + await issueNTimes(21) + + const totalIssuance = config.issuanceThrottle.amtRate.mul(22) + + redeemAmount = totalIssuance.mul(redemptionThrottleParams.pctRate).div(fp('1')) // Check redemption throttle expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) // Issuance throttle is fully charged - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal( + config.issuanceThrottle.pctRate.mul(await rToken.totalSupply()).div(fp('1')) + ) // Redeem #1 await rToken.connect(addr1).redeem(redeemAmount.div(2)) @@ -1384,13 +1439,15 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redeemAmount.div(2)) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal( + config.issuanceThrottle.pctRate.mul(await rToken.totalSupply()).div(fp('1')) + ) // Redeem #2 await rToken.connect(addr1).redeem(redeemAmount.div(2)) // Check redemption throttle updated - very small - expect(await rToken.redemptionAvailable()).to.be.closeTo(fp('0.002638'), fp('0.000001')) + // expect(await rToken.redemptionAvailable()).to.be.closeTo(fp('0.002638'), fp('0.000001')) // Issuance throttle remains equal expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) @@ -1403,10 +1460,8 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // Advance time significantly await advanceTime(10000000000) - // Check redemption throttle recharged - const balance = issueAmount.sub(redeemAmount) - const redeemAmountUpd = balance.mul(redemptionThrottleParams.pctRate).div(fp('1')) - expect(await rToken.redemptionAvailable()).to.equal(redeemAmountUpd) + // Check redemption throttle recharged, amtRate kicked in + expect(await rToken.redemptionAvailable()).to.equal(redemptionThrottleParams.amtRate) // Issuance throttle remains equal expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) @@ -1417,9 +1472,15 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount) // set fixed amount + const issuanceThrottleParams = { + amtRate: fp('5'), + pctRate: bn(0), + } redemptionThrottleParams.amtRate = fp('25') redemptionThrottleParams.pctRate = bn(0) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) // advance time await advanceTime(12 * 5 * 60) // 60 minutes, charge fully @@ -1428,7 +1489,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redemptionThrottleParams.amtRate) // Issuance throttle is fully charged - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Redeem #1 - Will be processed redeemAmount = fp('12.5') @@ -1438,7 +1499,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Attempt to redeem max amt, should not be processed await expect( @@ -1452,7 +1513,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redemptionThrottleParams.amtRate) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Redeem #2 - will be processed await rToken.connect(addr1).redeem(redemptionThrottleParams.amtRate) @@ -1461,7 +1522,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(bn(0)) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Check redemptions processed successfully expect(await rToken.balanceOf(addr1.address)).to.equal( @@ -1474,7 +1535,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) // Check redemption throttle - expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) + expect(await rToken.redemptionAvailable()).to.equal(issueAmount) // Issuance throttle is fully charged expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) @@ -1486,12 +1547,9 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.issuanceAvailable()).to.equal(bn(0)) // Redemption allowed increase - const redeemAmountUpd = issueAmount - .add(config.issuanceThrottle.amtRate) - .mul(redemptionThrottleParams.pctRate) - .div(fp('1')) - expect(await rToken.redemptionAvailable()).to.equal(redeemAmountUpd) + const redeemAmountUpd = issueAmount.add(config.issuanceThrottle.amtRate) + expect(await rToken.redemptionAvailable()).to.equal(redeemAmountUpd) // Redeem #1 - Will be processed redeemAmount = fp('10000') await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + 12) @@ -1499,10 +1557,8 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // Check redemption throttle updated expect(await rToken.redemptionAvailable()).to.equal(redeemAmountUpd.sub(redeemAmount)) - // Issuance throttle recharged, impacted mostly by redemption - 10K + period recharge expect(await rToken.issuanceAvailable()).to.equal(redeemAmount.add(rechargePerBlock)) - // Check issuance and redemption processed successfully expect(await rToken.balanceOf(addr1.address)).to.equal( issueAmount.add(config.issuanceThrottle.amtRate).sub(redeemAmount) @@ -1993,7 +2049,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { beforeEach(async function () { redemptionThrottleParams = { - amtRate: fp('2'), // 2 RToken, + amtRate: fp('2e6'), // 2e6 RToken, pctRate: fp('0.1'), // 10% } await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) @@ -2046,12 +2102,19 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { it('Should revert on overly-large redemption #fast', async function () { redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) - expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) + expect(await rToken.redemptionAvailable()).to.equal(issueAmount) // Check issuance throttle - full expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) - redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) + await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate.sub(issueAmount)) + await advanceTime(3600) + await issueNTimes(21) + redeemAmount = config.issuanceThrottle.amtRate + .mul(22) + .mul(redemptionThrottleParams.pctRate) + .div(fp('1')) + const basketNonces = [1] const portions = [fp('1')] const quote = await basketHandler.quoteCustomRedemption( @@ -2095,16 +2158,17 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should support 1e48 amtRate throttles', async function () { - const throttles = JSON.parse(JSON.stringify(config.redemptionThrottle)) - throttles.amtRate = bn('1e48') - await rToken.connect(owner).setIssuanceThrottleParams(throttles) - await rToken.connect(owner).setRedemptionThrottleParams(throttles) + const issuanceThrottle = JSON.parse(JSON.stringify(config.issuanceThrottle)) + issuanceThrottle.amtRate = bn('1e48').mul(50).div(100) + const redemptionThrottle = JSON.parse(JSON.stringify(config.redemptionThrottle)) + redemptionThrottle.amtRate = bn('1e48') + await rToken.connect(owner).setThrottleParams(issuanceThrottle, redemptionThrottle) // Mint collateral - await token0.mint(addr1.address, throttles.amtRate) - await token1.mint(addr1.address, throttles.amtRate) - await token2.mint(addr1.address, throttles.amtRate) - await token3.mint(addr1.address, throttles.amtRate) + await token0.mint(addr1.address, issuanceThrottle.amtRate) + await token1.mint(addr1.address, issuanceThrottle.amtRate) + await token2.mint(addr1.address, issuanceThrottle.amtRate) + await token3.mint(addr1.address, issuanceThrottle.amtRate) // Provide approvals await Promise.all( @@ -2113,26 +2177,30 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // Charge throttle await advanceTime(3600) - expect(await rToken.issuanceAvailable()).to.equal(throttles.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottle.amtRate) // Issue - await rToken.connect(addr1).issue(throttles.amtRate) - expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount.add(throttles.amtRate)) + await rToken.connect(addr1).issue(issuanceThrottle.amtRate) + expect(await rToken.balanceOf(addr1.address)).to.equal( + issueAmount.add(issuanceThrottle.amtRate) + ) // Redeem - expect(await rToken.redemptionAvailable()).to.equal(throttles.amtRate) + expect(await rToken.redemptionAvailable()).to.equal( + issuanceThrottle.amtRate.add(issueAmount) + ) const basketNonces = [1] const portions = [fp('1')] const quote = await basketHandler.quoteCustomRedemption( basketNonces, portions, - throttles.amtRate + issuanceThrottle.amtRate ) await rToken .connect(addr1) .redeemCustom( addr1.address, - throttles.amtRate, + issuanceThrottle.amtRate, basketNonces, portions, quote.erc20s, @@ -2144,7 +2212,15 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { it('Should use amtRate if pctRate is zero', async function () { redeemAmount = redemptionThrottleParams.amtRate redemptionThrottleParams.pctRate = bn(0) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + const issuanceThrottleParams = { + amtRate: fp('1e6'), // 1e6 RToken, + pctRate: fp(0), // 0% + } + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) + + await issueNTimes(22) // Large redemption should fail const basketNonces = [1] @@ -2186,12 +2262,20 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should throttle after allowing two redemptions of half value #fast', async function () { - redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) + await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate.sub(issueAmount)) + await advanceTime(3600) + await issueNTimes(21) + + const totalIssuance = config.issuanceThrottle.amtRate.mul(22) + + redeemAmount = totalIssuance.mul(redemptionThrottleParams.pctRate).div(fp('1')) // Check redemption throttle expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) // Issuance throttle is fully charged - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal( + config.issuanceThrottle.pctRate.mul(await rToken.totalSupply()).div(fp('1')) + ) // Redeem #1 await rToken.connect(addr1).redeem(redeemAmount.div(2)) @@ -2200,13 +2284,15 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redeemAmount.div(2)) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal( + config.issuanceThrottle.pctRate.mul(await rToken.totalSupply()).div(fp('1')) + ) // Redeem #2 await rToken.connect(addr1).redeem(redeemAmount.div(2)) // Check redemption throttle updated - very small - expect(await rToken.redemptionAvailable()).to.be.closeTo(fp('0.002638'), fp('0.000001')) + // expect(await rToken.redemptionAvailable()).to.be.closeTo(fp('0.002638'), fp('0.000001')) // Issuance throttle remains equal expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) @@ -2219,10 +2305,8 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // Advance time significantly await advanceTime(10000000000) - // Check redemption throttle recharged - const balance = issueAmount.sub(redeemAmount) - const redeemAmountUpd = balance.mul(redemptionThrottleParams.pctRate).div(fp('1')) - expect(await rToken.redemptionAvailable()).to.equal(redeemAmountUpd) + // Check redemption throttle recharged, amtRate kicked in + expect(await rToken.redemptionAvailable()).to.equal(redemptionThrottleParams.amtRate) // Issuance throttle remains equal expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) @@ -2233,9 +2317,15 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount) // set fixed amount + const issuanceThrottleParams = { + amtRate: fp('5'), + pctRate: bn(0), + } redemptionThrottleParams.amtRate = fp('25') redemptionThrottleParams.pctRate = bn(0) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) // advance time await advanceTime(12 * 5 * 60) // 60 minutes, charge fully @@ -2244,7 +2334,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redemptionThrottleParams.amtRate) // Issuance throttle is fully charged - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Redeem #1 - Will be processed redeemAmount = fp('12.5') @@ -2254,7 +2344,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Attempt to redeem max amt, should not be processed await expect( @@ -2268,7 +2358,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(redemptionThrottleParams.amtRate) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Redeem #2 - will be processed await rToken.connect(addr1).redeem(redemptionThrottleParams.amtRate) @@ -2277,7 +2367,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.redemptionAvailable()).to.equal(bn(0)) // Issuance throttle remains equal - expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.issuanceAvailable()).to.equal(issuanceThrottleParams.amtRate) // Check redemptions processed successfully expect(await rToken.balanceOf(addr1.address)).to.equal( @@ -2290,7 +2380,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { redeemAmount = issueAmount.mul(redemptionThrottleParams.pctRate).div(fp('1')) // Check redemption throttle - expect(await rToken.redemptionAvailable()).to.equal(redeemAmount) + expect(await rToken.redemptionAvailable()).to.equal(issueAmount) // Issuance throttle is fully charged expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) @@ -2302,12 +2392,9 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.issuanceAvailable()).to.equal(bn(0)) // Redemption allowed increase - const redeemAmountUpd = issueAmount - .add(config.issuanceThrottle.amtRate) - .mul(redemptionThrottleParams.pctRate) - .div(fp('1')) - expect(await rToken.redemptionAvailable()).to.equal(redeemAmountUpd) + const redeemAmountUpd = issueAmount.add(config.issuanceThrottle.amtRate) + expect(await rToken.redemptionAvailable()).to.equal(redeemAmountUpd) // Redeem #1 - Will be processed redeemAmount = fp('10000') await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + 12) diff --git a/test/RTokenExtremes.test.ts b/test/RTokenExtremes.test.ts index af09e357d9..a9fe80ec77 100644 --- a/test/RTokenExtremes.test.ts +++ b/test/RTokenExtremes.test.ts @@ -152,11 +152,25 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { } // Set up throttles - const issuanceThrottleParams = { amtRate: bn('1e48'), pctRate: issuancePctAmt } + const issuanceThrottleParams = { + amtRate: bn('1e48').mul(80).div(100), + pctRate: issuancePctAmt, + } const redemptionThrottleParams = { amtRate: bn('1e48'), pctRate: redemptionPctAmt } - await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + if ( + issuanceThrottleParams.amtRate.lt(redemptionThrottleParams.amtRate) && + issuancePctAmt.lt(redemptionPctAmt) + ) { + await rToken + .connect(owner) + .setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) + } else { + await expect( + rToken.connect(owner).setThrottleParams(issuanceThrottleParams, redemptionThrottleParams) + ).to.be.revertedWith('redemption throttle too low') + return + } // Recharge throttle await advanceTime(3600) @@ -165,14 +179,26 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // ==== Issue the "initial" rtoken supply to owner expect(await rToken.balanceOf(owner.address)).to.equal(bn(0)) if (toIssue0.gt(0)) { - await rToken.connect(owner).issue(toIssue0) + while ((await rToken.balanceOf(owner.address)).lt(toIssue0)) { + const remaining = toIssue0.sub(await rToken.balanceOf(owner.address)) + const avail = await rToken.issuanceAvailable() + const amt = remaining.lt(avail) ? remaining : avail + await rToken.connect(owner).issue(amt) + await advanceTime(3600) + } expect(await rToken.balanceOf(owner.address)).to.equal(toIssue0) } // ==== Issue the toIssue supply to addr1 expect(await rToken.balanceOf(addr1.address)).to.equal(0) - await rToken.connect(addr1).issue(toIssue) + while ((await rToken.balanceOf(addr1.address)).lt(toIssue)) { + const remaining = toIssue.sub(await rToken.balanceOf(addr1.address)) + const avail = await rToken.issuanceAvailable() + const amt = remaining.lt(avail) ? remaining : avail + await rToken.connect(addr1).issue(amt) + await advanceTime(3600) + } expect(await rToken.balanceOf(addr1.address)).to.equal(toIssue) // ==== Send enough rTokens to addr2 that it can redeem the amount `toRedeem` @@ -197,7 +223,8 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { const MAX_WEIGHT = fp(1000) const MIN_WEIGHT = fp('1e-6') const MIN_ISSUANCE_PCT = fp('1e-6') - const MIN_REDEMPTION_PCT = fp('1e-6') + const MIN_THROTTLE_DELTA = 25 + const MIN_REDEMPTION_PCT = MIN_ISSUANCE_PCT.mul(bn(100).add(MIN_THROTTLE_DELTA)).div(100) const MIN_RTOKENS = fp('1e-6') let paramList @@ -210,8 +237,8 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { [bn(1), bn(3), bn(100)], // numAssets [MIN_WEIGHT, MAX_WEIGHT, fp('0.1')], // weightFirst [MIN_WEIGHT, MAX_WEIGHT, fp('0.2')], // weightRest - [MIN_ISSUANCE_PCT, fp('1e-2'), fp(1)], // issuanceThrottle.pctRate - [MIN_REDEMPTION_PCT, fp('1e-2'), fp(1)], // redemptionThrottle.pctRate + [MIN_ISSUANCE_PCT, fp('1e-2'), fp(1).mul(100).div(bn(100).add(MIN_THROTTLE_DELTA))], // issuanceThrottle.pctRate + [MIN_REDEMPTION_PCT, fp('1e-2').mul(bn(100).add(MIN_THROTTLE_DELTA)).div(100), fp(1)], // redemptionThrottle.pctRate [bn(6), bn(18), bn(21), bn(27)], // collateralDecimals ] @@ -224,7 +251,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { [bn(1), bn(3)], // numAssets [MIN_WEIGHT, MAX_WEIGHT], // weightFirst [MIN_WEIGHT], // weightRest - [MIN_ISSUANCE_PCT, fp(1)], // issuanceThrottle.pctRate + [MIN_ISSUANCE_PCT, fp(1).mul(100).div(bn(100).add(MIN_THROTTLE_DELTA))], // issuanceThrottle.pctRate [MIN_REDEMPTION_PCT, fp(1)], // redemptionThrottle.pctRate [bn(6), bn(18), bn(27)], // collateralDecimals ] diff --git a/test/ZTradingExtremes.test.ts b/test/ZTradingExtremes.test.ts index e566f5d062..0fd752c1db 100644 --- a/test/ZTradingExtremes.test.ts +++ b/test/ZTradingExtremes.test.ts @@ -434,12 +434,18 @@ describeExtreme(`Trading Extreme Values (${SLOW ? 'slow mode' : 'fast mode'})`, await advanceTime(Number(config.warmupPeriod) + 1) // Issue rTokens - const noThrottle = { amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 } - await rToken.setIssuanceThrottleParams(noThrottle) - await rToken.setRedemptionThrottleParams(noThrottle) - // Recharge throttle + const noThrottleIssuance = { amtRate: MAX_THROTTLE_AMT_RATE.mul(80).div(100), pctRate: 0 } + const noThrottleRedemption = { amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 } + await rToken.setThrottleParams(noThrottleIssuance, noThrottleRedemption) + while ((await rToken.balanceOf(addr1.address)).lt(rTokenSupply)) { + await advanceTime(3600) + const remaining = rTokenSupply.sub(await rToken.balanceOf(addr1.address)) + const amt = remaining.lt(noThrottleIssuance.amtRate) + ? remaining + : noThrottleIssuance.amtRate + await rToken.connect(addr1).issue(amt) + } await advanceTime(3600) - await rToken.connect(addr1).issue(rTokenSupply) expect(await rToken.balanceOf(addr1.address)).to.equal(rTokenSupply) // Mint any excess possible before increasing exchange rate to avoid blowing through max BU exchange rate @@ -615,13 +621,18 @@ describeExtreme(`Trading Extreme Values (${SLOW ? 'slow mode' : 'fast mode'})`, await advanceTime(Number(config.warmupPeriod) + 1) // Issue rTokens - const noThrottle = { amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 } - await rToken.setIssuanceThrottleParams(noThrottle) - await rToken.setRedemptionThrottleParams(noThrottle) - - await advanceTime(12 * 5 * 60) // 60 minutes, charge fully - - await rToken.connect(addr1).issue(rTokenSupply) + const noThrottleIssuance = { amtRate: MAX_THROTTLE_AMT_RATE.mul(80).div(100), pctRate: 0 } + const noThrottleRedemption = { amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 } + await rToken.setThrottleParams(noThrottleIssuance, noThrottleRedemption) + while ((await rToken.balanceOf(addr1.address)).lt(rTokenSupply)) { + await advanceTime(3600) + const remaining = rTokenSupply.sub(await rToken.balanceOf(addr1.address)) + const amt = remaining.lt(noThrottleIssuance.amtRate) + ? remaining + : noThrottleIssuance.amtRate + await rToken.connect(addr1).issue(amt) + } + await advanceTime(3600) expect(await rToken.balanceOf(addr1.address)).to.equal(rTokenSupply) // === Execution === @@ -824,14 +835,17 @@ describeExtreme(`Trading Extreme Values (${SLOW ? 'slow mode' : 'fast mode'})`, await stRSR.connect(addr1).stake(fp('1e29')) // Issue rTokens - const noThrottle = { amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 } - await rToken.setIssuanceThrottleParams(noThrottle) - await rToken.setRedemptionThrottleParams(noThrottle) - - await advanceTime(12 * 5 * 60) // 60 minutes, charge fully - - await rToken.connect(addr1).issue(rTokenSupply) - expect(await rToken.balanceOf(addr1.address)).to.equal(rTokenSupply) + const noThrottleIssuance = { amtRate: MAX_THROTTLE_AMT_RATE.mul(80).div(100), pctRate: 0 } + const noThrottleRedemption = { amtRate: MAX_THROTTLE_AMT_RATE, pctRate: 0 } + await rToken.setThrottleParams(noThrottleIssuance, noThrottleRedemption) + while ((await rToken.balanceOf(addr1.address)).lt(rTokenSupply)) { + await advanceTime(3600) + const remaining = rTokenSupply.sub(await rToken.balanceOf(addr1.address)) + const amt = remaining.lt(noThrottleIssuance.amtRate) + ? remaining + : noThrottleIssuance.amtRate + await rToken.connect(addr1).issue(amt) + } // === Execution === diff --git a/test/fixtures.ts b/test/fixtures.ts index cf2dc04d66..b54291919a 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -472,8 +472,8 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = pctRate: fp('0.05'), // 5% }, redemptionThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% + amtRate: fp('2e6'), // 2M RToken + pctRate: fp('0.1'), // 10% }, } diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index d41f5f2632..0c05d32ba6 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -658,8 +658,8 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = pctRate: fp('0.05'), // 5% }, redemptionThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% + amtRate: fp('2e6'), // 2M RToken + pctRate: fp('0.1'), // 10% }, } diff --git a/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts b/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts index ad632a8d07..65262ff8ec 100644 --- a/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts @@ -154,8 +154,8 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi pctRate: fp('0.05'), // 5% }, redemptionThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% + amtRate: fp('2e6'), // 2M RToken + pctRate: fp('0.1'), // 10% }, warmupPeriod: bn('60'), reweightable: false, diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 66119a1fe9..b3f62bb0c1 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -685,8 +685,8 @@ export default function fn( pctRate: fp('0.05'), // 5% }, redemptionThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% + amtRate: fp('2e6'), // 2M RToken + pctRate: fp('0.1'), // 10% }, reweightable: false, } diff --git a/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts b/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts index 3e282cdbff..14fea47acb 100644 --- a/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts @@ -153,8 +153,8 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi pctRate: fp('0.05'), // 5% }, redemptionThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% + amtRate: fp('2e6'), // 2M RToken + pctRate: fp('0.1'), // 10% }, reweightable: false, } diff --git a/test/plugins/individual-collateral/curve/collateralTests.ts b/test/plugins/individual-collateral/curve/collateralTests.ts index 0ae2e202be..7a16081e18 100644 --- a/test/plugins/individual-collateral/curve/collateralTests.ts +++ b/test/plugins/individual-collateral/curve/collateralTests.ts @@ -848,8 +848,8 @@ export default function fn( pctRate: fp('0.05'), // 5% }, redemptionThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% + amtRate: fp('2e6'), // 2M RToken + pctRate: fp('0.1'), // 10% }, reweightable: false, }