From 4fb7a0ebafa2336663f096c9de19d183eb3cf1c8 Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:45:43 -0400 Subject: [PATCH 1/3] rebase circuit breaker --- contracts/UFragmentsPolicy.sol | 64 +- contracts/mocks/MockUFragments.sol | 3 +- test/unit/SafeMathInt.ts | 1 + test/unit/UFragmentsPolicy.ts | 2226 +++++++++++++++------------- 4 files changed, 1259 insertions(+), 1035 deletions(-) diff --git a/contracts/UFragmentsPolicy.sol b/contracts/UFragmentsPolicy.sol index 2ac2c822..d75b0224 100644 --- a/contracts/UFragmentsPolicy.sol +++ b/contracts/UFragmentsPolicy.sol @@ -98,6 +98,15 @@ contract UFragmentsPolicy is Ownable { int256 public rebaseFunctionUpperPercentage; int256 public rebaseFunctionGrowth; + // NOTE: This was added with v1.5 release, on-chain state will not + // have the history going back to epoch(0). + // Mapping between epoch and the supply at that epoch. + mapping(uint256 => uint256) public supplyHistory; + + // Circuit breaker parameters which limit supply decline within the defined look back period. + uint8 public epochLookback; + int256 public tolerableDeclinePercentage; + int256 private constant ONE = int256(10**DECIMALS); modifier onlyOrchestrator() { @@ -139,6 +148,8 @@ contract UFragmentsPolicy is Ownable { uint256 supplyAfterRebase = uFrags.rebase(epoch, supplyDelta); assert(supplyAfterRebase <= MAX_SUPPLY); + + supplyHistory[epoch] = supplyAfterRebase; emit LogRebaseV2(epoch, exchangeRate, targetRate, supplyDelta); } @@ -206,7 +217,7 @@ contract UFragmentsPolicy is Ownable { * @param minRebaseTimeIntervalSec_ More than this much time must pass between rebase * operations, in seconds. * @param rebaseWindowOffsetSec_ The number of seconds from the beginning of - the rebase interval, where the rebase window begins. + * the rebase interval, where the rebase window begins. * @param rebaseWindowLengthSec_ The length of the rebase window in seconds. */ function setRebaseTimingParameters( @@ -222,6 +233,22 @@ contract UFragmentsPolicy is Ownable { rebaseWindowLengthSec = rebaseWindowLengthSec_; } + /** + * @notice Sets the parameters which control rebase circuit breaker. + * @param epochLookback_ The number of rebase epochs to look back. + * @param tolerableDeclinePercentage_ The maximum supply decline percentage which is allowed + * within the defined look back period. + */ + function setRebaseCircuitBreakerParameters( + uint8 epochLookback_, + int256 tolerableDeclinePercentage_ + ) external onlyOwner { + require(tolerableDeclinePercentage_ > 0 && tolerableDeclinePercentage_ <= ONE); + + epochLookback = epochLookback_; + tolerableDeclinePercentage = tolerableDeclinePercentage_; + } + /** * @notice A multi-chain AMPL interface method. The Ampleforth monetary policy contract * on the base-chain and XC-AmpleController contracts on the satellite-chains @@ -257,6 +284,9 @@ contract UFragmentsPolicy is Ownable { lastRebaseTimestampSec = 0; epoch = 0; + epochLookback = 0; + tolerableDeclinePercentage = ONE; + uFrags = uFrags_; } @@ -328,7 +358,8 @@ contract UFragmentsPolicy is Ownable { * @return Computes the total supply adjustment in response to the exchange rate * and the targetRate. */ - function computeSupplyDelta(uint256 rate, uint256 targetRate) internal view returns (int256) { + function computeSupplyDelta(uint256 rate, uint256 targetRate) public view returns (int256) { + // No supply change if rate is within deviation threshold if (withinDeviationThreshold(rate, targetRate)) { return 0; } @@ -340,7 +371,28 @@ contract UFragmentsPolicy is Ownable { rebaseFunctionUpperPercentage, rebaseFunctionGrowth ); - return uFrags.totalSupply().toInt256Safe().mul(rebasePercentage).div(ONE); + + int256 currentSupply = uFrags.totalSupply().toInt256Safe(); // (or) supplyHistory[epoch] + int256 newSupply = ONE.add(rebasePercentage).mul(currentSupply).div(ONE); + + // When supply is decreasing: + // We limit the supply delta, based on recent supply history. + if (rebasePercentage < 0) { + int256 maxSupplyInHistory = currentSupply; + for (uint8 i = 1; i < epochLookback && epoch > i; i++) { + int256 epochSupply = supplyHistory[epoch - i].toInt256Safe(); + if (epochSupply > maxSupplyInHistory) { + maxSupplyInHistory = epochSupply; + } + } + int256 allowedSupplyMinimum = maxSupplyInHistory + .mul(ONE.sub(tolerableDeclinePercentage)) + .div(ONE); + newSupply = (newSupply > allowedSupplyMinimum) ? newSupply : allowedSupplyMinimum; + require(newSupply <= currentSupply); + } + + return newSupply.sub(currentSupply); } /** @@ -349,11 +401,7 @@ contract UFragmentsPolicy is Ownable { * @return If the rate is within the deviation threshold from the target rate, returns true. * Otherwise, returns false. */ - function withinDeviationThreshold(uint256 rate, uint256 targetRate) - internal - view - returns (bool) - { + function withinDeviationThreshold(uint256 rate, uint256 targetRate) public view returns (bool) { uint256 absoluteDeviationThreshold = targetRate.mul(deviationThreshold).div(10**DECIMALS); return diff --git a/contracts/mocks/MockUFragments.sol b/contracts/mocks/MockUFragments.sol index 13e42025..50ee681a 100644 --- a/contracts/mocks/MockUFragments.sol +++ b/contracts/mocks/MockUFragments.sol @@ -18,7 +18,8 @@ contract MockUFragments is Mock { int256[] memory intVals = new int256[](1); intVals[0] = supplyDelta; emit FunctionArguments(uintVals, intVals); - return uint256(int256(_supply) + int256(supplyDelta)); + _supply = uint256(int256(_supply) + int256(supplyDelta)); + return _supply; } function totalSupply() public view returns (uint256) { diff --git a/test/unit/SafeMathInt.ts b/test/unit/SafeMathInt.ts index f61bcc04..9123695b 100644 --- a/test/unit/SafeMathInt.ts +++ b/test/unit/SafeMathInt.ts @@ -167,6 +167,7 @@ describe('SafeMathInt', () => { await expect(safeMathInt.abs(MIN_INT256)).to.be.reverted }) }) + describe('twoPower', function () { const decimals18 = ethers.BigNumber.from('1000000000000000000') const decimals10 = ethers.BigNumber.from('10000000000') diff --git a/test/unit/UFragmentsPolicy.ts b/test/unit/UFragmentsPolicy.ts index 130f3a61..c4a6bc07 100644 --- a/test/unit/UFragmentsPolicy.ts +++ b/test/unit/UFragmentsPolicy.ts @@ -94,19 +94,22 @@ async function mockedUpgradablePolicyWithOpenRebaseWindow() { async function mockExternalData( currentRate: BigNumberish, targetRate: BigNumberish, - uFragSupply: BigNumberish, + uFragSupply: BigNumberish = undefined, currentRateValidity = true, targetRateValidity = true, ) { - await mockMarketOracle.connect(deployer).storeData(currentRate) - await mockMarketOracle.connect(deployer).storeValidity(currentRateValidity) - await mockCpiOracle.connect(deployer).storeData(targetRate) - await mockCpiOracle.connect(deployer).storeValidity(targetRateValidity) - await mockUFragments.connect(deployer).storeSupply(uFragSupply) + await mockMarketOracle.storeData(currentRate) + await mockMarketOracle.storeValidity(currentRateValidity) + await mockCpiOracle.storeData(targetRate) + await mockCpiOracle.storeValidity(targetRateValidity) + if (uFragSupply) { + await mockUFragments.storeSupply(uFragSupply) + } } async function parseRebaseEvent(response: Promise) { - const receipt = (await (await response).wait()) as any + const r = await response + const receipt = (await r.wait()) as any const logs = receipt.events.filter( (event: Event) => event.event === 'LogRebaseV2', ) @@ -114,27 +117,7 @@ async function parseRebaseEvent(response: Promise) { } describe('UFragmentsPolicy', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) - - it('should reject any ether sent to it', async function () { - await expect( - user.sendTransaction({ to: uFragmentsPolicy.address, value: 1 }), - ).to.be.reverted - }) -}) - -describe('UFragmentsPolicy:initialize', async function () { - describe('initial values set correctly', function () { + describe('UFragmentsPolicy', function () { before('setup UFragmentsPolicy contract', async () => { ;({ deployer, @@ -147,1185 +130,1376 @@ describe('UFragmentsPolicy:initialize', async function () { } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - it('deviationThreshold', async function () { - expect(await uFragmentsPolicy.deviationThreshold()).to.eq( - ethers.utils.parseUnits('5', 16), - ) - }) - it('rebaseLag', async function () { - expect(await uFragmentsPolicy.rebaseLag()).to.eq(1) - }) - it('minRebaseTimeIntervalSec', async function () { - expect(await uFragmentsPolicy.minRebaseTimeIntervalSec()).to.eq( - 24 * 60 * 60, - ) - }) - it('epoch', async function () { - expect(await uFragmentsPolicy.epoch()).to.eq(0) - }) - it('globalAmpleforthEpochAndAMPLSupply', async function () { - const r = await uFragmentsPolicy.globalAmpleforthEpochAndAMPLSupply() - expect(r[0]).to.eq(0) - expect(r[1]).to.eq(0) - }) - it('rebaseWindowOffsetSec', async function () { - expect(await uFragmentsPolicy.rebaseWindowOffsetSec()).to.eq(7200) - }) - it('rebaseWindowLengthSec', async function () { - expect(await uFragmentsPolicy.rebaseWindowLengthSec()).to.eq(1200) - }) - it('should set owner', async function () { - expect(await uFragmentsPolicy.owner()).to.eq(await deployer.getAddress()) - }) - it('should set reference to uFragments', async function () { - expect(await uFragmentsPolicy.uFrags()).to.eq(mockUFragments.address) + it('should reject any ether sent to it', async function () { + await expect( + user.sendTransaction({ to: uFragmentsPolicy.address, value: 1 }), + ).to.be.reverted }) }) -}) - -describe('UFragmentsPolicy:setMarketOracle', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) - - it('should set marketOracle', async function () { - await uFragmentsPolicy - .connect(deployer) - .setMarketOracle(await deployer.getAddress()) - expect(await uFragmentsPolicy.marketOracle()).to.eq( - await deployer.getAddress(), - ) - }) -}) - -describe('UFragments:setMarketOracle:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) - - it('should be callable by owner', async function () { - await expect( - uFragmentsPolicy - .connect(deployer) - .setMarketOracle(await deployer.getAddress()), - ).to.not.be.reverted - }) - - it('should NOT be callable by non-owner', async function () { - await expect( - uFragmentsPolicy - .connect(user) - .setMarketOracle(await deployer.getAddress()), - ).to.be.reverted - }) -}) - -describe('UFragmentsPolicy:setCpiOracle', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) - it('should set cpiOracle', async function () { - await uFragmentsPolicy - .connect(deployer) - .setCpiOracle(await deployer.getAddress()) - expect(await uFragmentsPolicy.cpiOracle()).to.eq( - await deployer.getAddress(), - ) + describe('UFragmentsPolicy:initialize', async function () { + describe('initial values set correctly', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) + + it('deviationThreshold', async function () { + expect(await uFragmentsPolicy.deviationThreshold()).to.eq( + ethers.utils.parseUnits('5', 16), + ) + }) + it('rebaseLag', async function () { + expect(await uFragmentsPolicy.rebaseLag()).to.eq(1) + }) + it('minRebaseTimeIntervalSec', async function () { + expect(await uFragmentsPolicy.minRebaseTimeIntervalSec()).to.eq( + 24 * 60 * 60, + ) + }) + it('epoch', async function () { + expect(await uFragmentsPolicy.epoch()).to.eq(0) + }) + it('globalAmpleforthEpochAndAMPLSupply', async function () { + const r = await uFragmentsPolicy.globalAmpleforthEpochAndAMPLSupply() + expect(r[0]).to.eq(0) + expect(r[1]).to.eq(0) + }) + it('rebaseWindowOffsetSec', async function () { + expect(await uFragmentsPolicy.rebaseWindowOffsetSec()).to.eq(7200) + }) + it('rebaseWindowLengthSec', async function () { + expect(await uFragmentsPolicy.rebaseWindowLengthSec()).to.eq(1200) + }) + it('should set owner', async function () { + expect(await uFragmentsPolicy.owner()).to.eq( + await deployer.getAddress(), + ) + }) + it('should set reference to uFragments', async function () { + expect(await uFragmentsPolicy.uFrags()).to.eq(mockUFragments.address) + }) + }) }) -}) -describe('UFragments:setCpiOracle:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) + describe('UFragmentsPolicy:setMarketOracle', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) - it('should be callable by owner', async function () { - await expect( - uFragmentsPolicy + it('should set marketOracle', async function () { + await uFragmentsPolicy .connect(deployer) - .setCpiOracle(await deployer.getAddress()), - ).to.not.be.reverted + .setMarketOracle(await deployer.getAddress()) + expect(await uFragmentsPolicy.marketOracle()).to.eq( + await deployer.getAddress(), + ) + }) }) - it('should NOT be callable by non-owner', async function () { - await expect( - uFragmentsPolicy.connect(user).setCpiOracle(await deployer.getAddress()), - ).to.be.reverted - }) -}) + describe('UFragments:setMarketOracle:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) -describe('UFragmentsPolicy:setOrchestrator', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) + it('should be callable by owner', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setMarketOracle(await deployer.getAddress()), + ).to.not.be.reverted + }) - it('should set orchestrator', async function () { - await uFragmentsPolicy - .connect(deployer) - .setOrchestrator(await user.getAddress()) - expect(await uFragmentsPolicy.orchestrator()).to.eq(await user.getAddress()) + it('should NOT be callable by non-owner', async function () { + await expect( + uFragmentsPolicy + .connect(user) + .setMarketOracle(await deployer.getAddress()), + ).to.be.reverted + }) }) -}) -describe('UFragments:setOrchestrator:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) + describe('UFragmentsPolicy:setCpiOracle', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) - it('should be callable by owner', async function () { - await expect( - uFragmentsPolicy + it('should set cpiOracle', async function () { + await uFragmentsPolicy .connect(deployer) - .setOrchestrator(await deployer.getAddress()), - ).to.not.be.reverted - }) - - it('should NOT be callable by non-owner', async function () { - await expect( - uFragmentsPolicy - .connect(user) - .setOrchestrator(await deployer.getAddress()), - ).to.be.reverted - }) -}) - -describe('UFragmentsPolicy:setDeviationThreshold', async function () { - let prevThreshold: BigNumber, threshold: BigNumber - before('setup UFragmentsPolicy contract', async function () { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - prevThreshold = await uFragmentsPolicy.deviationThreshold() - threshold = prevThreshold.add(ethers.utils.parseUnits('1', 16)) - await uFragmentsPolicy.connect(deployer).setDeviationThreshold(threshold) - }) - - it('should set deviationThreshold', async function () { - expect(await uFragmentsPolicy.deviationThreshold()).to.eq(threshold) - }) -}) - -describe('UFragments:setDeviationThreshold:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) - - it('should be callable by owner', async function () { - await expect(uFragmentsPolicy.connect(deployer).setDeviationThreshold(0)).to - .not.be.reverted - }) - - it('should NOT be callable by non-owner', async function () { - await expect(uFragmentsPolicy.connect(user).setDeviationThreshold(0)).to.be - .reverted - }) -}) - -describe('UFragmentsPolicy:CurveParameters', async function () { - before('setup UFragmentsPolicy contract', async function () { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) + .setCpiOracle(await deployer.getAddress()) + expect(await uFragmentsPolicy.cpiOracle()).to.eq( + await deployer.getAddress(), + ) + }) }) - describe('when rebaseFunctionGrowth is more than 0', async function () { - it('should setRebaseFunctionGrowth', async function () { - await uFragmentsPolicy.connect(deployer).setRebaseFunctionGrowth(1000) - expect(await uFragmentsPolicy.rebaseFunctionGrowth()).to.eq(1000) + describe('UFragments:setCpiOracle:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - }) - describe('when rebaseFunctionGrowth is less than 0', async function () { - it('should fail', async function () { + it('should be callable by owner', async function () { await expect( - uFragmentsPolicy.connect(deployer).setRebaseFunctionGrowth(-1), - ).to.be.reverted + uFragmentsPolicy + .connect(deployer) + .setCpiOracle(await deployer.getAddress()), + ).to.not.be.reverted }) - }) - describe('when rebaseFunctionLowerPercentage is more than 0', async function () { - it('should fail', async function () { + it('should NOT be callable by non-owner', async function () { await expect( uFragmentsPolicy - .connect(deployer) - .setRebaseFunctionLowerPercentage(1000), + .connect(user) + .setCpiOracle(await deployer.getAddress()), ).to.be.reverted }) }) - describe('when rebaseFunctionLowerPercentage is less than 0', async function () { - it('should setRebaseFunctionLowerPercentage', async function () { + describe('UFragmentsPolicy:setOrchestrator', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) + + it('should set orchestrator', async function () { await uFragmentsPolicy .connect(deployer) - .setRebaseFunctionLowerPercentage(-1) - expect(await uFragmentsPolicy.rebaseFunctionLowerPercentage()).to.eq(-1) + .setOrchestrator(await user.getAddress()) + expect(await uFragmentsPolicy.orchestrator()).to.eq( + await user.getAddress(), + ) }) }) - describe('when rebaseFunctionUpperPercentage is less than 0', async function () { - it('should fail', async function () { + describe('UFragments:setOrchestrator:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) + + it('should be callable by owner', async function () { await expect( - uFragmentsPolicy.connect(deployer).setRebaseFunctionUpperPercentage(-1), - ).to.be.reverted + uFragmentsPolicy + .connect(deployer) + .setOrchestrator(await deployer.getAddress()), + ).to.not.be.reverted }) - }) - describe('when rebaseFunctionUpperPercentage is more than 0', async function () { - it('should setRebaseFunctionUpperPercentage', async function () { - await uFragmentsPolicy - .connect(deployer) - .setRebaseFunctionUpperPercentage(1000) - expect(await uFragmentsPolicy.rebaseFunctionUpperPercentage()).to.eq(1000) + it('should NOT be callable by non-owner', async function () { + await expect( + uFragmentsPolicy + .connect(user) + .setOrchestrator(await deployer.getAddress()), + ).to.be.reverted }) }) -}) -describe('UFragments:setRebaseFunctionGrowth:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) + describe('UFragmentsPolicy:setDeviationThreshold', async function () { + let prevThreshold: BigNumber, threshold: BigNumber + before('setup UFragmentsPolicy contract', async function () { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + prevThreshold = await uFragmentsPolicy.deviationThreshold() + threshold = prevThreshold.add(ethers.utils.parseUnits('1', 16)) + await uFragmentsPolicy.connect(deployer).setDeviationThreshold(threshold) + }) - it('should be callable by owner', async function () { - await expect(uFragmentsPolicy.connect(deployer).setRebaseFunctionGrowth(1)) - .to.not.be.reverted + it('should set deviationThreshold', async function () { + expect(await uFragmentsPolicy.deviationThreshold()).to.eq(threshold) + }) }) - it('should NOT be callable by non-owner', async function () { - await expect(uFragmentsPolicy.connect(user).setRebaseFunctionGrowth(1)).to - .be.reverted - }) -}) + describe('UFragments:setDeviationThreshold:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) -describe('UFragments:setRebaseFunctionLowerPercentage:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) + it('should be callable by owner', async function () { + await expect(uFragmentsPolicy.connect(deployer).setDeviationThreshold(0)) + .to.not.be.reverted + }) - it('should be callable by owner', async function () { - await expect( - uFragmentsPolicy.connect(deployer).setRebaseFunctionLowerPercentage(-1), - ).to.not.be.reverted + it('should NOT be callable by non-owner', async function () { + await expect(uFragmentsPolicy.connect(user).setDeviationThreshold(0)).to + .be.reverted + }) }) - it('should NOT be callable by non-owner', async function () { - await expect( - uFragmentsPolicy.connect(user).setRebaseFunctionLowerPercentage(-1), - ).to.be.reverted - }) -}) + describe('UFragmentsPolicy:CurveParameters', async function () { + before('setup UFragmentsPolicy contract', async function () { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) -describe('UFragments:setRebaseFunctionUpperPercentage:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) + describe('when rebaseFunctionGrowth is more than 0', async function () { + it('should setRebaseFunctionGrowth', async function () { + await uFragmentsPolicy.connect(deployer).setRebaseFunctionGrowth(1000) + expect(await uFragmentsPolicy.rebaseFunctionGrowth()).to.eq(1000) + }) + }) - it('should be callable by owner', async function () { - await expect( - uFragmentsPolicy.connect(deployer).setRebaseFunctionUpperPercentage(1), - ).to.not.be.reverted - }) + describe('when rebaseFunctionGrowth is less than 0', async function () { + it('should fail', async function () { + await expect( + uFragmentsPolicy.connect(deployer).setRebaseFunctionGrowth(-1), + ).to.be.reverted + }) + }) - it('should NOT be callable by non-owner', async function () { - await expect( - uFragmentsPolicy.connect(user).setRebaseFunctionUpperPercentage(1), - ).to.be.reverted - }) -}) + describe('when rebaseFunctionLowerPercentage is more than 0', async function () { + it('should fail', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setRebaseFunctionLowerPercentage(1000), + ).to.be.reverted + }) + }) -describe('UFragmentsPolicy:setRebaseTimingParameters', async function () { - before('setup UFragmentsPolicy contract', async function () { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) + describe('when rebaseFunctionLowerPercentage is less than 0', async function () { + it('should setRebaseFunctionLowerPercentage', async function () { + await uFragmentsPolicy + .connect(deployer) + .setRebaseFunctionLowerPercentage(-1) + expect(await uFragmentsPolicy.rebaseFunctionLowerPercentage()).to.eq(-1) + }) + }) - describe('when interval=0', function () { - it('should fail', async function () { - await expect( - uFragmentsPolicy.connect(deployer).setRebaseTimingParameters(0, 0, 0), - ).to.be.reverted + describe('when rebaseFunctionUpperPercentage is less than 0', async function () { + it('should fail', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setRebaseFunctionUpperPercentage(-1), + ).to.be.reverted + }) }) - }) - describe('when offset > interval', function () { - it('should fail', async function () { - await expect( - uFragmentsPolicy + describe('when rebaseFunctionUpperPercentage is more than 0', async function () { + it('should setRebaseFunctionUpperPercentage', async function () { + await uFragmentsPolicy .connect(deployer) - .setRebaseTimingParameters(300, 3600, 300), - ).to.be.reverted + .setRebaseFunctionUpperPercentage(1000) + expect(await uFragmentsPolicy.rebaseFunctionUpperPercentage()).to.eq( + 1000, + ) + }) }) }) - describe('when params are valid', function () { - it('should setRebaseTimingParameters', async function () { - await uFragmentsPolicy - .connect(deployer) - .setRebaseTimingParameters(600, 60, 300) - expect(await uFragmentsPolicy.minRebaseTimeIntervalSec()).to.eq(600) - expect(await uFragmentsPolicy.rebaseWindowOffsetSec()).to.eq(60) - expect(await uFragmentsPolicy.rebaseWindowLengthSec()).to.eq(300) + describe('UFragments:setRebaseFunctionGrowth:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - }) -}) - -describe('UFragments:setRebaseTimingParameters:accessControl', function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - }) - it('should be callable by owner', async function () { - await expect( - uFragmentsPolicy - .connect(deployer) - .setRebaseTimingParameters(600, 60, 300), - ).to.not.be.reverted - }) + it('should be callable by owner', async function () { + await expect( + uFragmentsPolicy.connect(deployer).setRebaseFunctionGrowth(1), + ).to.not.be.reverted + }) - it('should NOT be callable by non-owner', async function () { - await expect( - uFragmentsPolicy.connect(user).setRebaseTimingParameters(600, 60, 300), - ).to.be.reverted + it('should NOT be callable by non-owner', async function () { + await expect(uFragmentsPolicy.connect(user).setRebaseFunctionGrowth(1)).to + .be.reverted + }) }) -}) -describe('UFragmentsPolicy:Rebase:accessControl', async function () { - beforeEach('setup UFragmentsPolicy contract', async function () { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - // await setupContractsWithOpenRebaseWindow() - await mockExternalData( - INITIAL_RATE_30P_MORE, - INITIAL_TARGET_RATE, - 1000, - true, - ) - await increaseTime(60) - }) + describe('UFragments:setRebaseFunctionLowerPercentage:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + }) - describe('when rebase called by orchestrator', function () { - it('should succeed', async function () { - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be - .reverted + it('should be callable by owner', async function () { + await expect( + uFragmentsPolicy.connect(deployer).setRebaseFunctionLowerPercentage(-1), + ).to.not.be.reverted }) - }) - describe('when rebase called by non-orchestrator', function () { - it('should fail', async function () { - await expect(uFragmentsPolicy.connect(user).rebase()).to.be.reverted + it('should NOT be callable by non-owner', async function () { + await expect( + uFragmentsPolicy.connect(user).setRebaseFunctionLowerPercentage(-1), + ).to.be.reverted }) }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - }) - - describe('when minRebaseTimeIntervalSec has NOT passed since the previous rebase', function () { - before(async function () { - await mockExternalData(INITIAL_RATE_30P_MORE, INITIAL_TARGET_RATE, 1010) - await increaseTime(60) - await uFragmentsPolicy.connect(orchestrator).rebase() + describe('UFragments:setRebaseFunctionUpperPercentage:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - it('should fail', async function () { - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.be - .reverted + it('should be callable by owner', async function () { + await expect( + uFragmentsPolicy.connect(deployer).setRebaseFunctionUpperPercentage(1), + ).to.not.be.reverted }) - }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + it('should NOT be callable by non-owner', async function () { + await expect( + uFragmentsPolicy.connect(user).setRebaseFunctionUpperPercentage(1), + ).to.be.reverted + }) }) - describe('when rate is within deviationThreshold', function () { - before(async function () { - await uFragmentsPolicy - .connect(deployer) - .setRebaseTimingParameters(60, 0, 60) + describe('UFragmentsPolicy:setRebaseTimingParameters', async function () { + before('setup UFragmentsPolicy contract', async function () { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - it('should return 0', async function () { - await mockExternalData(INITIAL_RATE.sub(1), INITIAL_TARGET_RATE, 1000) - await increaseTime(60) - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(0) - await increaseTime(60) - - await mockExternalData(INITIAL_RATE.add(1), INITIAL_TARGET_RATE, 1000) - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(0) - await increaseTime(60) - - await mockExternalData( - INITIAL_RATE_2P_MORE.sub(2), - INITIAL_TARGET_RATE, - 1000, - ) - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(0) - await increaseTime(60) - - await mockExternalData( - INITIAL_RATE_2P_LESS.add(2), - INITIAL_TARGET_RATE, - 1000, - ) - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(0) - await increaseTime(60) + describe('when interval=0', function () { + it('should fail', async function () { + await expect( + uFragmentsPolicy.connect(deployer).setRebaseTimingParameters(0, 0, 0), + ).to.be.reverted + }) }) - }) -}) - -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - }) - - describe('when rate is more than MAX_RATE', function () { - it('should return same supply delta as delta for MAX_RATE', async function () { - // Any exchangeRate >= (MAX_RATE=100x) would result in the same supply increase - await mockExternalData(MAX_RATE, INITIAL_TARGET_RATE, 1000) - await increaseTime(60) - - const supplyChange = ( - await parseRebaseEvent(uFragmentsPolicy.connect(orchestrator).rebase()) - ).requestedSupplyAdjustment - - await increaseTime(60) - await mockExternalData( - MAX_RATE.add(ethers.utils.parseUnits('1', 17)), - INITIAL_TARGET_RATE, - 1000, - ) - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(supplyChange) - - await increaseTime(60) - - await mockExternalData(MAX_RATE.mul(2), INITIAL_TARGET_RATE, 1000) - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(supplyChange) + describe('when offset > interval', function () { + it('should fail', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setRebaseTimingParameters(300, 3600, 300), + ).to.be.reverted + }) }) - }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + describe('when params are valid', function () { + it('should setRebaseTimingParameters', async function () { + await uFragmentsPolicy + .connect(deployer) + .setRebaseTimingParameters(600, 60, 300) + expect(await uFragmentsPolicy.minRebaseTimeIntervalSec()).to.eq(600) + expect(await uFragmentsPolicy.rebaseWindowOffsetSec()).to.eq(60) + expect(await uFragmentsPolicy.rebaseWindowLengthSec()).to.eq(300) + }) + }) }) - describe('when uFragments grows beyond MAX_SUPPLY', function () { - before(async function () { - await mockExternalData( - INITIAL_RATE_2X, - INITIAL_TARGET_RATE, - MAX_SUPPLY.sub(1), - ) - await increaseTime(60) + describe('UFragments:setRebaseTimingParameters:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - it('should apply SupplyAdjustment {MAX_SUPPLY - totalSupply}', async function () { - // Supply is MAX_SUPPLY-1, exchangeRate is 2x; resulting in a new supply more than MAX_SUPPLY - // However, supply is ONLY increased by 1 to MAX_SUPPLY - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(1) + it('should be callable by owner', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setRebaseTimingParameters(600, 60, 300), + ).to.not.be.reverted }) - }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + it('should NOT be callable by non-owner', async function () { + await expect( + uFragmentsPolicy.connect(user).setRebaseTimingParameters(600, 60, 300), + ).to.be.reverted + }) }) - describe('when uFragments supply equals MAX_SUPPLY and rebase attempts to grow', function () { - before(async function () { - await mockExternalData(INITIAL_RATE_2X, INITIAL_TARGET_RATE, MAX_SUPPLY) - await increaseTime(60) + describe('UFragmentsPolicy:setRebaseCircuitBreakerParameters', async function () { + before('setup UFragmentsPolicy contract', async function () { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - it('should not grow', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), + describe('when decline<0', function () { + it('should fail', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setRebaseCircuitBreakerParameters(0, -1), + ).to.be.reverted + }) + }) + + describe('when decline>100%', function () { + it('should fail', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setRebaseCircuitBreakerParameters( + 0, + ethers.utils.parseUnits('1.01', 18), + ), + ).to.be.reverted + }) + }) + + describe('when params are valid', function () { + it('should setRebaseTimingParameters', async function () { + await uFragmentsPolicy + .connect(deployer) + .setRebaseCircuitBreakerParameters( + 7, + ethers.utils.parseUnits('0.15', 18), ) - ).requestedSupplyAdjustment, - ).to.eq(0) + expect(await uFragmentsPolicy.epochLookback()).to.eq(7) + expect(await uFragmentsPolicy.tolerableDeclinePercentage()).to.eq( + ethers.utils.parseUnits('0.15', 18), + ) + }) }) }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - }) - - describe('when the market oracle returns invalid data', function () { - it('should NOT fail', async function () { - await mockExternalData( - INITIAL_RATE_30P_MORE, - INITIAL_TARGET_RATE, - 1000, - false, - ) - await increaseTime(60) - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be - .reverted + describe('UFragments:setRebaseCircuitBreakerParameters:accessControl', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) }) - }) - describe('when the market oracle returns valid data', function () { - it('should NOT fail', async function () { - await mockExternalData( - INITIAL_RATE_30P_MORE, - INITIAL_TARGET_RATE, - 1000, - true, - ) - await increaseTime(60) - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be - .reverted + it('should be callable by owner', async function () { + await expect( + uFragmentsPolicy + .connect(deployer) + .setRebaseCircuitBreakerParameters( + 7, + ethers.utils.parseUnits('0.15', 18), + ), + ).to.not.be.reverted }) - }) -}) - -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - }) - describe('when the cpi oracle returns invalid data', function () { - it('should NOT fail', async function () { - await mockExternalData( - INITIAL_RATE_30P_MORE, - INITIAL_TARGET_RATE, - 1000, - true, - false, - ) - await increaseTime(60) - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be - .reverted + it('should NOT be callable by non-owner', async function () { + await expect( + uFragmentsPolicy + .connect(user) + .setRebaseCircuitBreakerParameters( + 7, + ethers.utils.parseUnits('0.15', 18), + ), + ).to.be.reverted }) }) - describe('when the cpi oracle returns valid data', function () { - it('should NOT fail', async function () { + describe('UFragmentsPolicy:Rebase:accessControl', async function () { + beforeEach('setup UFragmentsPolicy contract', async function () { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + // await setupContractsWithOpenRebaseWindow() await mockExternalData( INITIAL_RATE_30P_MORE, INITIAL_TARGET_RATE, 1000, true, - true, ) await increaseTime(60) - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be - .reverted }) - }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - }) - - describe('positive rate and no change target', function () { - beforeEach(async function () { - await mockExternalData(INITIAL_RATE_30P_MORE, INITIAL_TARGET_RATE, 1000) - await uFragmentsPolicy - .connect(deployer) - .setRebaseTimingParameters(60, 0, 60) - await increaseTime(60) - await uFragmentsPolicy.connect(orchestrator).rebase() - prevEpoch = await uFragmentsPolicy.epoch() - prevTime = await uFragmentsPolicy.lastRebaseTimestampSec() - await mockExternalData(INITIAL_RATE_60P_MORE, INITIAL_TARGET_RATE, 1010) - await increaseTime(60) + describe('when rebase called by orchestrator', function () { + it('should succeed', async function () { + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be + .reverted + }) }) - it('should increment epoch', async function () { - await uFragmentsPolicy.connect(orchestrator).rebase() - expect(await uFragmentsPolicy.epoch()).to.eq(prevEpoch.add(1)) + describe('when rebase called by non-orchestrator', function () { + it('should fail', async function () { + await expect(uFragmentsPolicy.connect(user).rebase()).to.be.reverted + }) }) + }) - it('should update globalAmpleforthEpochAndAMPLSupply', async function () { - await uFragmentsPolicy.connect(orchestrator).rebase() - const r = await uFragmentsPolicy.globalAmpleforthEpochAndAMPLSupply() - expect(r[0]).to.eq(prevEpoch.add(1)) - expect(r[1]).to.eq('1010') + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should update lastRebaseTimestamp', async function () { - await uFragmentsPolicy.connect(orchestrator).rebase() - const time = await uFragmentsPolicy.lastRebaseTimestampSec() - expect(time.sub(prevTime)).to.gte(60) - }) + describe('when minRebaseTimeIntervalSec has NOT passed since the previous rebase', function () { + before(async function () { + await mockExternalData(INITIAL_RATE_30P_MORE, INITIAL_TARGET_RATE, 1010) + await increaseTime(60) + await uFragmentsPolicy.connect(orchestrator).rebase() + }) - it('should emit Rebase with positive requestedSupplyAdjustment', async function () { - const r = uFragmentsPolicy.connect(orchestrator).rebase() - await expect(r) - .to.emit(uFragmentsPolicy, 'LogRebaseV2') - .withArgs( - prevEpoch.add(1), - INITIAL_RATE_60P_MORE, - INITIAL_TARGET_RATE, - 55, - ) + it('should fail', async function () { + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.be + .reverted + }) }) + }) - it('should call getData from the market oracle', async function () { - await expect(uFragmentsPolicy.connect(orchestrator).rebase()) - .to.emit(mockMarketOracle, 'FunctionCalled') - .withArgs('MarketOracle', 'getData', uFragmentsPolicy.address) - }) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, - it('should call getData from the cpi oracle', async function () { - await expect(uFragmentsPolicy.connect(orchestrator).rebase()) - .to.emit(mockCpiOracle, 'FunctionCalled') - .withArgs('CpiOracle', 'getData', uFragmentsPolicy.address) + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should call uFrag Rebase', async function () { - const r = uFragmentsPolicy.connect(orchestrator).rebase() - await expect(r) - .to.emit(mockUFragments, 'FunctionCalled') - .withArgs('UFragments', 'rebase', uFragmentsPolicy.address) - await expect(r) - .to.emit(mockUFragments, 'FunctionArguments') - .withArgs([prevEpoch.add(1)], [55]) + describe('when rate is within deviationThreshold', function () { + before(async function () { + await uFragmentsPolicy + .connect(deployer) + .setRebaseTimingParameters(60, 0, 60) + }) + + it('should return 0', async function () { + await mockExternalData(INITIAL_RATE.sub(1), INITIAL_TARGET_RATE, 1000) + await increaseTime(60) + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + await increaseTime(60) + + await mockExternalData(INITIAL_RATE.add(1), INITIAL_TARGET_RATE, 1000) + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + await increaseTime(60) + + await mockExternalData( + INITIAL_RATE_2P_MORE.sub(2), + INITIAL_TARGET_RATE, + 1000, + ) + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + await increaseTime(60) + + await mockExternalData( + INITIAL_RATE_2P_LESS.add(2), + INITIAL_TARGET_RATE, + 1000, + ) + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + await increaseTime(60) + }) }) }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - }) - - describe('negative rate', function () { - before(async function () { - await mockExternalData(INITIAL_RATE_30P_LESS, INITIAL_TARGET_RATE, 1000) - await increaseTime(60) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should emit Rebase with negative requestedSupplyAdjustment', async function () { - expect( - ( + describe('when rate is more than MAX_RATE', function () { + it('should return same supply delta as delta for MAX_RATE', async function () { + // Any exchangeRate >= (MAX_RATE=100x) would result in the same supply increase + await mockExternalData(MAX_RATE, INITIAL_TARGET_RATE, 1000) + await increaseTime(60) + + const supplyChange = ( await parseRebaseEvent( uFragmentsPolicy.connect(orchestrator).rebase(), ) - ).requestedSupplyAdjustment, - ).to.eq(-29) + ).requestedSupplyAdjustment + + await increaseTime(60) + + await mockExternalData( + MAX_RATE.add(ethers.utils.parseUnits('1', 17)), + INITIAL_TARGET_RATE, + 1000, + ) + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(supplyChange) + + await increaseTime(60) + + await mockExternalData(MAX_RATE.mul(2), INITIAL_TARGET_RATE, 1000) + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(supplyChange) + }) }) }) - describe('max positive rebase', function () { - before(async function () { - await mockExternalData(INITIAL_RATE_2X, INITIAL_TARGET_RATE, 1000) - await uFragmentsPolicy - .connect(deployer) - .setRebaseFunctionGrowth('100' + '000000000000000000') - await increaseTime(60) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should emit Rebase with positive requestedSupplyAdjustment', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(100) + describe('when uFragments grows beyond MAX_SUPPLY', function () { + before(async function () { + await mockExternalData( + INITIAL_RATE_2X, + INITIAL_TARGET_RATE, + MAX_SUPPLY.sub(1), + ) + await increaseTime(60) + }) + + it('should apply SupplyAdjustment {MAX_SUPPLY - totalSupply}', async function () { + // Supply is MAX_SUPPLY-1, exchangeRate is 2x; resulting in a new supply more than MAX_SUPPLY + // However, supply is ONLY increased by 1 to MAX_SUPPLY + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(1) + }) }) }) - describe('max negative rebase', function () { - before(async function () { - await mockExternalData(0, INITIAL_TARGET_RATE, 1000) - await uFragmentsPolicy - .connect(deployer) - .setRebaseFunctionGrowth('75' + '000000000000000000') - await increaseTime(60) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should emit Rebase with negative requestedSupplyAdjustment', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(-100) + describe('when uFragments supply equals MAX_SUPPLY and rebase attempts to grow', function () { + before(async function () { + await mockExternalData(INITIAL_RATE_2X, INITIAL_TARGET_RATE, MAX_SUPPLY) + await increaseTime(60) + }) + + it('should not grow', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + }) }) }) - describe('exponent less than -100', function () { - before(async function () { - await mockExternalData(0, INITIAL_TARGET_RATE, 1000) - await uFragmentsPolicy - .connect(deployer) - .setRebaseFunctionGrowth('150' + '000000000000000000') - await increaseTime(60) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should emit Rebase with negative requestedSupplyAdjustment', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(-100) + describe('when the market oracle returns invalid data', function () { + it('should NOT fail', async function () { + await mockExternalData( + INITIAL_RATE_30P_MORE, + INITIAL_TARGET_RATE, + 1000, + false, + ) + await increaseTime(60) + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be + .reverted + }) }) - }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + describe('when the market oracle returns valid data', function () { + it('should NOT fail', async function () { + await mockExternalData( + INITIAL_RATE_30P_MORE, + INITIAL_TARGET_RATE, + 1000, + true, + ) + await increaseTime(60) + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be + .reverted + }) + }) }) - describe('when target increases', function () { - before(async function () { - await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE_25P_MORE, 1000) - await increaseTime(60) - await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should emit Rebase with negative requestedSupplyAdjustment', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(-20) + describe('when the cpi oracle returns invalid data', function () { + it('should NOT fail', async function () { + await mockExternalData( + INITIAL_RATE_30P_MORE, + INITIAL_TARGET_RATE, + 1000, + true, + false, + ) + await increaseTime(60) + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be + .reverted + }) }) - }) -}) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + describe('when the cpi oracle returns valid data', function () { + it('should NOT fail', async function () { + await mockExternalData( + INITIAL_RATE_30P_MORE, + INITIAL_TARGET_RATE, + 1000, + true, + true, + ) + await increaseTime(60) + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be + .reverted + }) + }) }) - describe('when target decreases', function () { - before(async function () { - await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE_25P_LESS, 1000) - await increaseTime(60) - await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should emit Rebase with positive requestedSupplyAdjustment', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), + describe('positive rate and no change target', function () { + beforeEach(async function () { + await mockExternalData(INITIAL_RATE_30P_MORE, INITIAL_TARGET_RATE, 1000) + await uFragmentsPolicy + .connect(deployer) + .setRebaseTimingParameters(60, 0, 60) + await increaseTime(60) + await uFragmentsPolicy.connect(orchestrator).rebase() + prevEpoch = await uFragmentsPolicy.epoch() + prevTime = await uFragmentsPolicy.lastRebaseTimestampSec() + await mockExternalData(INITIAL_RATE_60P_MORE, INITIAL_TARGET_RATE, 1010) + await increaseTime(60) + }) + + it('should increment epoch', async function () { + await uFragmentsPolicy.connect(orchestrator).rebase() + expect(await uFragmentsPolicy.epoch()).to.eq(prevEpoch.add(1)) + }) + + it('should record supply history', async function () { + await uFragmentsPolicy.connect(orchestrator).rebase() + expect(await uFragmentsPolicy.supplyHistory(prevEpoch.add(1))).to.eq( + '1065', + ) + }) + + it('should update globalAmpleforthEpochAndAMPLSupply', async function () { + await uFragmentsPolicy.connect(orchestrator).rebase() + await mockExternalData(INITIAL_RATE_60P_MORE, INITIAL_TARGET_RATE, 1065) + const r = await uFragmentsPolicy.globalAmpleforthEpochAndAMPLSupply() + expect(r[0]).to.eq(prevEpoch.add(1)) + expect(r[1]).to.eq('1065') + }) + + it('should update lastRebaseTimestamp', async function () { + await uFragmentsPolicy.connect(orchestrator).rebase() + const time = await uFragmentsPolicy.lastRebaseTimestampSec() + expect(time.sub(prevTime)).to.gte(60) + }) + + it('should emit Rebase with positive requestedSupplyAdjustment', async function () { + const r = uFragmentsPolicy.connect(orchestrator).rebase() + await expect(r) + .to.emit(uFragmentsPolicy, 'LogRebaseV2') + .withArgs( + prevEpoch.add(1), + INITIAL_RATE_60P_MORE, + INITIAL_TARGET_RATE, + 55, ) - ).requestedSupplyAdjustment, - ).to.eq(32) - }) - }) -}) + }) -describe('UFragmentsPolicy:Rebase', async function () { - before('setup UFragmentsPolicy contract', async () => { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) - }) + it('should call getData from the market oracle', async function () { + await expect(uFragmentsPolicy.connect(orchestrator).rebase()) + .to.emit(mockMarketOracle, 'FunctionCalled') + .withArgs('MarketOracle', 'getData', uFragmentsPolicy.address) + }) - describe('rate=TARGET_RATE', function () { - before(async function () { - await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE, 1000) - await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) - await increaseTime(60) - }) + it('should call getData from the cpi oracle', async function () { + await expect(uFragmentsPolicy.connect(orchestrator).rebase()) + .to.emit(mockCpiOracle, 'FunctionCalled') + .withArgs('CpiOracle', 'getData', uFragmentsPolicy.address) + }) - it('should emit Rebase with 0 requestedSupplyAdjustment', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(0) + it('should call uFrag Rebase', async function () { + const r = uFragmentsPolicy.connect(orchestrator).rebase() + await expect(r) + .to.emit(mockUFragments, 'FunctionCalled') + .withArgs('UFragments', 'rebase', uFragmentsPolicy.address) + await expect(r) + .to.emit(mockUFragments, 'FunctionArguments') + .withArgs([prevEpoch.add(1)], [55]) + }) }) }) - describe('rate is invalid', function () { - before(async function () { - await mockExternalData( - INITIAL_RATE_30P_MORE, - INITIAL_TARGET_RATE, - 1000, - false, - ) - await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) - await increaseTime(60) - }) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + }) + + describe('negative rate', function () { + before(async function () { + await mockExternalData(INITIAL_RATE_30P_LESS, INITIAL_TARGET_RATE, 1000) + await increaseTime(60) + }) + + it('should emit Rebase with negative requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(-30) + }) + }) + + describe('max positive rebase', function () { + before(async function () { + await mockExternalData(INITIAL_RATE_2X, INITIAL_TARGET_RATE, 1000) + await uFragmentsPolicy + .connect(deployer) + .setRebaseFunctionGrowth('100' + '000000000000000000') + await increaseTime(60) + }) + + it('should emit Rebase with positive requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(100) + }) + }) + + describe('max negative rebase', function () { + before(async function () { + await mockExternalData(0, INITIAL_TARGET_RATE, 1000) + await uFragmentsPolicy + .connect(deployer) + .setRebaseFunctionGrowth('75' + '000000000000000000') + await increaseTime(60) + }) + + it('should emit Rebase with negative requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(-100) + }) + }) + + describe('exponent less than -100', function () { + before(async function () { + await mockExternalData(0, INITIAL_TARGET_RATE, 1000) + await uFragmentsPolicy + .connect(deployer) + .setRebaseFunctionGrowth('150' + '000000000000000000') + await increaseTime(60) + }) - it('should emit Rebase with 0 requestedSupplyAdjustment', async function () { - expect( - ( - await parseRebaseEvent( - uFragmentsPolicy.connect(orchestrator).rebase(), - ) - ).requestedSupplyAdjustment, - ).to.eq(0) + it('should emit Rebase with negative requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(-100) + }) }) }) - describe('target rate is invalid', function () { - before(async function () { - await mockExternalData( - INITIAL_RATE, - INITIAL_TARGET_RATE_25P_MORE, - 1000, - true, - false, - ) - await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) - await increaseTime(60) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - it('should emit Rebase with 0 requestedSupplyAdjustment', async function () { - expect( - (await parseRebaseEvent(uFragmentsPolicy.connect(orchestrator).rebase())) - .requestedSupplyAdjustment, - ).to.eq(0) - }) - }) -}) + describe('when target increases', function () { + before(async function () { + await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE_25P_MORE, 1000) + await increaseTime(60) + await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) + }) -describe('UFragmentsPolicy:Rebase', async function () { - let rbTime: BigNumber, - rbWindow: BigNumber, - minRebaseTimeIntervalSec: BigNumber, - now: BigNumber, - nextRebaseWindowOpenTime: BigNumber, - timeToWait: BigNumber, - lastRebaseTimestamp: BigNumber - - beforeEach('setup UFragmentsPolicy contract', async function () { - ;({ - deployer, - user, - orchestrator, - mockUFragments, - mockMarketOracle, - mockCpiOracle, - uFragmentsPolicy, - } = await waffle.loadFixture(mockedUpgradablePolicy)) - await uFragmentsPolicy - .connect(deployer) - .setRebaseTimingParameters(86400, 72000, 900) - await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE, 1000) - rbTime = await uFragmentsPolicy.rebaseWindowOffsetSec() - rbWindow = await uFragmentsPolicy.rebaseWindowLengthSec() - minRebaseTimeIntervalSec = await uFragmentsPolicy.minRebaseTimeIntervalSec() - now = ethers.BigNumber.from( - (await ethers.provider.getBlock('latest')).timestamp, - ) - nextRebaseWindowOpenTime = now - .sub(now.mod(minRebaseTimeIntervalSec)) - .add(rbTime) - .add(minRebaseTimeIntervalSec) + it('should emit Rebase with negative requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(-21) + }) + }) }) - describe('when its 5s after the rebase window closes', function () { - it('should fail', async function () { - timeToWait = nextRebaseWindowOpenTime.sub(now).add(rbWindow).add(5) - await increaseTime(timeToWait) - expect(await uFragmentsPolicy.inRebaseWindow()).to.be.false - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.be - .reverted + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) }) - }) - describe('when its 5s before the rebase window opens', function () { - it('should fail', async function () { - timeToWait = nextRebaseWindowOpenTime.sub(now).sub(5) - await increaseTime(timeToWait) - expect(await uFragmentsPolicy.inRebaseWindow()).to.be.false - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.be - .reverted + describe('when target decreases', function () { + before(async function () { + await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE_25P_LESS, 1000) + await increaseTime(60) + await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) + }) + + it('should emit Rebase with positive requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(32) + }) }) }) - describe('when its 5s after the rebase window opens', function () { - it('should NOT fail', async function () { - timeToWait = nextRebaseWindowOpenTime.sub(now).add(5) - await increaseTime(timeToWait) - expect(await uFragmentsPolicy.inRebaseWindow()).to.be.true - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be - .reverted - lastRebaseTimestamp = await uFragmentsPolicy.lastRebaseTimestampSec() - expect(lastRebaseTimestamp).to.eq(nextRebaseWindowOpenTime) + describe('UFragmentsPolicy:Rebase', async function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + }) + + describe('rate=TARGET_RATE', function () { + before(async function () { + await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE, 1000) + await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) + await increaseTime(60) + }) + + it('should emit Rebase with 0 requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + }) + }) + + describe('rate is invalid', function () { + before(async function () { + await mockExternalData( + INITIAL_RATE_30P_MORE, + INITIAL_TARGET_RATE, + 1000, + false, + ) + await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) + await increaseTime(60) + }) + + it('should emit Rebase with 0 requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + }) + }) + + describe('target rate is invalid', function () { + before(async function () { + await mockExternalData( + INITIAL_RATE, + INITIAL_TARGET_RATE_25P_MORE, + 1000, + true, + false, + ) + await uFragmentsPolicy.connect(deployer).setDeviationThreshold(0) + await increaseTime(60) + }) + + it('should emit Rebase with 0 requestedSupplyAdjustment', async function () { + expect( + ( + await parseRebaseEvent( + uFragmentsPolicy.connect(orchestrator).rebase(), + ) + ).requestedSupplyAdjustment, + ).to.eq(0) + }) }) }) - describe('when its 5s before the rebase window closes', function () { - it('should NOT fail', async function () { - timeToWait = nextRebaseWindowOpenTime.sub(now).add(rbWindow).sub(5) - await increaseTime(timeToWait) - expect(await uFragmentsPolicy.inRebaseWindow()).to.be.true - await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be - .reverted - lastRebaseTimestamp = await uFragmentsPolicy.lastRebaseTimestampSec.call() - expect(lastRebaseTimestamp).to.eq(nextRebaseWindowOpenTime) + describe('UFragmentsPolicy:Rebase', function () { + before('setup UFragmentsPolicy contract', async () => { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicyWithOpenRebaseWindow)) + + await uFragmentsPolicy.setDeviationThreshold(0) + await uFragmentsPolicy.setRebaseCircuitBreakerParameters( + 7, + ethers.utils.parseUnits('0.05', 18), + ) + await uFragmentsPolicy.setOrchestrator(await deployer.getAddress()) + }) + + describe('when uFragments shrinks and rebase attempts to go outside tolerance', function () { + it('should limit the decline', async function () { + await mockExternalData(INITIAL_RATE_30P_MORE, INITIAL_TARGET_RATE, 1000) + await increaseTime(60) + await uFragmentsPolicy.rebase() + + await mockExternalData(INITIAL_RATE_30P_MORE, INITIAL_TARGET_RATE) + await increaseTime(60) + await uFragmentsPolicy.rebase() + + await mockExternalData(INITIAL_RATE_2P_LESS, INITIAL_TARGET_RATE) + await increaseTime(60) + await uFragmentsPolicy.rebase() + + await mockExternalData(INITIAL_RATE_2P_LESS, INITIAL_TARGET_RATE) + await increaseTime(60) + await uFragmentsPolicy.rebase() + + await mockExternalData(INITIAL_RATE_50P_LESS, INITIAL_TARGET_RATE) + await increaseTime(60) + expect( + (await parseRebaseEvent(uFragmentsPolicy.rebase())) + .requestedSupplyAdjustment, + ).to.eq(-49) + + await mockExternalData(INITIAL_RATE_50P_LESS, INITIAL_TARGET_RATE) + await increaseTime(60) + expect( + (await parseRebaseEvent(uFragmentsPolicy.rebase())) + .requestedSupplyAdjustment, + ).to.eq(0) + }) + }) + }) + + describe('UFragmentsPolicy:Rebase', function () { + let rbTime: BigNumber, + rbWindow: BigNumber, + minRebaseTimeIntervalSec: BigNumber, + now: BigNumber, + nextRebaseWindowOpenTime: BigNumber, + timeToWait: BigNumber, + lastRebaseTimestamp: BigNumber + + beforeEach('setup UFragmentsPolicy contract', async function () { + ;({ + deployer, + user, + orchestrator, + mockUFragments, + mockMarketOracle, + mockCpiOracle, + uFragmentsPolicy, + } = await waffle.loadFixture(mockedUpgradablePolicy)) + await uFragmentsPolicy + .connect(deployer) + .setRebaseTimingParameters(86400, 72000, 900) + await mockExternalData(INITIAL_RATE, INITIAL_TARGET_RATE, 1000) + rbTime = await uFragmentsPolicy.rebaseWindowOffsetSec() + rbWindow = await uFragmentsPolicy.rebaseWindowLengthSec() + minRebaseTimeIntervalSec = + await uFragmentsPolicy.minRebaseTimeIntervalSec() + now = ethers.BigNumber.from( + (await ethers.provider.getBlock('latest')).timestamp, + ) + nextRebaseWindowOpenTime = now + .sub(now.mod(minRebaseTimeIntervalSec)) + .add(rbTime) + .add(minRebaseTimeIntervalSec) + }) + + describe('when its 5s after the rebase window closes', function () { + it('should fail', async function () { + timeToWait = nextRebaseWindowOpenTime.sub(now).add(rbWindow).add(5) + await increaseTime(timeToWait) + expect(await uFragmentsPolicy.inRebaseWindow()).to.be.false + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.be + .reverted + }) + }) + + describe('when its 5s before the rebase window opens', function () { + it('should fail', async function () { + timeToWait = nextRebaseWindowOpenTime.sub(now).sub(5) + await increaseTime(timeToWait) + expect(await uFragmentsPolicy.inRebaseWindow()).to.be.false + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.be + .reverted + }) + }) + + describe('when its 5s after the rebase window opens', function () { + it('should NOT fail', async function () { + timeToWait = nextRebaseWindowOpenTime.sub(now).add(5) + await increaseTime(timeToWait) + expect(await uFragmentsPolicy.inRebaseWindow()).to.be.true + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be + .reverted + lastRebaseTimestamp = await uFragmentsPolicy.lastRebaseTimestampSec() + expect(lastRebaseTimestamp).to.eq(nextRebaseWindowOpenTime) + }) + }) + + describe('when its 5s before the rebase window closes', function () { + it('should NOT fail', async function () { + timeToWait = nextRebaseWindowOpenTime.sub(now).add(rbWindow).sub(5) + await increaseTime(timeToWait) + expect(await uFragmentsPolicy.inRebaseWindow()).to.be.true + await expect(uFragmentsPolicy.connect(orchestrator).rebase()).to.not.be + .reverted + lastRebaseTimestamp = + await uFragmentsPolicy.lastRebaseTimestampSec.call() + expect(lastRebaseTimestamp).to.eq(nextRebaseWindowOpenTime) + }) }) }) }) From 21060f880c5b312d3433ddfa1167f028a8678d9e Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:56:47 -0400 Subject: [PATCH 2/3] code review comments --- contracts/UFragmentsPolicy.sol | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/UFragmentsPolicy.sol b/contracts/UFragmentsPolicy.sol index d75b0224..5393e2d3 100644 --- a/contracts/UFragmentsPolicy.sol +++ b/contracts/UFragmentsPolicy.sol @@ -378,18 +378,21 @@ contract UFragmentsPolicy is Ownable { // When supply is decreasing: // We limit the supply delta, based on recent supply history. if (rebasePercentage < 0) { - int256 maxSupplyInHistory = currentSupply; - for (uint8 i = 1; i < epochLookback && epoch > i; i++) { - int256 epochSupply = supplyHistory[epoch - i].toInt256Safe(); - if (epochSupply > maxSupplyInHistory) { - maxSupplyInHistory = epochSupply; + int256 maxSupply = currentSupply; + for ( + uint256 e = ((epoch > epochLookback) ? (epoch - epochLookback) : 0); + e < epoch; + e++ + ) { + int256 epochSupply = supplyHistory[e].toInt256Safe(); + if (epochSupply > maxSupply) { + maxSupply = epochSupply; } } - int256 allowedSupplyMinimum = maxSupplyInHistory - .mul(ONE.sub(tolerableDeclinePercentage)) - .div(ONE); - newSupply = (newSupply > allowedSupplyMinimum) ? newSupply : allowedSupplyMinimum; - require(newSupply <= currentSupply); + int256 allowedMin = maxSupply.mul(ONE.sub(tolerableDeclinePercentage)).div(ONE); + if (newSupply < allowedMin) { + newSupply = allowedMin; + } } return newSupply.sub(currentSupply); From 98ae0127da465d820af45fb8a7d3a1b114c8f61f Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:51:27 -0500 Subject: [PATCH 3/3] circuit breaking based on supply on look-back time (not max in the period) --- contracts/UFragmentsPolicy.sol | 22 +++++++++------------- test/unit/UFragmentsPolicy.ts | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/contracts/UFragmentsPolicy.sol b/contracts/UFragmentsPolicy.sol index 5393e2d3..b8f31a07 100644 --- a/contracts/UFragmentsPolicy.sol +++ b/contracts/UFragmentsPolicy.sol @@ -236,7 +236,7 @@ contract UFragmentsPolicy is Ownable { /** * @notice Sets the parameters which control rebase circuit breaker. * @param epochLookback_ The number of rebase epochs to look back. - * @param tolerableDeclinePercentage_ The maximum supply decline percentage which is allowed + * @param tolerableDeclinePercentage_ The supply decline percentage which is allowed * within the defined look back period. */ function setRebaseCircuitBreakerParameters( @@ -377,19 +377,15 @@ contract UFragmentsPolicy is Ownable { // When supply is decreasing: // We limit the supply delta, based on recent supply history. - if (rebasePercentage < 0) { - int256 maxSupply = currentSupply; - for ( - uint256 e = ((epoch > epochLookback) ? (epoch - epochLookback) : 0); - e < epoch; - e++ - ) { - int256 epochSupply = supplyHistory[e].toInt256Safe(); - if (epochSupply > maxSupply) { - maxSupply = epochSupply; - } + if (rebasePercentage < 0 && epoch > epochLookback) { + int256 allowedMin = supplyHistory[epoch - epochLookback] + .toInt256Safe() + .mul(ONE.sub(tolerableDeclinePercentage)) + .div(ONE); + if (allowedMin > currentSupply) { + // NOTE: Allowed minimum supply can only be at most the current supply. + allowedMin = currentSupply; } - int256 allowedMin = maxSupply.mul(ONE.sub(tolerableDeclinePercentage)).div(ONE); if (newSupply < allowedMin) { newSupply = allowedMin; } diff --git a/test/unit/UFragmentsPolicy.ts b/test/unit/UFragmentsPolicy.ts index c4a6bc07..15691381 100644 --- a/test/unit/UFragmentsPolicy.ts +++ b/test/unit/UFragmentsPolicy.ts @@ -1409,7 +1409,21 @@ describe('UFragmentsPolicy', function () { expect( (await parseRebaseEvent(uFragmentsPolicy.rebase())) .requestedSupplyAdjustment, - ).to.eq(-49) + ).to.eq(-51) + + await mockExternalData(INITIAL_RATE_50P_LESS, INITIAL_TARGET_RATE) + await increaseTime(60) + expect( + (await parseRebaseEvent(uFragmentsPolicy.rebase())) + .requestedSupplyAdjustment, + ).to.eq(-48) + + await mockExternalData(INITIAL_RATE_50P_LESS, INITIAL_TARGET_RATE) + await increaseTime(60) + expect( + (await parseRebaseEvent(uFragmentsPolicy.rebase())) + .requestedSupplyAdjustment, + ).to.eq(-46) await mockExternalData(INITIAL_RATE_50P_LESS, INITIAL_TARGET_RATE) await increaseTime(60)