Skip to content

Commit

Permalink
Require redemption throttle limits above issuance throttle limits (#1209
Browse files Browse the repository at this point in the history
)

Co-authored-by: Taylor Brent <taylor.w.brent@gmail.com>
  • Loading branch information
pmckelvy1 and tbrent authored Nov 6, 2024
1 parent 39d5d68 commit a5235dd
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 174 deletions.
5 changes: 5 additions & 0 deletions contracts/interfaces/IRToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
59 changes: 55 additions & 4 deletions contracts/p0/RToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
60 changes: 55 additions & 5 deletions contracts/p1/RToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 RTokens 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:
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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
Expand Down
7 changes: 4 additions & 3 deletions test/Broker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 15 additions & 19 deletions test/Facade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -1437,26 +1441,22 @@ 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)
)

// 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(
Expand All @@ -1483,20 +1483,16 @@ 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
await rToken.connect(addr1).redeem(totalSupply.div(10).div(2))

// 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')
)

Expand Down
9 changes: 6 additions & 3 deletions test/Furnace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit a5235dd

Please sign in to comment.