Skip to content

Commit

Permalink
fix p0.
Browse files Browse the repository at this point in the history
  • Loading branch information
pmckelvy1 committed Sep 20, 2024
1 parent 0820cd8 commit 01374d7
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 47 deletions.
58 changes: 55 additions & 3 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}
uint256 public constant MIN_THROTTLE_DELTA = 25; // {%}

/// Weakly immutable: expected to be an IPFS link but could be the mandate itself
string public mandate;
Expand All @@ -54,8 +55,8 @@ contract RTokenP0 is ComponentP0, ERC20PermitUpgradeable, IRToken {
__ERC20Permit_init(name_);

mandate = mandate_;
setIssuanceThrottleParams(issuanceThrottleParams_);
setRedemptionThrottleParams(redemptionThrottleParams_);
setIssuanceThrottleParams(issuanceThrottleParams_);

issuanceThrottle.lastTimestamp = uint48(block.timestamp);
redemptionThrottle.lastTimestamp = uint48(block.timestamp);
Expand Down Expand Up @@ -330,25 +331,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
) external governance {
_setIssuanceThrottleParams(issuanceParams);
_setRedemptionThrottleParams(redemptionParams);
require(
isRedemptionThrottleGreaterByDelta(issuanceParams, redemptionParams),
"redemption throttle too low"
);
}

// === Private ===

function _setIssuanceThrottleParams(ThrottleLib.Params calldata params) public governance {
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) public governance {
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) / 100);
uint256 requiredPctRate = issuance.pctRate +
((issuance.pctRate * MIN_THROTTLE_DELTA) / 100);

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
119 changes: 75 additions & 44 deletions test/RToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -1280,13 +1287,6 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => {
await advanceTime(3600)
})

async function issueNTimes(n: number) {
for (let i = 0; i < n; i++) {
await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate)
await advanceTime(3600)
}
}

it('Should calculate redemption limit correctly', async function () {
await rToken.connect(addr1).issue(config.issuanceThrottle.amtRate.sub(issueAmount))
await advanceTime(3600)
Expand Down Expand Up @@ -2042,7 +2042,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)
Expand Down Expand Up @@ -2095,12 +2095,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(
Expand Down Expand Up @@ -2144,16 +2151,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(
Expand All @@ -2162,26 +2170,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,
Expand All @@ -2193,7 +2205,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]
Expand Down Expand Up @@ -2235,12 +2255,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))
Expand All @@ -2249,13 +2277,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)
Expand All @@ -2268,10 +2298,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)
Expand All @@ -2282,9 +2310,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
Expand All @@ -2293,7 +2327,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')
Expand All @@ -2303,7 +2337,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(
Expand All @@ -2317,7 +2351,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)
Expand All @@ -2326,7 +2360,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(
Expand All @@ -2339,7 +2373,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)
Expand All @@ -2351,12 +2385,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)
Expand Down

0 comments on commit 01374d7

Please sign in to comment.