From 6a150c2cd45335935ebd31c811cfe623114fef1c Mon Sep 17 00:00:00 2001 From: mejango Date: Mon, 21 Aug 2023 10:42:34 -0400 Subject: [PATCH 01/22] queue multiple fcs --- contracts/JBFundingCycleStore.sol | 58 ++++---- contracts/JBReconfigurationBufferBallot.sol | 2 + hardhat.config.js | 2 +- .../configure_for.test.js | 127 +++++++++++++++++- 4 files changed, 159 insertions(+), 30 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index 625299cef..61c5796cb 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -265,8 +265,13 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { } } - // The configuration timestamp is now. - uint256 _configuration = block.timestamp; + // Get a reference to the latest configration. + uint256 _latestConfiguration = latestConfigurationOf[_projectId]; + + // The configuration timestamp is now, or an increment from now if the current timestamp is taken. + uint256 _configuration = _latestConfiguration >= block.timestamp + ? _latestConfiguration + 1 + : block.timestamp; // Set up a reconfiguration by configuring intrinsic properties. _configureIntrinsicPropertiesFor(_projectId, _configuration, _data.weight, _mustStartAtOrAfter); @@ -332,10 +337,12 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Get a reference to the funding cycle. JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _currentConfiguration); - if (!_isApproved(_projectId, _baseFundingCycle) || block.timestamp < _baseFundingCycle.start) - // If it hasn't been approved or hasn't yet started, set the ID to be the funding cycle it's based on, - // which carries the latest approved configuration. - _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); + // If the funding cycle hasn't started but is currently approved OR or it has started but wasn't approved, set the ID to be the funding cycle it's based on, + // which carries the latest approved configuration. + if ( + (block.timestamp < _baseFundingCycle.start && _isApproved(_projectId, _baseFundingCycle)) || + (block.timestamp > _baseFundingCycle.start && !_isApproved(_projectId, _baseFundingCycle)) + ) _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); // The configuration can't be the same as the base configuration. if (_baseFundingCycle.configuration == _configuration) revert NO_SAME_BLOCK_RECONFIGURATION(); @@ -476,35 +483,30 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { /// @dev A value of 0 is returned if no funding cycle was found. /// @dev Assumes the project has a latest configuration. /// @param _projectId The ID of the project to look through. - /// @return configuration The configuration of an eligible funding cycle if one exists, or 0 if one doesn't exist. - function _eligibleOf(uint256 _projectId) private view returns (uint256 configuration) { + /// @return The configuration of an eligible funding cycle if one exists, or 0 if one doesn't exist. + function _eligibleOf(uint256 _projectId) private view returns (uint256) { // Get a reference to the project's latest funding cycle. - configuration = latestConfigurationOf[_projectId]; + uint256 _configuration = latestConfigurationOf[_projectId]; // Get the latest funding cycle. - JBFundingCycle memory _fundingCycle = _getStructFor(_projectId, configuration); - - // If the latest is expired, return an empty funding cycle. - // A duration of 0 cannot be expired. - if ( - _fundingCycle.duration > 0 && block.timestamp >= _fundingCycle.start + _fundingCycle.duration - ) return 0; + JBFundingCycle memory _fundingCycle = _getStructFor(_projectId, _configuration); - // Return the funding cycle's configuration if it has started. - if (block.timestamp >= _fundingCycle.start) return _fundingCycle.configuration; + // Loop through all most recently configured funding cycles until an eligible one is found, or we've proven one can't exit. + while (_fundingCycle.number != 0) { + // If the latest is expired, return an empty funding cycle. + // A duration of 0 cannot be expired. + if ( + _fundingCycle.duration != 0 && + block.timestamp >= _fundingCycle.start + _fundingCycle.duration + ) return 0; - // Get a reference to the cycle's base configuration. - JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _fundingCycle.basedOn); + // Return the funding cycle's configuration if it has started. + if (block.timestamp >= _fundingCycle.start) return _fundingCycle.configuration; - // If the base cycle isn't eligible, the project has no eligible cycle. - // A duration of 0 is always eligible. - if ( - _baseFundingCycle.duration > 0 && - block.timestamp >= _baseFundingCycle.start + _baseFundingCycle.duration - ) return 0; + _fundingCycle = _getStructFor(_projectId, _fundingCycle.basedOn); + } - // Return the configuration that the latest funding cycle is based on. - configuration = _fundingCycle.basedOn; + return 0; } /// @notice A view of the funding cycle that would be created based on the provided one if the project doesn't make a reconfiguration. diff --git a/contracts/JBReconfigurationBufferBallot.sol b/contracts/JBReconfigurationBufferBallot.sol index a1bf09d9f..1bae9d53a 100644 --- a/contracts/JBReconfigurationBufferBallot.sol +++ b/contracts/JBReconfigurationBufferBallot.sol @@ -36,6 +36,8 @@ contract JBReconfigurationBufferBallot is ERC165, IJBFundingCycleBallot { if (_configured > _start) return JBBallotState.Failed; unchecked { + // If the ballot hasn't yet started, it's state is active. + if (block.timestamp < _start - duration) return JBBallotState.Active; // If there was sufficient time between configuration and the start of the cycle, it is approved. Otherwise, it is failed. return (_start - _configured < duration) ? JBBallotState.Failed : JBBallotState.Approved; } diff --git a/hardhat.config.js b/hardhat.config.js index 712df2b84..d4dbdc5b2 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -31,7 +31,7 @@ module.exports = { defaultNetwork, networks: { hardhat: { - allowUnlimitedContractSize: true, + allowUnlimitedContractSize: true }, localhost: { url: 'http://localhost:8545', diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 7112850d2..0ac06dbb4 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -15,7 +15,7 @@ import ijbFundingCycleBallot from '../../artifacts/contracts/interfaces/IJBFundi import { BigNumber } from 'ethers'; import errors from '../helpers/errors.json'; -describe('JBFundingCycleStore::configureFor(...)', function () { +describe.only('JBFundingCycleStore::configureFor(...)', function () { const PROJECT_ID = 2; const EMPTY_FUNDING_CYCLE = { @@ -815,6 +815,131 @@ describe('JBFundingCycleStore::configureFor(...)', function () { ); }); + // it.only('Should not overwrite when configure a subsequent cycle during a funding cycle with approved ballot', async function () { + // const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); + // await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + // const firstFundingCycleData = createFundingCycleData({ ballot: mockBallot.address }); + + // // Configure first funding cycle + // const firstConfigureForTx = await jbFundingCycleStore + // .connect(controller) + // .configureFor( + // PROJECT_ID, + // firstFundingCycleData, + // RANDOM_FUNDING_CYCLE_METADATA_1, + // FUNDING_CYCLE_CAN_START_ASAP, + // ); + + // // The timestamp the first configuration was made during. + // const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + // const expectedFirstFundingCycle = { + // number: ethers.BigNumber.from(1), + // configuration: firstConfigurationTimestamp, + // basedOn: ethers.BigNumber.from(0), + // start: firstConfigurationTimestamp, + // duration: firstFundingCycleData.duration, + // weight: firstFundingCycleData.weight, + // discountRate: firstFundingCycleData.discountRate, + // ballot: firstFundingCycleData.ballot, + // metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + // }; + + // const secondFundingCycleData = createFundingCycleData({ + // ballot: ethers.constants.AddressZero, + // duration: firstFundingCycleData.duration.add(1), + // discountRate: firstFundingCycleData.discountRate.add(1), + // weight: firstFundingCycleData.weight.add(1), + // }); + + // const ballotDuration = firstFundingCycleData.duration / 2; + + // // Set the ballot to have a short duration. + // await mockBallot.mock.duration.withArgs().returns(ballotDuration); + + // // Configure second funding cycle + // const secondConfigureForTx = await jbFundingCycleStore + // .connect(controller) + // .configureFor( + // PROJECT_ID, + // secondFundingCycleData, + // RANDOM_FUNDING_CYCLE_METADATA_2, + // FUNDING_CYCLE_CAN_START_ASAP, + // ); + + // // The timestamp the second configuration was made during. + // const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + // await expect(secondConfigureForTx) + // .to.emit(jbFundingCycleStore, `Init`) + // .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + // //keep half the seconds before the end of the cycle so make all necessary checks before the cycle ends. + // await fastForward( + // firstConfigureForTx.blockNumber, + // firstFundingCycleData.duration.sub(ballotDuration / 2), + // ); + + // const expectedSecondFundingCycle = { + // number: 2, // second cycle + // configuration: secondConfigurationTimestamp, + // basedOn: firstConfigurationTimestamp, // based on the first cycle + // start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the first cycle + // duration: secondFundingCycleData.duration, + // weight: secondFundingCycleData.weight, + // discountRate: secondFundingCycleData.discountRate, + // ballot: secondFundingCycleData.ballot, + // metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + // }; + + // // Mock the ballot on the funding cycle as approved. + // await mockBallot.mock.stateOf + // .withArgs( + // PROJECT_ID, + // secondConfigurationTimestamp, + // firstConfigurationTimestamp.add(firstFundingCycleData.duration), + // ) + // .returns(ballotStatus.APPROVED); + + // const thirdFundingCycleData = createFundingCycleData({ + // ballot: ethers.constants.AddressZero, + // duration: secondFundingCycleData.duration.add(1), + // discountRate: secondFundingCycleData.discountRate.add(1), + // weight: secondFundingCycleData.weight.add(1), + // }); + + // // Configure third funding cycle + // const thirdConfigureForTx = await jbFundingCycleStore + // .connect(controller) + // .configureFor( + // PROJECT_ID, + // thirdFundingCycleData, + // RANDOM_FUNDING_CYCLE_METADATA_2, + // FUNDING_CYCLE_CAN_START_ASAP, + // ); + + // // expect( + // // cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + // // ).to.eql(expectedSecondFundingCycle); + + // let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + // PROJECT_ID, + // ); + // // expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedThirdFundingCycle); + // // expect(ballotState).to.deep.eql(ballotStatus.APPROVED); + // // expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + // // ...expectedFirstFundingCycle, + // // number: expectedFirstFundingCycle.number.add(cycleDiff.sub(1)), + // // start: expectedFirstFundingCycle.start.add( + // // expectedFirstFundingCycle.duration.mul(cycleDiff.sub(1)), + // // ), + // // }); + // // expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql( + // // expectedSecondFundingCycle, + // // ); + // }); + it('Should configure subsequent cycle during a rolled over funding cycle many multiples of duration later', async function () { // Increase timeout because this test will need a long for loop iteration. this.timeout(20000); From 0fa9447cbbd49d8d329b942d9cafe0fa899e05fe Mon Sep 17 00:00:00 2001 From: mejango Date: Mon, 21 Aug 2023 15:20:40 -0400 Subject: [PATCH 02/22] cant replace a queued funding cycle once it's been approved --- contracts/JBFundingCycleStore.sol | 96 +++++--- contracts/JBReconfigurationBufferBallot.sol | 4 +- contracts/enums/JBBallotState.sol | 2 + .../configure_for.test.js | 206 +++++++++++++++--- 4 files changed, 256 insertions(+), 52 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index 61c5796cb..fede74bf2 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -100,11 +100,19 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Get a reference to the configuration of the standby funding cycle. uint256 _standbyFundingCycleConfiguration = _standbyOf(_projectId); + // Keep a reference to the ballot state. + JBBallotState _ballotState; + // If it exists, return its funding cycle if it is approved. - if (_standbyFundingCycleConfiguration > 0) { + if (_standbyFundingCycleConfiguration != 0) { fundingCycle = _getStructFor(_projectId, _standbyFundingCycleConfiguration); - if (_isApproved(_projectId, fundingCycle)) return fundingCycle; + // Get a reference to the ballot state. + _ballotState = _ballotStateOf(_projectId, fundingCycle); + + // If the ballot hasn't failed, return it. + if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) + return fundingCycle; // Resolve the funding cycle for the latest configured funding cycle. fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); @@ -121,9 +129,13 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // There's no queued if the current has a duration of 0. if (fundingCycle.duration == 0) return _getStructFor(0, 0); - // Check to see if this funding cycle's ballot is approved. + // Get a reference to the ballot state. + _ballotState = _ballotStateOf(_projectId, fundingCycle); + + // Check to see if this funding cycle's ballot hasn't failed. // If so, return a funding cycle based on it. - if (_isApproved(_projectId, fundingCycle)) return _mockFundingCycleBasedOn(fundingCycle, false); + if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) + return _mockFundingCycleBasedOn(fundingCycle, false); // Get the funding cycle of its base funding cycle, which carries the last approved configuration. fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); @@ -152,17 +164,24 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { JBFundingCycle memory _fundingCycle; // If an eligible funding cycle exists... - if (_fundingCycleConfiguration > 0) { + if (_fundingCycleConfiguration != 0) { // Resolve the funding cycle for the eligible configuration. _fundingCycle = _getStructFor(_projectId, _fundingCycleConfiguration); - // Check to see if this funding cycle's ballot is approved. + // Get a reference to the ballot state. + JBBallotState _ballotState = _ballotStateOf(_projectId, _fundingCycle); + + // Check to see if this funding cycle's ballot is approved if it exists. // If so, return it. - if (_isApproved(_projectId, _fundingCycle)) return _fundingCycle; + if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) + return _fundingCycle; // If it hasn't been approved, set the funding cycle configuration to be the configuration of the funding cycle that it's based on, // which carries the last approved configuration. _fundingCycleConfiguration = _fundingCycle.basedOn; + + // Keep a reference to its funding cycle. + _fundingCycle = _getStructFor(_projectId, _fundingCycleConfiguration); } else { // No upcoming funding cycle found that is eligible to become active, // so use the last configuration. @@ -171,17 +190,23 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Get the funding cycle for the latest ID. _fundingCycle = _getStructFor(_projectId, _fundingCycleConfiguration); - // If it's not approved or if it hasn't yet started, get a reference to the funding cycle that the latest is based on, which has the latest approved configuration. - if (!_isApproved(_projectId, _fundingCycle) || block.timestamp < _fundingCycle.start) + // Get a reference to the ballot state. + JBBallotState _ballotState = _ballotStateOf(_projectId, _fundingCycle); + + // While the cycle has a ballot that isn't approved or if it hasn't yet started, get a reference to the funding cycle that the latest is based on, which has the latest approved configuration. + while ( + (_ballotState != JBBallotState.Approved && _ballotState != JBBallotState.Empty) || + block.timestamp < _fundingCycle.start + ) { _fundingCycleConfiguration = _fundingCycle.basedOn; + _fundingCycle = _getStructFor(_projectId, _fundingCycleConfiguration); + _ballotState = _ballotStateOf(_projectId, _fundingCycle); + } } // If there is not funding cycle to base the current one on, there can't be a current one. if (_fundingCycleConfiguration == 0) return _getStructFor(0, 0); - // The funding cycle to base a current one on. - _fundingCycle = _getStructFor(_projectId, _fundingCycleConfiguration); - // If the base has no duration, it's still the current one. if (_fundingCycle.duration == 0) return _fundingCycle; @@ -337,11 +362,22 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Get a reference to the funding cycle. JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _currentConfiguration); - // If the funding cycle hasn't started but is currently approved OR or it has started but wasn't approved, set the ID to be the funding cycle it's based on, + // Get a reference to the ballot state. + JBBallotState _ballotState = _ballotStateOf(_projectId, _baseFundingCycle); + + // If the funding cycle hasn't started but is currently approved OR or it has started but wasn't approved if a ballot exists, set the ID to be the funding cycle it's based on, // which carries the latest approved configuration. + // if ( + // (block.timestamp < _baseFundingCycle.start && _ballotState == JBBallotState.Approved) || + // (block.timestamp > _baseFundingCycle.start && + // _ballotState != JBBallotState.Approved && + // _ballotState != JBBallotState.Empty) + // ) _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); if ( - (block.timestamp < _baseFundingCycle.start && _isApproved(_projectId, _baseFundingCycle)) || - (block.timestamp > _baseFundingCycle.start && !_isApproved(_projectId, _baseFundingCycle)) + (block.timestamp >= _baseFundingCycle.start && + _ballotState != JBBallotState.Approved && + _ballotState != JBBallotState.Empty) || + (block.timestamp < _baseFundingCycle.start && _ballotState != JBBallotState.Approved) ) _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); // The configuration can't be the same as the base configuration. @@ -472,9 +508,20 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Get the necessary properties for the base funding cycle. JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _fundingCycle.basedOn); + // Find the base cycle that is not still queued. + while (_baseFundingCycle.start >= block.timestamp) { + // Set the configuration. + configuration = _baseFundingCycle.configuration; + // Set the new funding cycle. + _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); + } + + // Get the funding cycle for the configuration. + _fundingCycle = _getStructFor(_projectId, configuration); + // If the latest configuration doesn't start until after another base cycle, return 0. if ( - _baseFundingCycle.duration > 0 && + _baseFundingCycle.duration != 0 && block.timestamp < _fundingCycle.start - _baseFundingCycle.duration ) return 0; } @@ -644,18 +691,18 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { /// @notice Checks to see if the provided funding cycle is approved according to the correct ballot. /// @param _projectId The ID of the project to which the funding cycle belongs. /// @param _fundingCycle The funding cycle to get an approval flag for. - /// @return The approval flag. - function _isApproved( + /// @return The ballot state of the project. + function _ballotStateOf( uint256 _projectId, JBFundingCycle memory _fundingCycle - ) private view returns (bool) { + ) private view returns (JBBallotState) { return _ballotStateOf( _projectId, _fundingCycle.configuration, _fundingCycle.start, _fundingCycle.basedOn - ) == JBBallotState.Approved; + ); } /// @notice A project's latest funding cycle configuration approval status. @@ -670,8 +717,8 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { uint256 _start, uint256 _ballotFundingCycleConfiguration ) private view returns (JBBallotState) { - // If there is no ballot funding cycle, implicitly approve. - if (_ballotFundingCycleConfiguration == 0) return JBBallotState.Approved; + // If there is no ballot funding cycle, the ballot is empty. + if (_ballotFundingCycleConfiguration == 0) return JBBallotState.Empty; // Get the ballot funding cycle. JBFundingCycle memory _ballotFundingCycle = _getStructFor( @@ -679,9 +726,8 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { _ballotFundingCycleConfiguration ); - // If there is no ballot, the ID is auto approved. - if (_ballotFundingCycle.ballot == IJBFundingCycleBallot(address(0))) - return JBBallotState.Approved; + // If there is no ballot, it's considered empty. + if (_ballotFundingCycle.ballot == IJBFundingCycleBallot(address(0))) return JBBallotState.Empty; // Return the ballot's state return _ballotFundingCycle.ballot.stateOf(_projectId, _configuration, _start); diff --git a/contracts/JBReconfigurationBufferBallot.sol b/contracts/JBReconfigurationBufferBallot.sol index 1bae9d53a..4266a60e9 100644 --- a/contracts/JBReconfigurationBufferBallot.sol +++ b/contracts/JBReconfigurationBufferBallot.sol @@ -36,8 +36,8 @@ contract JBReconfigurationBufferBallot is ERC165, IJBFundingCycleBallot { if (_configured > _start) return JBBallotState.Failed; unchecked { - // If the ballot hasn't yet started, it's state is active. - if (block.timestamp < _start - duration) return JBBallotState.Active; + // If the ballot hasn't yet started, it's state is Standby. + if (block.timestamp < _start - duration) return JBBallotState.Standby; // If there was sufficient time between configuration and the start of the cycle, it is approved. Otherwise, it is failed. return (_start - _configured < duration) ? JBBallotState.Failed : JBBallotState.Approved; } diff --git a/contracts/enums/JBBallotState.sol b/contracts/enums/JBBallotState.sol index b74c30f43..c3b283586 100644 --- a/contracts/enums/JBBallotState.sol +++ b/contracts/enums/JBBallotState.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; enum JBBallotState { + Empty, + Standby, Active, Approved, Failed diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 0ac06dbb4..7ac62c647 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -44,9 +44,11 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { const DEFAULT_FUNDING_CYCLE_DATA = createFundingCycleData(); const ballotStatus = { - ACTIVE: 0, - APPROVED: 1, - FAILED: 2, + EMPTY: 0, + STANDBY: 1, + ACTIVE: 2, + APPROVED: 3, + FAILED: 4, }; async function setup() { @@ -86,14 +88,14 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { it('Should have no current or queued funding cycle before configuring', async function () { const { jbFundingCycleStore } = await setup(); - // Ballot status should be approved since there is no ballot. - expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(1); + // Ballot status should be empty since there is no ballot. + expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(ballotStatus.EMPTY); const [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(EMPTY_FUNDING_CYCLE); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( EMPTY_FUNDING_CYCLE, ); @@ -153,8 +155,8 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { metadata: fundingCycleMetadata, }; - // Ballot status should be approved since there is no ballot. - expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(1); + // Ballot status should be empty since there is no ballot. + expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(ballotStatus.EMPTY); expect( cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, configurationTimestamp)), @@ -164,7 +166,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedCurrentFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( expectedCurrentFundingCycle, ); @@ -180,7 +182,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf(PROJECT_ID); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedCurrentFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( expectedCurrentFundingCycle, ); @@ -199,7 +201,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf(PROJECT_ID); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedCurrentFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); // What was the queued cycle should now be the current cycle. expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ ...expectedCurrentFundingCycle, @@ -220,7 +222,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf(PROJECT_ID); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedCurrentFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); // What was the queued cycle should now be the current cycle. expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ ...expectedCurrentFundingCycle, @@ -299,8 +301,8 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { metadata: fundingCycleMetadata, }; - // Ballot status should be approved since there is no ballot. - expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(1); + // Ballot status should be empty since there is no ballot. + expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(ballotStatus.EMPTY); expect( cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, configurationTimestamp)), @@ -309,7 +311,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedUpcomingFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( EMPTY_FUNDING_CYCLE, ); @@ -404,7 +406,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( expectedFirstFundingCycle, ); @@ -496,7 +498,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( expectedFirstFundingCycle, ); @@ -602,7 +604,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( expectedFirstFundingCycle, ); @@ -694,7 +696,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ ...expectedFirstFundingCycle, number: expectedFirstFundingCycle.number.add(cycleDiff.sub(1)), @@ -1244,7 +1246,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf(PROJECT_ID); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedFailedFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.APPROVED); // Current should be the now-approved fc. expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql( @@ -1749,7 +1751,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { .returns(ballotStatus.FAILED); // Ballot status should be failed. - expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(2); + expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(ballotStatus.FAILED); await expect(secondConfigureForTx) .to.emit(jbFundingCycleStore, `Init`) @@ -1839,7 +1841,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { .returns(ballotStatus.FAILED); // Ballot status should be failed. - expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(2); + expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(ballotStatus.FAILED); await expect(secondConfigureForTx) .to.emit(jbFundingCycleStore, `Init`) @@ -1855,7 +1857,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { }); it("Should hold off on using a reconfigured funding cycle if the current cycle's ballot duration doesn't end until after the current cycle is over", async function () { - const { controller, mockJbDirectory, mockBallot, jbFundingCycleStore, addrs } = await setup(); + const { controller, mockJbDirectory, mockBallot, jbFundingCycleStore } = await setup(); await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); const firstFundingCycleData = createFundingCycleData({ ballot: mockBallot.address }); @@ -1938,6 +1940,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { number: expectedFirstFundingCycle.number.add(1), // next number start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), // starts at the end of the first cycle }); + // The reconfiguration should not have taken effect. expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ ...expectedFirstFundingCycle, @@ -1965,7 +1968,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { .returns(ballotStatus.APPROVED); // Ballot status should be approved. - expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(1); + expect(await jbFundingCycleStore.currentBallotStateOf(PROJECT_ID)).to.eql(ballotStatus.APPROVED); const expectedReconfiguredFundingCycle = { number: ethers.BigNumber.from(3), @@ -2530,7 +2533,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { PROJECT_ID, ); expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedThirdFundingCycle); - expect(ballotState).to.deep.eql(1); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ ...expectedFirstFundingCycle, number: expectedFirstFundingCycle.number.add(1), // next number @@ -2541,6 +2544,159 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { ); }); + it('Should configure subsequent cycle during a rolled over funding cycle without overriding an already-proposed configuration if it has been approved by the ballot', async function () { + const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); + await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + const firstFundingCycleData = createFundingCycleData({ + ballot: mockBallot.address, + }); + + // Configure first funding cycle + const firstConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + firstFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_1, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the first configuration was made during. + const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + const expectedFirstFundingCycle = { + number: ethers.BigNumber.from(1), + configuration: firstConfigurationTimestamp, + basedOn: ethers.BigNumber.from(0), + start: firstConfigurationTimestamp, + duration: firstFundingCycleData.duration, + weight: firstFundingCycleData.weight, + discountRate: firstFundingCycleData.discountRate, + ballot: firstFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + }; + + const ballotDuration = 20; + + // Set the ballot to have an arbitrary positive duration. + await mockBallot.mock.duration.withArgs().returns(ballotDuration); + + //fast forward to within the second cycle, which should have rolled over from the first. + //keep more than the ballot duration worth of seconds before the end of the cycle so make all necessary checks before the cycle ends. + await fastForward( + firstConfigureForTx.blockNumber, + firstFundingCycleData.duration.mul(2).sub(ballotDuration + 5), + ); + + const secondFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(1), + discountRate: firstFundingCycleData.discountRate.add(1), + weight: firstFundingCycleData.weight.add(1), + }); + + // Configure second funding cycle + const secondConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + secondFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the second configuration was made during. + const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + const expectedSecondFundingCycle = { + number: ethers.BigNumber.from(3), // third cycle + configuration: secondConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(2)), // starts at the end of the second cycle + duration: secondFundingCycleData.duration, + weight: secondFundingCycleData.weight, + discountRate: secondFundingCycleData.discountRate, + ballot: secondFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + await expect(secondConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + secondConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(2)), + ) + .returns(ballotStatus.APPROVED); + + //fast forward to within the cycle and ballot. + //keep 5 seconds before the end of the cycle so make all necessary checks before the cycle ends. + await fastForward( + firstConfigureForTx.blockNumber, + firstFundingCycleData.duration.mul(2).sub(ballotDuration - 5), + ); + + const thirdFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(2), + discountRate: firstFundingCycleData.discountRate.add(2), + weight: firstFundingCycleData.weight.add(2), + }); + + // Configure third funding cycle + const thirdConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + thirdFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the second configuration was made during. + const thirdConfigurationTimestamp = await getTimestamp(thirdConfigureForTx.blockNumber); + + const expectedThirdFundingCycle = { + number: ethers.BigNumber.from(4), // fourth cycle still + configuration: thirdConfigurationTimestamp, + basedOn: secondConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(2)).add(secondFundingCycleData.duration), // starts at the end of the 3rd cycle + duration: thirdFundingCycleData.duration, + weight: thirdFundingCycleData.weight, + discountRate: thirdFundingCycleData.discountRate, + ballot: thirdFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + await expect(thirdConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(thirdConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ secondConfigurationTimestamp); + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + ).to.eql(expectedSecondFundingCycle); + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, thirdConfigurationTimestamp)), + ).to.eql(expectedThirdFundingCycle); + + let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + PROJECT_ID, + ); + expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedThirdFundingCycle); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), // next number + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), // starts at the end of the first cycle + }); + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql( + expectedSecondFundingCycle, + ); + }); + it("Can't configure if caller is not project's controller", async function () { const { controller, mockJbDirectory, jbFundingCycleStore, addrs } = await setup(); const [nonController] = addrs; From 889916bab0c56f1e1e7fa8322f92e6e32dc5c55c Mon Sep 17 00:00:00 2001 From: mejango Date: Mon, 21 Aug 2023 15:27:31 -0400 Subject: [PATCH 03/22] bug fix --- contracts/JBFundingCycleStore.sol | 7 +++---- test/jb_funding_cycle_store/configure_for.test.js | 9 +++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index fede74bf2..333f48fc3 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -509,16 +509,15 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _fundingCycle.basedOn); // Find the base cycle that is not still queued. - while (_baseFundingCycle.start >= block.timestamp) { + while (_baseFundingCycle.start > block.timestamp) { // Set the configuration. configuration = _baseFundingCycle.configuration; + // Get the funding cycle for the configuration. + _fundingCycle = _getStructFor(_projectId, configuration); // Set the new funding cycle. _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); } - // Get the funding cycle for the configuration. - _fundingCycle = _getStructFor(_projectId, configuration); - // If the latest configuration doesn't start until after another base cycle, return 0. if ( _baseFundingCycle.duration != 0 && diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 7ac62c647..04073e9ca 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -2695,6 +2695,15 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql( expectedSecondFundingCycle, ); + + //fast forward to after the ballot. + await fastForward( + firstConfigureForTx.blockNumber, + firstFundingCycleData.duration.mul(2), + ); + + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql(expectedSecondFundingCycle); + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql(expectedThirdFundingCycle); }); it("Can't configure if caller is not project's controller", async function () { From ca91edee060f98c522df5df0d6558cd5b558e131 Mon Sep 17 00:00:00 2001 From: mejango Date: Mon, 21 Aug 2023 17:45:44 -0400 Subject: [PATCH 04/22] removed comment --- contracts/JBFundingCycleStore.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index 333f48fc3..284e425e0 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -367,12 +367,6 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // If the funding cycle hasn't started but is currently approved OR or it has started but wasn't approved if a ballot exists, set the ID to be the funding cycle it's based on, // which carries the latest approved configuration. - // if ( - // (block.timestamp < _baseFundingCycle.start && _ballotState == JBBallotState.Approved) || - // (block.timestamp > _baseFundingCycle.start && - // _ballotState != JBBallotState.Approved && - // _ballotState != JBBallotState.Empty) - // ) _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); if ( (block.timestamp >= _baseFundingCycle.start && _ballotState != JBBallotState.Approved && From 6fee6bcda6d52dc54c15245847c549653a7d8f7f Mon Sep 17 00:00:00 2001 From: mejango Date: Tue, 22 Aug 2023 12:15:24 -0400 Subject: [PATCH 05/22] better ballot states for indicating intent --- contracts/JBFundingCycleStore.sol | 27 +- contracts/JBReconfigurationBufferBallot.sol | 10 +- contracts/enums/JBBallotState.sol | 1 + .../configure_for.test.js | 305 +++++++++++++++++- 4 files changed, 329 insertions(+), 14 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index 284e425e0..db83fe237 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -345,22 +345,25 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { uint256 _weight, uint256 _mustStartAtOrAfter ) private { + // Keep a reference to the project's latest configuration. + uint256 _latestConfiguration = latestConfigurationOf[_projectId]; + // If there's not yet a funding cycle for the project, initialize one. - if (latestConfigurationOf[_projectId] == 0) + if (_latestConfiguration == 0) // Use an empty funding cycle as the base. return _initFor(_projectId, _getStructFor(0, 0), _configuration, _mustStartAtOrAfter, _weight); - // Get the active funding cycle's configuration. - uint256 _currentConfiguration = _eligibleOf(_projectId); + // // Get the active funding cycle's configuration. + // uint256 _currentConfiguration = _eligibleOf(_projectId); - // If an eligible funding cycle does not exist, get a reference to the latest funding cycle configuration for the project. - if (_currentConfiguration == 0) - // Get the latest funding cycle's configuration. - _currentConfiguration = latestConfigurationOf[_projectId]; + // // If an eligible funding cycle does not exist, get a reference to the latest funding cycle configuration for the project. + // if (_currentConfiguration == 0) + // // Get the latest funding cycle's configuration. + // _currentConfiguration = latestConfigurationOf[_projectId]; // Get a reference to the funding cycle. - JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _currentConfiguration); + JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _latestConfiguration); // Get a reference to the ballot state. JBBallotState _ballotState = _ballotStateOf(_projectId, _baseFundingCycle); @@ -371,7 +374,13 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { (block.timestamp >= _baseFundingCycle.start && _ballotState != JBBallotState.Approved && _ballotState != JBBallotState.Empty) || - (block.timestamp < _baseFundingCycle.start && _ballotState != JBBallotState.Approved) + (block.timestamp < _baseFundingCycle.start && + _mustStartAtOrAfter < _baseFundingCycle.start + _baseFundingCycle.duration && + _ballotState != JBBallotState.Approved && + _ballotState != JBBallotState.ApprovalExpected) || + (block.timestamp < _baseFundingCycle.start && + _mustStartAtOrAfter >= _baseFundingCycle.start + _baseFundingCycle.duration && + _ballotState == JBBallotState.Failed) ) _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); // The configuration can't be the same as the base configuration. diff --git a/contracts/JBReconfigurationBufferBallot.sol b/contracts/JBReconfigurationBufferBallot.sol index 4266a60e9..2e1c91092 100644 --- a/contracts/JBReconfigurationBufferBallot.sol +++ b/contracts/JBReconfigurationBufferBallot.sol @@ -36,10 +36,14 @@ contract JBReconfigurationBufferBallot is ERC165, IJBFundingCycleBallot { if (_configured > _start) return JBBallotState.Failed; unchecked { - // If the ballot hasn't yet started, it's state is Standby. - if (block.timestamp < _start - duration) return JBBallotState.Standby; // If there was sufficient time between configuration and the start of the cycle, it is approved. Otherwise, it is failed. - return (_start - _configured < duration) ? JBBallotState.Failed : JBBallotState.Approved; + // If the ballot hasn't yet started, it's state is ApprovalExpected. + return + (_start - _configured < duration) + ? JBBallotState.Failed + : (block.timestamp < _start - duration) + ? JBBallotState.ApprovalExpected + : JBBallotState.Approved; } } diff --git a/contracts/enums/JBBallotState.sol b/contracts/enums/JBBallotState.sol index c3b283586..73db02716 100644 --- a/contracts/enums/JBBallotState.sol +++ b/contracts/enums/JBBallotState.sol @@ -5,6 +5,7 @@ enum JBBallotState { Empty, Standby, Active, + ApprovalExpected, Approved, Failed } diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 04073e9ca..47c9f2776 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -47,8 +47,9 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { EMPTY: 0, STANDBY: 1, ACTIVE: 2, - APPROVED: 3, - FAILED: 4, + APPROVAL_EXPECTED: 3, + APPROVED: 4, + FAILED: 5, }; async function setup() { @@ -523,6 +524,306 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { ); }); + it('Should configure multiple subsequent cycles that start in the future', async function () { + const { controller, mockJbDirectory, jbFundingCycleStore } = await setup(); + await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + const firstFundingCycleData = createFundingCycleData(); + + // Configure first funding cycle + const firstConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + firstFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_1, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the first configuration was made during. + const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + const expectedFirstFundingCycle = { + number: ethers.BigNumber.from(1), + configuration: firstConfigurationTimestamp, + basedOn: ethers.BigNumber.from(0), + start: firstConfigurationTimestamp, + duration: firstFundingCycleData.duration, + weight: firstFundingCycleData.weight, + discountRate: firstFundingCycleData.discountRate, + ballot: firstFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + }; + + const secondFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(1), + discountRate: firstFundingCycleData.discountRate.add(1), + weight: firstFundingCycleData.weight.add(1), + }); + + const thirdFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(2), + discountRate: firstFundingCycleData.discountRate.add(2), + weight: firstFundingCycleData.weight.add(2), + }); + + + const secondFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ); + + const thirdFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ).add(secondFundingCycleData.duration); + + // Configure second funding cycle + const secondConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + secondFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + secondFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the second configuration was made during. + const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + await expect(secondConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + const expectedSecondFundingCycle = { + number: ethers.BigNumber.from(2), // second cycle + configuration: secondConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the second cycle + duration: secondFundingCycleData.duration, + weight: secondFundingCycleData.weight, + discountRate: secondFundingCycleData.discountRate, + ballot: secondFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + ).to.eql(expectedSecondFundingCycle); + + let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + PROJECT_ID, + ); + expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); + + // Queued shows the second. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedSecondFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // Configure third funding cycle + const thirdConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + thirdFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + thirdFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the third configuration was made during. + const thirdConfigurationTimestamp = await getTimestamp(thirdConfigureForTx.blockNumber); + + await expect(thirdConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(thirdConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ secondConfigurationTimestamp); + + const expectedThirdFundingCycle = { + number: ethers.BigNumber.from(3), // third cycle + configuration: thirdConfigurationTimestamp, + basedOn: secondConfigurationTimestamp, // based on the second cycle + start: secondConfigurationTimestamp.add(secondFundingCycleData.duration), // starts at the end of the second cycle + duration: thirdFundingCycleData.duration, + weight: thirdFundingCycleData.weight, + discountRate: thirdFundingCycleData.discountRate, + ballot: thirdFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + // Current shows the first still. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql(expectedFirstFundingCycle); + + // Queued shows the second still. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedSecondFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + //fast forward to after the cycle. + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration); + + // Current shows the second still. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedSecondFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // Queued shows the second still. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedThirdFundingCycle, + number: expectedFirstFundingCycle.number.add(2), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration).add(expectedSecondFundingCycle.duration), + }); + }); + + it('Should configure multiple subsequent cycles that start in the future, while overriding if timestamps dont match up', async function () { + const { controller, mockJbDirectory, jbFundingCycleStore } = await setup(); + await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + const firstFundingCycleData = createFundingCycleData(); + + // Configure first funding cycle + const firstConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + firstFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_1, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the first configuration was made during. + const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + const expectedFirstFundingCycle = { + number: ethers.BigNumber.from(1), + configuration: firstConfigurationTimestamp, + basedOn: ethers.BigNumber.from(0), + start: firstConfigurationTimestamp, + duration: firstFundingCycleData.duration, + weight: firstFundingCycleData.weight, + discountRate: firstFundingCycleData.discountRate, + ballot: firstFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + }; + + const secondFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(1), + discountRate: firstFundingCycleData.discountRate.add(1), + weight: firstFundingCycleData.weight.add(1), + }); + + const thirdFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(2), + discountRate: firstFundingCycleData.discountRate.add(2), + weight: firstFundingCycleData.weight.add(2), + }); + + + const secondFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ); + + const thirdFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration.mul(2), + ); + + // Configure second funding cycle + const secondConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + secondFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + secondFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the second configuration was made during. + const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + await expect(secondConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + const expectedSecondFundingCycle = { + number: ethers.BigNumber.from(2), // second cycle + configuration: secondConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the second cycle + duration: secondFundingCycleData.duration, + weight: secondFundingCycleData.weight, + discountRate: secondFundingCycleData.discountRate, + ballot: secondFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + ).to.eql(expectedSecondFundingCycle); + + let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + PROJECT_ID, + ); + expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); + expect(ballotState).to.deep.eql(ballotStatus.EMPTY); + + // Queued shows the second over fc. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedSecondFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // Configure third funding cycle + const thirdConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + thirdFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + thirdFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the third configuration was made during. + const thirdConfigurationTimestamp = await getTimestamp(thirdConfigureForTx.blockNumber); + + await expect(thirdConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(thirdConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + const expectedThirdFundingCycle = { + number: ethers.BigNumber.from(3), // third cycle + configuration: thirdConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the second cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(2)), // starts at the end of the third cycle + duration: thirdFundingCycleData.duration, + weight: thirdFundingCycleData.weight, + discountRate: thirdFundingCycleData.discountRate, + ballot: thirdFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + // Queued shows the rolled over fc. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + //fast forward to after the cycle. + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration); + + // Queued shows the rolled over fc. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedThirdFundingCycle, + number: expectedFirstFundingCycle.number.add(2), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(2)), + }); + }); + it('Should configure subsequent cycle that starts in the future if current cycle has no duration', async function () { const { controller, mockJbDirectory, jbFundingCycleStore } = await setup(); await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); From d127fb305c8bd416eb946d26a1d8e12aa231049c Mon Sep 17 00:00:00 2001 From: mejango Date: Tue, 22 Aug 2023 13:18:19 -0400 Subject: [PATCH 06/22] haven't yet solved failed fc dependency --- contracts/JBFundingCycleStore.sol | 21 ++- .../configure_for.test.js | 157 ++++++++++++++++++ 2 files changed, 172 insertions(+), 6 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index db83fe237..dc1b1c3a0 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -111,8 +111,11 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { _ballotState = _ballotStateOf(_projectId, fundingCycle); // If the ballot hasn't failed, return it. - if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) - return fundingCycle; + if ( + _ballotState == JBBallotState.Approved || + _ballotState == JBBallotState.ApprovalExpected || + _ballotState == JBBallotState.Empty + ) return fundingCycle; // Resolve the funding cycle for the latest configured funding cycle. fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); @@ -134,8 +137,11 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Check to see if this funding cycle's ballot hasn't failed. // If so, return a funding cycle based on it. - if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) - return _mockFundingCycleBasedOn(fundingCycle, false); + if ( + _ballotState == JBBallotState.Approved || + _ballotState == JBBallotState.ApprovalExpected || + _ballotState == JBBallotState.Empty + ) return _mockFundingCycleBasedOn(fundingCycle, false); // Get the funding cycle of its base funding cycle, which carries the last approved configuration. fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); @@ -173,8 +179,11 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Check to see if this funding cycle's ballot is approved if it exists. // If so, return it. - if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) - return _fundingCycle; + if ( + _ballotState == JBBallotState.Approved || + _ballotState == JBBallotState.ApprovalExpected || + _ballotState == JBBallotState.Empty + ) return _fundingCycle; // If it hasn't been approved, set the funding cycle configuration to be the configuration of the funding cycle that it's based on, // which carries the last approved configuration. diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 47c9f2776..898b2354d 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -679,6 +679,163 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { }); }); + it('Should configure multiple subsequent cycles that start in the future with standby ballots that turn failed', async function () { + const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); + await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + const firstFundingCycleData = createFundingCycleData({ + ballot: mockBallot.address + }); + + // Set the ballot to have a short duration. + await mockBallot.mock.duration.withArgs().returns(0); + + // Configure first funding cycle + const firstConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + firstFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_1, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the first configuration was made during. + const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + const expectedFirstFundingCycle = { + number: ethers.BigNumber.from(1), + configuration: firstConfigurationTimestamp, + basedOn: ethers.BigNumber.from(0), + start: firstConfigurationTimestamp, + duration: firstFundingCycleData.duration, + weight: firstFundingCycleData.weight, + discountRate: firstFundingCycleData.discountRate, + ballot: firstFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + }; + + const secondFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(1), + discountRate: firstFundingCycleData.discountRate.add(1), + weight: firstFundingCycleData.weight.add(1), + }); + + const thirdFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(2), + discountRate: firstFundingCycleData.discountRate.add(2), + weight: firstFundingCycleData.weight.add(2), + }); + + + const secondFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ); + + const thirdFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ).add(secondFundingCycleData.duration); + + // Configure second funding cycle + const secondConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + secondFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + secondFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the second configuration was made during. + const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + await expect(secondConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + secondConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.STANDBY); + + const expectedSecondFundingCycle = { + number: ethers.BigNumber.from(2), // second cycle + configuration: secondConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the second cycle + duration: secondFundingCycleData.duration, + weight: secondFundingCycleData.weight, + discountRate: secondFundingCycleData.discountRate, + ballot: secondFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + ).to.eql(expectedSecondFundingCycle); + + let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + PROJECT_ID, + ); + expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); + expect(ballotState).to.deep.eql(ballotStatus.STANDBY); + + // Queued shows the first. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // Configure third funding cycle + const thirdConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + thirdFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + thirdFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the third configuration was made during. + const thirdConfigurationTimestamp = await getTimestamp(thirdConfigureForTx.blockNumber); + + const expectedThirdFundingCycle = { + number: ethers.BigNumber.from(3), // third cycle + configuration: thirdConfigurationTimestamp, + basedOn: secondConfigurationTimestamp, // based on the second cycle + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration).add(expectedSecondFundingCycle.duration), // starts at the end of the second cycle + duration: thirdFundingCycleData.duration, + weight: thirdFundingCycleData.weight, + discountRate: thirdFundingCycleData.discountRate, + ballot: thirdFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + secondConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.FAILED); + + // fast forward to after the cycle. + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration); + + // Queued shows the first again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(2), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration).add(expectedFirstFundingCycle.duration), + }); + }); + it('Should configure multiple subsequent cycles that start in the future, while overriding if timestamps dont match up', async function () { const { controller, mockJbDirectory, jbFundingCycleStore } = await setup(); await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); From 1944e144e479e22899e54b038de66e3279061c55 Mon Sep 17 00:00:00 2001 From: mejango Date: Tue, 22 Aug 2023 13:36:29 -0400 Subject: [PATCH 07/22] loop test fails --- contracts/JBFundingCycleStore.sol | 13 +- .../configure_for.test.js | 228 +++++++++++++++++- 2 files changed, 237 insertions(+), 4 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index dc1b1c3a0..db3e05b9e 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -107,6 +107,9 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { if (_standbyFundingCycleConfiguration != 0) { fundingCycle = _getStructFor(_projectId, _standbyFundingCycleConfiguration); + // Get a reference to the base cycle + JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); + // Get a reference to the ballot state. _ballotState = _ballotStateOf(_projectId, fundingCycle); @@ -115,10 +118,14 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { _ballotState == JBBallotState.Approved || _ballotState == JBBallotState.ApprovalExpected || _ballotState == JBBallotState.Empty - ) return fundingCycle; - + ) { + JBBallotState _baseBallotState = _ballotStateOf(_projectId, _baseFundingCycle); + if (_baseBallotState == JBBallotState.Approved || _baseBallotState == JBBallotState.Empty) + return fundingCycle; + else fundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); + } // Resolve the funding cycle for the latest configured funding cycle. - fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); + else fundingCycle = _baseFundingCycle; } else { // Resolve the funding cycle for the latest configured funding cycle. fundingCycle = _getStructFor(_projectId, latestConfigurationOf[_projectId]); diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 898b2354d..20d48d83b 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -828,11 +828,237 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { // fast forward to after the cycle. await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration); + // Current shows the first. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // Queued shows the first again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(2), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(2)), + }); + + // fast forward another cycle + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration.mul(2)); + + // Current shows the first rolled over. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(2), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(2)), + }); + + // Queued shows the first rolled over again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(3), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), + }); + }); + + it('Should configure multiple subsequent cycles that start in the future with standby ballots that turn failed with deeper nesting', async function () { + const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); + await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + const firstFundingCycleData = createFundingCycleData({ + ballot: mockBallot.address + }); + + // Set the ballot to have a short duration. + await mockBallot.mock.duration.withArgs().returns(0); + + // Configure first funding cycle + const firstConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + firstFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_1, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the first configuration was made during. + const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + const expectedFirstFundingCycle = { + number: ethers.BigNumber.from(1), + configuration: firstConfigurationTimestamp, + basedOn: ethers.BigNumber.from(0), + start: firstConfigurationTimestamp, + duration: firstFundingCycleData.duration, + weight: firstFundingCycleData.weight, + discountRate: firstFundingCycleData.discountRate, + ballot: firstFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + }; + + const secondFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(1), + discountRate: firstFundingCycleData.discountRate.add(1), + weight: firstFundingCycleData.weight.add(1), + }); + + const thirdFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(2), + discountRate: firstFundingCycleData.discountRate.add(2), + weight: firstFundingCycleData.weight.add(2), + }); + + const fourthFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(3), + discountRate: firstFundingCycleData.discountRate.add(3), + weight: firstFundingCycleData.weight.add(3), + }); + + const secondFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ); + + const thirdFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ).add(secondFundingCycleData.duration); + + const fourthFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ).add(secondFundingCycleData.duration).add(thirdFundingCycleData.duration); + + // Configure second funding cycle + const secondConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + secondFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + secondFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the second configuration was made during. + const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + await expect(secondConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + secondConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.STANDBY); + + const expectedSecondFundingCycle = { + number: ethers.BigNumber.from(2), // second cycle + configuration: secondConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the second cycle + duration: secondFundingCycleData.duration, + weight: secondFundingCycleData.weight, + discountRate: secondFundingCycleData.discountRate, + ballot: secondFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + ).to.eql(expectedSecondFundingCycle); + + let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + PROJECT_ID, + ); + expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); + expect(ballotState).to.deep.eql(ballotStatus.STANDBY); + + // Queued shows the first. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // Configure third funding cycle + const thirdConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + thirdFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + thirdFundingCycleMustStartOnOrAfter, + ); + + // Configure fourth funding cycle + const fourthConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + fourthFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + fourthFundingCycleMustStartOnOrAfter, + ); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + secondConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.FAILED); + + // fast forward to after the cycle. + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration); + + // Current shows the first. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + // Queued shows the first again. expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ ...expectedFirstFundingCycle, number: expectedFirstFundingCycle.number.add(2), - start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration).add(expectedFirstFundingCycle.duration), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(2)), + }); + + // fast forward another cycle + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration.mul(2)); + + // Current shows the first rolled over. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(2), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(2)), + }); + + // Queued shows the first rolled over again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(3), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), + }); + + // fast forward yet another cycle + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration.mul(3)); + + // Current shows the first rolled over twice. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(3), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), + }); + + // Queued shows the first rolled over again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(4), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(4)), }); }); From b2de42565c3bb4eddf1b800d1a2aaf245b8c305f Mon Sep 17 00:00:00 2001 From: mejango Date: Tue, 22 Aug 2023 19:59:00 -0400 Subject: [PATCH 08/22] cleaner --- contracts/JBFundingCycleStore.sol | 45 +-- .../configure_for.test.js | 289 ++++++++++++++++-- 2 files changed, 283 insertions(+), 51 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index db3e05b9e..6064089d0 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -107,9 +107,6 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { if (_standbyFundingCycleConfiguration != 0) { fundingCycle = _getStructFor(_projectId, _standbyFundingCycleConfiguration); - // Get a reference to the base cycle - JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); - // Get a reference to the ballot state. _ballotState = _ballotStateOf(_projectId, fundingCycle); @@ -118,22 +115,19 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { _ballotState == JBBallotState.Approved || _ballotState == JBBallotState.ApprovalExpected || _ballotState == JBBallotState.Empty - ) { - JBBallotState _baseBallotState = _ballotStateOf(_projectId, _baseFundingCycle); - if (_baseBallotState == JBBallotState.Approved || _baseBallotState == JBBallotState.Empty) - return fundingCycle; - else fundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); - } + ) return fundingCycle; + // Resolve the funding cycle for the latest configured funding cycle. - else fundingCycle = _baseFundingCycle; + fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); } else { // Resolve the funding cycle for the latest configured funding cycle. fundingCycle = _getStructFor(_projectId, latestConfigurationOf[_projectId]); // If the latest funding cycle starts in the future, it must start in the distant future // since its not in standby. In this case base the queued cycles on the base cycle. - if (fundingCycle.start > block.timestamp) + while (fundingCycle.start > block.timestamp) { fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); + } } // There's no queued if the current has a duration of 0. @@ -144,11 +138,8 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Check to see if this funding cycle's ballot hasn't failed. // If so, return a funding cycle based on it. - if ( - _ballotState == JBBallotState.Approved || - _ballotState == JBBallotState.ApprovalExpected || - _ballotState == JBBallotState.Empty - ) return _mockFundingCycleBasedOn(fundingCycle, false); + if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) + return _mockFundingCycleBasedOn(fundingCycle, false); // Get the funding cycle of its base funding cycle, which carries the last approved configuration. fundingCycle = _getStructFor(_projectId, fundingCycle.basedOn); @@ -186,11 +177,8 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Check to see if this funding cycle's ballot is approved if it exists. // If so, return it. - if ( - _ballotState == JBBallotState.Approved || - _ballotState == JBBallotState.ApprovalExpected || - _ballotState == JBBallotState.Empty - ) return _fundingCycle; + if (_ballotState == JBBallotState.Approved || _ballotState == JBBallotState.Empty) + return _fundingCycle; // If it hasn't been approved, set the funding cycle configuration to be the configuration of the funding cycle that it's based on, // which carries the last approved configuration. @@ -220,9 +208,6 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { } } - // If there is not funding cycle to base the current one on, there can't be a current one. - if (_fundingCycleConfiguration == 0) return _getStructFor(0, 0); - // If the base has no duration, it's still the current one. if (_fundingCycle.duration == 0) return _fundingCycle; @@ -370,14 +355,6 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { return _initFor(_projectId, _getStructFor(0, 0), _configuration, _mustStartAtOrAfter, _weight); - // // Get the active funding cycle's configuration. - // uint256 _currentConfiguration = _eligibleOf(_projectId); - - // // If an eligible funding cycle does not exist, get a reference to the latest funding cycle configuration for the project. - // if (_currentConfiguration == 0) - // // Get the latest funding cycle's configuration. - // _currentConfiguration = latestConfigurationOf[_projectId]; - // Get a reference to the funding cycle. JBFundingCycle memory _baseFundingCycle = _getStructFor(_projectId, _latestConfiguration); @@ -396,7 +373,9 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { _ballotState != JBBallotState.ApprovalExpected) || (block.timestamp < _baseFundingCycle.start && _mustStartAtOrAfter >= _baseFundingCycle.start + _baseFundingCycle.duration && - _ballotState == JBBallotState.Failed) + _ballotState != JBBallotState.Approved && + _ballotState != JBBallotState.ApprovalExpected && + _ballotState != JBBallotState.Empty) ) _baseFundingCycle = _getStructFor(_projectId, _baseFundingCycle.basedOn); // The configuration can't be the same as the base configuration. diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 20d48d83b..0db537c61 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -816,12 +816,11 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { metadata: RANDOM_FUNDING_CYCLE_METADATA_2, }; - // Mock the ballot on the failed funding cycle as approved. await mockBallot.mock.stateOf .withArgs( PROJECT_ID, - secondConfigurationTimestamp, - firstConfigurationTimestamp.add(firstFundingCycleData.duration), + thirdConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(3)) ) .returns(ballotStatus.FAILED); @@ -860,6 +859,187 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { }); }); + it('Should configure multiple subsequent cycles that start in the future with standby ballots and approved other ballots', async function () { + const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); + await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + const firstFundingCycleData = createFundingCycleData({ + ballot: mockBallot.address + }); + + // Set the ballot to have a short duration. + await mockBallot.mock.duration.withArgs().returns(0); + + // Configure first funding cycle + const firstConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + firstFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_1, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the first configuration was made during. + const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + const expectedFirstFundingCycle = { + number: ethers.BigNumber.from(1), + configuration: firstConfigurationTimestamp, + basedOn: ethers.BigNumber.from(0), + start: firstConfigurationTimestamp, + duration: firstFundingCycleData.duration, + weight: firstFundingCycleData.weight, + discountRate: firstFundingCycleData.discountRate, + ballot: firstFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + }; + + const secondFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(1), + weight: firstFundingCycleData.weight.add(1), + }); + + const thirdFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(2), + weight: firstFundingCycleData.weight.add(2), + }); + + const secondFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ); + + const thirdFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( + firstFundingCycleData.duration, + ).add(secondFundingCycleData.duration); + + // Configure second funding cycle + const secondConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + secondFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + secondFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the second configuration was made during. + const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + await expect(secondConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + secondConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.STANDBY); + + const expectedSecondFundingCycle = { + number: ethers.BigNumber.from(2), // second cycle + configuration: secondConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the second cycle + duration: secondFundingCycleData.duration, + weight: secondFundingCycleData.weight, + discountRate: secondFundingCycleData.discountRate, + ballot: secondFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + ).to.eql(expectedSecondFundingCycle); + + let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + PROJECT_ID, + ); + expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); + expect(ballotState).to.deep.eql(ballotStatus.STANDBY); + + // Queued shows the first. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // Configure third funding cycle + const thirdConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + thirdFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + thirdFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the third configuration was made during. + const thirdConfigurationTimestamp = await getTimestamp(thirdConfigureForTx.blockNumber); + + await expect(thirdConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(thirdConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + const expectedThirdFundingCycle = { + number: ethers.BigNumber.from(3), // third cycle + configuration: thirdConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration).add(expectedSecondFundingCycle.duration), // starts at the end of the second cycle + duration: thirdFundingCycleData.duration, + weight: thirdFundingCycleData.weight, + discountRate: thirdFundingCycleData.discountRate, + ballot: thirdFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + thirdConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(3)) + ) + .returns(ballotStatus.APPROVED); + + // fast forward to after the cycle. + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration.mul(2)); + + // Current shows the first. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(2), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(2)), + }); + + // Queued shows the first again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedThirdFundingCycle, + number: expectedFirstFundingCycle.number.add(3), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), + }); + + // fast forward another cycle + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration.mul(3)); + + // Current shows the first rolled over. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedThirdFundingCycle, + number: expectedFirstFundingCycle.number.add(3), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), + }); + + // Queued shows the first rolled over again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedThirdFundingCycle, + number: expectedFirstFundingCycle.number.add(4), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3).add(expectedThirdFundingCycle.duration)), + }); + }); + it('Should configure multiple subsequent cycles that start in the future with standby ballots that turn failed with deeper nesting', async function () { const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); @@ -898,19 +1078,16 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { const secondFundingCycleData = createFundingCycleData({ duration: firstFundingCycleData.duration.add(1), - discountRate: firstFundingCycleData.discountRate.add(1), weight: firstFundingCycleData.weight.add(1), }); const thirdFundingCycleData = createFundingCycleData({ duration: firstFundingCycleData.duration.add(2), - discountRate: firstFundingCycleData.discountRate.add(2), weight: firstFundingCycleData.weight.add(2), }); const fourthFundingCycleData = createFundingCycleData({ duration: firstFundingCycleData.duration.add(3), - discountRate: firstFundingCycleData.discountRate.add(3), weight: firstFundingCycleData.weight.add(3), }); @@ -991,6 +1168,22 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { thirdFundingCycleMustStartOnOrAfter, ); + // The timestamp the second configuration was made during. + const thirdConfigurationTimestamp = await getTimestamp(thirdConfigureForTx.blockNumber); + + await expect(thirdConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(thirdConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + thirdConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(3)) + ) + .returns(ballotStatus.APPROVAL_EXPECTED); + // Configure fourth funding cycle const fourthConfigureForTx = await jbFundingCycleStore .connect(controller) @@ -1001,18 +1194,34 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { fourthFundingCycleMustStartOnOrAfter, ); - // Mock the ballot on the failed funding cycle as approved. - await mockBallot.mock.stateOf - .withArgs( - PROJECT_ID, - secondConfigurationTimestamp, - firstConfigurationTimestamp.add(firstFundingCycleData.duration), - ) - .returns(ballotStatus.FAILED); + // The timestamp the second configuration was made during. + const fourthConfigurationTimestamp = await getTimestamp(fourthConfigureForTx.blockNumber); - // fast forward to after the cycle. + await expect(fourthConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(fourthConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ thirdConfigurationTimestamp); + + // Queued shows the first. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFirstFundingCycle, + number: expectedFirstFundingCycle.number.add(1), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration), + }); + + // fast forward to after the first cycle. await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration); + const expectedThirdFundingCycle = { + configuration: thirdConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(3)), // starts at the end of the second cycle + duration: thirdFundingCycleData.duration, + weight: thirdFundingCycleData.weight, + discountRate: thirdFundingCycleData.discountRate, + ballot: thirdFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + // Current shows the first. expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ ...expectedFirstFundingCycle, @@ -1039,7 +1248,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { // Queued shows the first rolled over again. expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ - ...expectedFirstFundingCycle, + ...expectedThirdFundingCycle, number: expectedFirstFundingCycle.number.add(3), start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), }); @@ -1047,6 +1256,17 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { // fast forward yet another cycle await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration.mul(3)); + const expectedFourthFundingCycle = { + configuration: fourthConfigurationTimestamp, + basedOn: thirdConfigurationTimestamp, // based on the third cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(3)).add(thirdFundingCycleData.duration), // starts at the end of the second cycle + duration: fourthFundingCycleData.duration, + weight: fourthFundingCycleData.weight, + discountRate: fourthFundingCycleData.discountRate, + ballot: fourthFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + // Current shows the first rolled over twice. expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ ...expectedFirstFundingCycle, @@ -1054,11 +1274,44 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), }); + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + thirdConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration.mul(3)) + ) + .returns(ballotStatus.APPROVED); + + // Current shows the third now. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedThirdFundingCycle, + number: expectedFirstFundingCycle.number.add(3), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3)), + }); + // Queued shows the first rolled over again. expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ - ...expectedFirstFundingCycle, + ...expectedFourthFundingCycle, number: expectedFirstFundingCycle.number.add(4), - start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(4)), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3).add(expectedThirdFundingCycle.duration)), + }); + + // fast forward yet another cycle + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration.mul(3).add(expectedThirdFundingCycle.duration)); + + // Current shows the first rolled over twice. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql({ + ...expectedFourthFundingCycle, + number: expectedFirstFundingCycle.number.add(4), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3).add(expectedThirdFundingCycle.duration)), + }); + + // Queued shows the first rolled over again. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql({ + ...expectedFourthFundingCycle, + number: expectedFirstFundingCycle.number.add(5), + start: expectedFirstFundingCycle.start.add(expectedFirstFundingCycle.duration.mul(3).add(expectedThirdFundingCycle.duration).add(expectedFourthFundingCycle.duration)), }); }); From 270bd8f7c464193d5c8aaa591375d088b0985e67 Mon Sep 17 00:00:00 2001 From: mejango Date: Tue, 22 Aug 2023 20:01:20 -0400 Subject: [PATCH 09/22] docs --- contracts/JBFundingCycleStore.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index 6064089d0..fa565d845 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -361,7 +361,7 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Get a reference to the ballot state. JBBallotState _ballotState = _ballotStateOf(_projectId, _baseFundingCycle); - // If the funding cycle hasn't started but is currently approved OR or it has started but wasn't approved if a ballot exists, set the ID to be the funding cycle it's based on, + // If the funding cycle has started but wasn't approved if a ballot exists OR it hasn't started but is currently approved, set the ID to be the funding cycle it's based on, // which carries the latest approved configuration. if ( (block.timestamp >= _baseFundingCycle.start && From 22abeeb639bc50edf187bbe85ea142fcee667d84 Mon Sep 17 00:00:00 2001 From: mejango Date: Sun, 10 Sep 2023 23:41:47 -0400 Subject: [PATCH 10/22] launch projects, launch funding cycles, and reconfigure funding cycles with multiple reconfigurations --- contracts/JBController3_2.sol | 717 ++++++++++++++++++ contracts/interfaces/IJBController3_2.sol | 170 +++++ .../structs/JBFundingCycleConfiguration.sol | 18 + 3 files changed, 905 insertions(+) create mode 100644 contracts/JBController3_2.sol create mode 100644 contracts/interfaces/IJBController3_2.sol create mode 100644 contracts/structs/JBFundingCycleConfiguration.sol diff --git a/contracts/JBController3_2.sol b/contracts/JBController3_2.sol new file mode 100644 index 000000000..5132068e6 --- /dev/null +++ b/contracts/JBController3_2.sol @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {ERC165} from '@openzeppelin/contracts/utils/introspection/ERC165.sol'; +import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import {PRBMath} from '@paulrberg/contracts/math/PRBMath.sol'; +import {JBOperatable} from './abstract/JBOperatable.sol'; +import {JBBallotState} from './enums/JBBallotState.sol'; +import {IJBController3_2} from './interfaces/IJBController3_2.sol'; +import {IJBController} from './interfaces/IJBController.sol'; +import {IJBDirectory} from './interfaces/IJBDirectory.sol'; +import {IJBFundAccessConstraintsStore} from './interfaces/IJBFundAccessConstraintsStore.sol'; +import {IJBFundingCycleStore} from './interfaces/IJBFundingCycleStore.sol'; +import {IJBMigratable} from './interfaces/IJBMigratable.sol'; +import {IJBOperatable} from './interfaces/IJBOperatable.sol'; +import {IJBOperatorStore} from './interfaces/IJBOperatorStore.sol'; +import {IJBPaymentTerminal} from './interfaces/IJBPaymentTerminal.sol'; +import {IJBProjects} from './interfaces/IJBProjects.sol'; +import {IJBSplitAllocator} from './interfaces/IJBSplitAllocator.sol'; +import {IJBSplitsStore} from './interfaces/IJBSplitsStore.sol'; +import {IJBTokenStore} from './interfaces/IJBTokenStore.sol'; +import {JBConstants} from './libraries/JBConstants.sol'; +import {JBFundingCycleMetadataResolver} from './libraries/JBFundingCycleMetadataResolver.sol'; +import {JBOperations} from './libraries/JBOperations.sol'; +import {JBSplitsGroups} from './libraries/JBSplitsGroups.sol'; +import {JBFundingCycle} from './structs/JBFundingCycle.sol'; +import {JBSplitAllocationData} from './structs/JBSplitAllocationData.sol'; +import {JBFundAccessConstraints} from './structs/JBFundAccessConstraints.sol'; +import {JBFundingCycle} from './structs/JBFundingCycle.sol'; +import {JBFundingCycleData} from './structs/JBFundingCycleData.sol'; +import {JBFundingCycleMetadata} from './structs/JBFundingCycleMetadata.sol'; +import {JBGroupedSplits} from './structs/JBGroupedSplits.sol'; +import {JBProjectMetadata} from './structs/JBProjectMetadata.sol'; +import {JBSplit} from './structs/JBSplit.sol'; +import {JBFundingCycleConfiguration} from './structs/JBFundingCycleConfiguration.sol'; + +/// @notice Stitches together funding cycles and project tokens, making sure all activity is accounted for and correct. +/// @dev This Controller has the same functionality as JBController3_0_1, except it is not backwards compatible with the original IJBController view methods. +contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratable { + // A library that parses the packed funding cycle metadata into a more friendly format. + using JBFundingCycleMetadataResolver for JBFundingCycle; + + //*********************************************************************// + // --------------------------- custom errors ------------------------- // + //*********************************************************************// + + error BURN_PAUSED_AND_SENDER_NOT_VALID_TERMINAL_DELEGATE(); + error FUNDING_CYCLE_ALREADY_LAUNCHED(); + error INVALID_BALLOT_REDEMPTION_RATE(); + error INVALID_REDEMPTION_RATE(); + error INVALID_RESERVED_RATE(); + error MIGRATION_NOT_ALLOWED(); + error MINT_NOT_ALLOWED_AND_NOT_TERMINAL_DELEGATE(); + error NO_BURNABLE_TOKENS(); + error NOT_CURRENT_CONTROLLER(); + error ZERO_TOKENS_TO_MINT(); + + //*********************************************************************// + // --------------------- internal stored properties ------------------ // + //*********************************************************************// + + /// @notice Data regarding the distribution limit of a project during a configuration. + /// @dev bits 0-231: The amount of token that a project can distribute per funding cycle. + /// @dev bits 232-255: The currency of amount that a project can distribute. + /// @custom:param _projectId The ID of the project to get the packed distribution limit data of. + /// @custom:param _configuration The configuration during which the packed distribution limit data applies. + /// @custom:param _terminal The terminal from which distributions are being limited. + /// @custom:param _token The token for which distributions are being limited. + mapping(uint256 => mapping(uint256 => mapping(IJBPaymentTerminal => mapping(address => uint256)))) + internal _packedDistributionLimitDataOf; + + /// @notice Data regarding the overflow allowance of a project during a configuration. + /// @dev bits 0-231: The amount of overflow that a project is allowed to tap into on-demand throughout the configuration. + /// @dev bits 232-255: The currency of the amount of overflow that a project is allowed to tap. + /// @custom:param _projectId The ID of the project to get the packed overflow allowance data of. + /// @custom:param _configuration The configuration during which the packed overflow allowance data applies. + /// @custom:param _terminal The terminal managing the overflow. + /// @custom:param _token The token for which overflow is being allowed. + mapping(uint256 => mapping(uint256 => mapping(IJBPaymentTerminal => mapping(address => uint256)))) + internal _packedOverflowAllowanceDataOf; + + //*********************************************************************// + // --------------- public immutable stored properties ---------------- // + //*********************************************************************// + + /// @notice Mints ERC-721's that represent project ownership. + IJBProjects public immutable override projects; + + /// @notice The contract storing all funding cycle configurations. + IJBFundingCycleStore public immutable override fundingCycleStore; + + /// @notice The contract that manages token minting and burning. + IJBTokenStore public immutable override tokenStore; + + /// @notice The contract that stores splits for each project. + IJBSplitsStore public immutable override splitsStore; + + /// @notice A contract that stores fund access constraints for each project. + IJBFundAccessConstraintsStore public immutable override fundAccessConstraintsStore; + + /// @notice The directory of terminals and controllers for projects. + IJBDirectory public immutable override directory; + + //*********************************************************************// + // --------------------- public stored properties -------------------- // + //*********************************************************************// + + /// @notice The current undistributed reserved token balance of. + mapping(uint256 => uint256) public override reservedTokenBalanceOf; + + //*********************************************************************// + // ------------------------- external views -------------------------- // + //*********************************************************************// + + /// @notice A project's funding cycle for the specified configuration along with its metadata. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The funding cycle. + /// @return metadata The funding cycle's metadata. + function getFundingCycleOf( + uint256 _projectId, + uint256 _configuration + ) + external + view + override + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata memory metadata) + { + fundingCycle = fundingCycleStore.get(_projectId, _configuration); + metadata = fundingCycle.expandMetadata(); + } + + /// @notice A project's latest configured funding cycle along with its metadata and the ballot state of the configuration. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The latest configured funding cycle. + /// @return metadata The latest configured funding cycle's metadata. + /// @return ballotState The state of the configuration. + function latestConfiguredFundingCycleOf( + uint256 _projectId + ) + external + view + override + returns ( + JBFundingCycle memory fundingCycle, + JBFundingCycleMetadata memory metadata, + JBBallotState ballotState + ) + { + (fundingCycle, ballotState) = fundingCycleStore.latestConfiguredOf(_projectId); + metadata = fundingCycle.expandMetadata(); + } + + /// @notice A project's current funding cycle along with its metadata. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The current funding cycle. + /// @return metadata The current funding cycle's metadata. + function currentFundingCycleOf( + uint256 _projectId + ) + external + view + override + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata memory metadata) + { + fundingCycle = fundingCycleStore.currentOf(_projectId); + metadata = fundingCycle.expandMetadata(); + } + + /// @notice A project's queued funding cycle along with its metadata. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The queued funding cycle. + /// @return metadata The queued funding cycle's metadata. + function queuedFundingCycleOf( + uint256 _projectId + ) + external + view + override + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata memory metadata) + { + fundingCycle = fundingCycleStore.queuedOf(_projectId); + metadata = fundingCycle.expandMetadata(); + } + + //*********************************************************************// + // -------------------------- public views --------------------------- // + //*********************************************************************// + + /// @notice Gets the current total amount of outstanding tokens for a project. + /// @param _projectId The ID of the project to get total outstanding tokens of. + /// @return The current total amount of outstanding tokens for the project. + function totalOutstandingTokensOf(uint256 _projectId) public view override returns (uint256) { + // Add the reserved tokens to the total supply. + return tokenStore.totalSupplyOf(_projectId) + reservedTokenBalanceOf[_projectId]; + } + + /// @notice Indicates if this contract adheres to the specified interface. + /// @dev See {IERC165-supportsInterface}. + /// @param _interfaceId The ID of the interface to check for adherance to. + /// @return A flag indicating if the provided interface ID is supported. + function supportsInterface( + bytes4 _interfaceId + ) public view virtual override(ERC165, IERC165) returns (bool) { + return + _interfaceId == type(IJBController3_2).interfaceId || + _interfaceId == type(IJBMigratable).interfaceId || + _interfaceId == type(IJBOperatable).interfaceId || + super.supportsInterface(_interfaceId); + } + + //*********************************************************************// + // ---------------------------- constructor -------------------------- // + //*********************************************************************// + + /// @param _operatorStore A contract storing operator assignments. + /// @param _projects A contract which mints ERC-721's that represent project ownership and transfers. + /// @param _directory A contract storing directories of terminals and controllers for each project. + /// @param _fundingCycleStore A contract storing all funding cycle configurations. + /// @param _tokenStore A contract that manages token minting and burning. + /// @param _splitsStore A contract that stores splits for each project. + /// @param _fundAccessConstraintsStore A contract that stores fund access constraints for each project. + constructor( + IJBOperatorStore _operatorStore, + IJBProjects _projects, + IJBDirectory _directory, + IJBFundingCycleStore _fundingCycleStore, + IJBTokenStore _tokenStore, + IJBSplitsStore _splitsStore, + IJBFundAccessConstraintsStore _fundAccessConstraintsStore + ) JBOperatable(_operatorStore) { + projects = _projects; + directory = _directory; + fundingCycleStore = _fundingCycleStore; + tokenStore = _tokenStore; + splitsStore = _splitsStore; + fundAccessConstraintsStore = _fundAccessConstraintsStore; + } + + //*********************************************************************// + // --------------------- external transactions ----------------------- // + //*********************************************************************// + + /// @notice Creates a project. This will mint an ERC-721 into the specified owner's account, configure a first funding cycle, and set up any splits. + /// @dev Each operation within this transaction can be done in sequence separately. + /// @dev Anyone can deploy a project on an owner's behalf. + /// @param _owner The address to set as the owner of the project. The project ERC-721 will be owned by this address. + /// @param _projectMetadata Metadata to associate with the project within a particular domain. This can be updated any time by the owner of the project. + /// @param _configurations The funding cycle configurations to schedule. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @param _terminals Payment terminals to add for the project. + /// @param _memo A memo to pass along to the emitted event. + /// @return projectId The ID of the project. + function launchProjectFor( + address _owner, + JBProjectMetadata calldata _projectMetadata, + JBFundingCycleConfiguration[] calldata _configurations, + uint256 _mustStartAtOrAfter, + IJBPaymentTerminal[] memory _terminals, + string memory _memo + ) external virtual override returns (uint256 projectId) { + // Keep a reference to the directory. + IJBDirectory _directory = directory; + + // Mint the project into the wallet of the owner. + projectId = projects.createFor(_owner, _projectMetadata); + + // Set this contract as the project's controller in the directory. + _directory.setControllerOf(projectId, address(this)); + + // Configure the first funding cycle. + uint256 _configuration = _configure(projectId, _configurations, _mustStartAtOrAfter); + + // Add the provided terminals to the list of terminals. + if (_terminals.length > 0) _directory.setTerminalsOf(projectId, _terminals); + + emit LaunchProject(_configuration, projectId, _memo, msg.sender); + } + + /// @notice Creates a funding cycle for an already existing project ERC-721. + /// @dev Each operation within this transaction can be done in sequence separately. + /// @dev Only a project owner or operator can launch its funding cycles. + /// @param _projectId The ID of the project to launch funding cycles for. + /// @param _configurations The funding cycle configurations to schedule. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @param _terminals Payment terminals to add for the project. + /// @param _memo A memo to pass along to the emitted event. + /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. + function launchFundingCyclesFor( + uint256 _projectId, + JBFundingCycleConfiguration[] calldata _configurations, + uint256 _mustStartAtOrAfter, + IJBPaymentTerminal[] memory _terminals, + string memory _memo + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.RECONFIGURE) + returns (uint256 configured) + { + // If there is a previous configuration, reconfigureFundingCyclesOf should be called instead + if (fundingCycleStore.latestConfigurationOf(_projectId) > 0) + revert FUNDING_CYCLE_ALREADY_LAUNCHED(); + + // Set this contract as the project's controller in the directory. + directory.setControllerOf(_projectId, address(this)); + + // Configure the first funding cycle. + configured = _configure(_projectId, _configurations, _mustStartAtOrAfter); + + // Add the provided terminals to the list of terminals. + if (_terminals.length > 0) directory.setTerminalsOf(_projectId, _terminals); + + emit LaunchFundingCycles(configured, _projectId, _memo, msg.sender); + } + + /// @notice Proposes a configuration of a subsequent funding cycle that will take effect once the current one expires if it is approved by the current funding cycle's ballot. + /// @dev Only a project's owner or a designated operator can configure its funding cycles. + /// @param _projectId The ID of the project whose funding cycles are being reconfigured. + /// @param _configurations The funding cycle configurations to schedule. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @param _memo A memo to pass along to the emitted event. + /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. + function reconfigureFundingCyclesOf( + uint256 _projectId, + JBFundingCycleConfiguration[] calldata _configurations, + uint256 _mustStartAtOrAfter, + string calldata _memo + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.RECONFIGURE) + returns (uint256 configured) + { + // Configure the next funding cycle. + configured = _configure(_projectId, _configurations, _mustStartAtOrAfter); + + emit ReconfigureFundingCycles(configured, _projectId, _memo, msg.sender); + } + + /// @notice Mint new token supply into an account, and optionally reserve a supply to be distributed according to the project's current funding cycle configuration. + /// @dev Only a project's owner, a designated operator, one of its terminals, or the current data source can mint its tokens. + /// @param _projectId The ID of the project to which the tokens being minted belong. + /// @param _tokenCount The amount of tokens to mint in total, counting however many should be reserved. + /// @param _beneficiary The account that the tokens are being minted for. + /// @param _memo A memo to pass along to the emitted event. + /// @param _preferClaimedTokens A flag indicating whether a project's attached token contract should be minted if they have been issued. + /// @param _useReservedRate Whether to use the current funding cycle's reserved rate in the mint calculation. + /// @return beneficiaryTokenCount The amount of tokens minted for the beneficiary. + function mintTokensOf( + uint256 _projectId, + uint256 _tokenCount, + address _beneficiary, + string calldata _memo, + bool _preferClaimedTokens, + bool _useReservedRate + ) external virtual override returns (uint256 beneficiaryTokenCount) { + // There should be tokens to mint. + if (_tokenCount == 0) revert ZERO_TOKENS_TO_MINT(); + + // Define variables that will be needed outside scoped section below. + // Keep a reference to the reserved rate to use + uint256 _reservedRate; + + // Scoped section prevents stack too deep. `_fundingCycle` only used within scope. + { + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Minting limited to: project owner, authorized callers, project terminal and current funding cycle data source + _requirePermissionAllowingOverride( + projects.ownerOf(_projectId), + _projectId, + JBOperations.MINT, + directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) || + msg.sender == address(_fundingCycle.dataSource()) + ); + + // If the message sender is not a terminal or a datasource, the current funding cycle must allow minting. + if ( + !_fundingCycle.mintingAllowed() && + !directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) && + msg.sender != address(_fundingCycle.dataSource()) + ) revert MINT_NOT_ALLOWED_AND_NOT_TERMINAL_DELEGATE(); + + // Determine the reserved rate to use. + _reservedRate = _useReservedRate ? _fundingCycle.reservedRate() : 0; + + // Override the claimed token preference with the funding cycle value. + _preferClaimedTokens = _preferClaimedTokens == true + ? _preferClaimedTokens + : _fundingCycle.preferClaimedTokenOverride(); + } + + if (_reservedRate != JBConstants.MAX_RESERVED_RATE) { + // The unreserved token count that will be minted for the beneficiary. + beneficiaryTokenCount = PRBMath.mulDiv( + _tokenCount, + JBConstants.MAX_RESERVED_RATE - _reservedRate, + JBConstants.MAX_RESERVED_RATE + ); + + // Mint the tokens. + tokenStore.mintFor(_beneficiary, _projectId, beneficiaryTokenCount, _preferClaimedTokens); + } + + // Add reserved tokens if needed + if (_reservedRate > 0) + reservedTokenBalanceOf[_projectId] += _tokenCount - beneficiaryTokenCount; + + emit MintTokens( + _beneficiary, + _projectId, + _tokenCount, + beneficiaryTokenCount, + _memo, + _reservedRate, + msg.sender + ); + } + + /// @notice Burns a token holder's supply. + /// @dev Only a token's holder, a designated operator, or a project's terminal can burn it. + /// @param _holder The account that is having its tokens burned. + /// @param _projectId The ID of the project to which the tokens being burned belong. + /// @param _tokenCount The number of tokens to burn. + /// @param _memo A memo to pass along to the emitted event. + /// @param _preferClaimedTokens A flag indicating whether a project's attached token contract should be burned first if they have been issued. + function burnTokensOf( + address _holder, + uint256 _projectId, + uint256 _tokenCount, + string calldata _memo, + bool _preferClaimedTokens + ) + external + virtual + override + requirePermissionAllowingOverride( + _holder, + _projectId, + JBOperations.BURN, + directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) + ) + { + // There should be tokens to burn + if (_tokenCount == 0) revert NO_BURNABLE_TOKENS(); + + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // If the message sender is a terminal, the current funding cycle must not be paused. + if ( + _fundingCycle.burnPaused() && + !directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) + ) revert BURN_PAUSED_AND_SENDER_NOT_VALID_TERMINAL_DELEGATE(); + + // Burn the tokens. + tokenStore.burnFrom(_holder, _projectId, _tokenCount, _preferClaimedTokens); + + emit BurnTokens(_holder, _projectId, _tokenCount, _memo, msg.sender); + } + + /// @notice Distributes all outstanding reserved tokens for a project. + /// @param _projectId The ID of the project to which the reserved tokens belong. + /// @param _memo A memo to pass along to the emitted event. + /// @return The amount of minted reserved tokens. + function distributeReservedTokensOf( + uint256 _projectId, + string calldata _memo + ) external virtual override returns (uint256) { + return _distributeReservedTokensOf(_projectId, _memo); + } + + /// @notice Allows other controllers to signal to this one that a migration is expected for the specified project. + /// @dev This controller should not yet be the project's controller. + /// @param _projectId The ID of the project that will be migrated to this controller. + /// @param _from The controller being migrated from. + function prepForMigrationOf(uint256 _projectId, address _from) external virtual override { + _projectId; // Prevents unused var compiler and natspec complaints. + _from; // Prevents unused var compiler and natspec complaints. + } + + /// @notice Allows a project to migrate from this controller to another. + /// @dev Only a project's owner or a designated operator can migrate it. + /// @param _projectId The ID of the project that will be migrated from this controller. + /// @param _to The controller to which the project is migrating. + function migrate( + uint256 _projectId, + IJBMigratable _to + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.MIGRATE_CONTROLLER) + { + // Keep a reference to the directory. + IJBDirectory _directory = directory; + + // This controller must be the project's current controller. + if (_directory.controllerOf(_projectId) != address(this)) revert NOT_CURRENT_CONTROLLER(); + + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Migration must be allowed. + if (!_fundingCycle.controllerMigrationAllowed()) revert MIGRATION_NOT_ALLOWED(); + + // All reserved tokens must be minted before migrating. + if (reservedTokenBalanceOf[_projectId] != 0) _distributeReservedTokensOf(_projectId, ''); + + // Make sure the new controller is prepped for the migration. + _to.prepForMigrationOf(_projectId, address(this)); + + // Set the new controller. + _directory.setControllerOf(_projectId, address(_to)); + + emit Migrate(_projectId, _to, msg.sender); + } + + //*********************************************************************// + // ------------------------ internal functions ----------------------- // + //*********************************************************************// + + /// @notice Distributes all outstanding reserved tokens for a project. + /// @param _projectId The ID of the project to which the reserved tokens belong. + /// @param _memo A memo to pass along to the emitted event. + /// @return tokenCount The amount of minted reserved tokens. + function _distributeReservedTokensOf( + uint256 _projectId, + string memory _memo + ) internal returns (uint256 tokenCount) { + // Keep a reference to the token store. + IJBTokenStore _tokenStore = tokenStore; + + // Get the current funding cycle to read the reserved rate from. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Get a reference to the number of tokens that need to be minted. + tokenCount = reservedTokenBalanceOf[_projectId]; + + // Reset the reserved token balance + reservedTokenBalanceOf[_projectId] = 0; + + // Get a reference to the project owner. + address _owner = projects.ownerOf(_projectId); + + // Distribute tokens to splits and get a reference to the leftover amount to mint after all splits have gotten their share. + uint256 _leftoverTokenCount = tokenCount == 0 + ? 0 + : _distributeToReservedTokenSplitsOf( + _projectId, + _fundingCycle.configuration, + JBSplitsGroups.RESERVED_TOKENS, + tokenCount + ); + + // Mint any leftover tokens to the project owner. + if (_leftoverTokenCount > 0) + _tokenStore.mintFor(_owner, _projectId, _leftoverTokenCount, false); + + emit DistributeReservedTokens( + _fundingCycle.configuration, + _fundingCycle.number, + _projectId, + _owner, + tokenCount, + _leftoverTokenCount, + _memo, + msg.sender + ); + } + + /// @notice Distribute tokens to the splits according to the specified funding cycle configuration. + /// @param _projectId The ID of the project for which reserved token splits are being distributed. + /// @param _domain The domain of the splits to distribute the reserved tokens between. + /// @param _group The group of the splits to distribute the reserved tokens between. + /// @param _amount The total amount of tokens to mint. + /// @return leftoverAmount If the splits percents dont add up to 100%, the leftover amount is returned. + function _distributeToReservedTokenSplitsOf( + uint256 _projectId, + uint256 _domain, + uint256 _group, + uint256 _amount + ) internal returns (uint256 leftoverAmount) { + // Keep a reference to the token store. + IJBTokenStore _tokenStore = tokenStore; + + // Set the leftover amount to the initial amount. + leftoverAmount = _amount; + + // Get a reference to the project's reserved token splits. + JBSplit[] memory _splits = splitsStore.splitsOf(_projectId, _domain, _group); + + //Transfer between all splits. + for (uint256 _i; _i < _splits.length; ) { + // Get a reference to the split being iterated on. + JBSplit memory _split = _splits[_i]; + + // The amount to send towards the split. + uint256 _tokenCount = PRBMath.mulDiv( + _amount, + _split.percent, + JBConstants.SPLITS_TOTAL_PERCENT + ); + + // Mints tokens for the split if needed. + if (_tokenCount > 0) { + _tokenStore.mintFor( + // If an allocator is set in the splits, set it as the beneficiary. + // Otherwise if a projectId is set in the split, set the project's owner as the beneficiary. + // If the split has a beneficiary send to the split's beneficiary. Otherwise send to the msg.sender. + _split.allocator != IJBSplitAllocator(address(0)) + ? address(_split.allocator) + : _split.projectId != 0 + ? projects.ownerOf(_split.projectId) + : _split.beneficiary != address(0) + ? _split.beneficiary + : msg.sender, + _projectId, + _tokenCount, + _split.preferClaimed + ); + + // If there's an allocator set, trigger its `allocate` function. + if (_split.allocator != IJBSplitAllocator(address(0))) + _split.allocator.allocate( + JBSplitAllocationData( + address(_tokenStore.tokenOf(_projectId)), + _tokenCount, + 18, + _projectId, + _group, + _split + ) + ); + + // Subtract from the amount to be sent to the beneficiary. + leftoverAmount = leftoverAmount - _tokenCount; + } + + emit DistributeToReservedTokenSplit( + _projectId, + _domain, + _group, + _split, + _tokenCount, + msg.sender + ); + + unchecked { + ++_i; + } + } + } + + /// @notice Configures a funding cycle and stores information pertinent to the configuration. + /// @param _projectId The ID of the project whose funding cycles are being reconfigured. + /// @param _configurations The funding cycle configurations to schedule. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. + function _configure( + uint256 _projectId, + JBFundingCycleConfiguration[] calldata _configurations, + uint256 _mustStartAtOrAfter + ) internal returns (uint256 configured) { + // Keep a reference to the configuration being iterated on. + JBFundingCycleConfiguration memory _configuration; + + // Keep a reference to the number of configurations being scheduled. + uint256 _numberOfConfigurations = _configurations.length; + + for (uint256 _i; _i < _numberOfConfigurations; ) { + _configuration = _configurations[_i]; + + // Make sure the provided reserved rate is valid. + if (_configuration.metadata.reservedRate > JBConstants.MAX_RESERVED_RATE) + revert INVALID_RESERVED_RATE(); + + // Make sure the provided redemption rate is valid. + if (_configuration.metadata.redemptionRate > JBConstants.MAX_REDEMPTION_RATE) + revert INVALID_REDEMPTION_RATE(); + + // Make sure the provided ballot redemption rate is valid. + if (_configuration.metadata.ballotRedemptionRate > JBConstants.MAX_REDEMPTION_RATE) + revert INVALID_BALLOT_REDEMPTION_RATE(); + + // Configure the funding cycle's properties. + JBFundingCycle memory _fundingCycle = fundingCycleStore.configureFor( + _projectId, + _configuration.data, + JBFundingCycleMetadataResolver.packFundingCycleMetadata(_configuration.metadata), + _mustStartAtOrAfter + ); + + // Set splits for the group. + splitsStore.set(_projectId, _fundingCycle.configuration, _configuration.groupedSplits); + + // Set the funds access constraints. + fundAccessConstraintsStore.setFor( + _projectId, + _fundingCycle.configuration, + _configuration.fundAccessConstraints + ); + + // Return the configured timestamp if this is the last configuration being scheduled. + if (_i == _numberOfConfigurations - 1) configured = _fundingCycle.configuration; + + unchecked { + ++_i; + } + } + + return 0; + } +} diff --git a/contracts/interfaces/IJBController3_2.sol b/contracts/interfaces/IJBController3_2.sol new file mode 100644 index 000000000..d5057f906 --- /dev/null +++ b/contracts/interfaces/IJBController3_2.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import {JBBallotState} from './../enums/JBBallotState.sol'; +import {JBFundAccessConstraints} from './../structs/JBFundAccessConstraints.sol'; +import {JBFundingCycle} from './../structs/JBFundingCycle.sol'; +import {JBFundingCycleData} from './../structs/JBFundingCycleData.sol'; +import {JBFundingCycleMetadata} from './../structs/JBFundingCycleMetadata.sol'; +import {JBGroupedSplits} from './../structs/JBGroupedSplits.sol'; +import {JBProjectMetadata} from './../structs/JBProjectMetadata.sol'; +import {JBFundingCycleConfiguration} from './../structs/JBFundingCycleConfiguration.sol'; +import {JBSplit} from './../structs/JBSplit.sol'; +import {IJBController3_0_1} from './IJBController3_0_1.sol'; +import {IJBDirectory} from './IJBDirectory.sol'; +import {IJBFundAccessConstraintsStore} from './IJBFundAccessConstraintsStore.sol'; +import {IJBFundingCycleStore} from './IJBFundingCycleStore.sol'; +import {IJBMigratable} from './IJBMigratable.sol'; +import {IJBPaymentTerminal} from './IJBPaymentTerminal.sol'; +import {IJBProjects} from './IJBProjects.sol'; +import {IJBSplitsStore} from './IJBSplitsStore.sol'; +import {IJBTokenStore} from './IJBTokenStore.sol'; + +interface IJBController3_2 is IERC165 { + event LaunchProject(uint256 configuration, uint256 projectId, string memo, address caller); + + event LaunchFundingCycles(uint256 configuration, uint256 projectId, string memo, address caller); + + event ReconfigureFundingCycles( + uint256 configuration, + uint256 projectId, + string memo, + address caller + ); + + event DistributeReservedTokens( + uint256 indexed fundingCycleConfiguration, + uint256 indexed fundingCycleNumber, + uint256 indexed projectId, + address beneficiary, + uint256 tokenCount, + uint256 beneficiaryTokenCount, + string memo, + address caller + ); + + event DistributeToReservedTokenSplit( + uint256 indexed projectId, + uint256 indexed domain, + uint256 indexed group, + JBSplit split, + uint256 tokenCount, + address caller + ); + + event MintTokens( + address indexed beneficiary, + uint256 indexed projectId, + uint256 tokenCount, + uint256 beneficiaryTokenCount, + string memo, + uint256 reservedRate, + address caller + ); + + event BurnTokens( + address indexed holder, + uint256 indexed projectId, + uint256 tokenCount, + string memo, + address caller + ); + + event Migrate(uint256 indexed projectId, IJBMigratable to, address caller); + + event PrepMigration(uint256 indexed projectId, address from, address caller); + + function projects() external view returns (IJBProjects); + + function fundingCycleStore() external view returns (IJBFundingCycleStore); + + function tokenStore() external view returns (IJBTokenStore); + + function splitsStore() external view returns (IJBSplitsStore); + + function fundAccessConstraintsStore() external view returns (IJBFundAccessConstraintsStore); + + function directory() external view returns (IJBDirectory); + + function reservedTokenBalanceOf(uint256 projectId) external view returns (uint256); + + function totalOutstandingTokensOf(uint256 projectId) external view returns (uint256); + + function getFundingCycleOf( + uint256 projectId, + uint256 configuration + ) + external + view + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata memory metadata); + + function latestConfiguredFundingCycleOf( + uint256 projectId + ) + external + view + returns (JBFundingCycle memory, JBFundingCycleMetadata memory metadata, JBBallotState); + + function currentFundingCycleOf( + uint256 projectId + ) + external + view + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata memory metadata); + + function queuedFundingCycleOf( + uint256 projectId + ) + external + view + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata memory metadata); + + function launchProjectFor( + address owner, + JBProjectMetadata calldata projectMetadata, + JBFundingCycleConfiguration[] calldata configurations, + uint256 mustStartAtOrAfter, + IJBPaymentTerminal[] memory terminals, + string calldata memo + ) external returns (uint256 projectId); + + function launchFundingCyclesFor( + uint256 projectId, + JBFundingCycleConfiguration[] calldata configurations, + uint256 mustStartAtOrAfter, + IJBPaymentTerminal[] memory terminals, + string calldata memo + ) external returns (uint256 configured); + + function reconfigureFundingCyclesOf( + uint256 projectId, + JBFundingCycleConfiguration[] calldata configurations, + uint256 mustStartAtOrAfter, + string calldata memo + ) external returns (uint256 configured); + + function mintTokensOf( + uint256 projectId, + uint256 tokenCount, + address beneficiary, + string calldata memo, + bool preferClaimedTokens, + bool useReservedRate + ) external returns (uint256 beneficiaryTokenCount); + + function burnTokensOf( + address holder, + uint256 projectId, + uint256 tokenCount, + string calldata memo, + bool preferClaimedTokens + ) external; + + function distributeReservedTokensOf( + uint256 projectId, + string memory memo + ) external returns (uint256); + + function migrate(uint256 projectId, IJBMigratable to) external; +} diff --git a/contracts/structs/JBFundingCycleConfiguration.sol b/contracts/structs/JBFundingCycleConfiguration.sol new file mode 100644 index 000000000..a09e38969 --- /dev/null +++ b/contracts/structs/JBFundingCycleConfiguration.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {JBFundingCycleData} from './JBFundingCycleData.sol'; +import {JBFundingCycleMetadata} from './JBFundingCycleMetadata.sol'; +import {JBGroupedSplits} from './JBGroupedSplits.sol'; +import {JBFundAccessConstraints} from './JBFundAccessConstraints.sol'; + +/// @custom:member _data Data that defines the project's first funding cycle. These properties will remain fixed for the duration of the funding cycle. +/// @custom:member _metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. +/// @custom:member _groupedSplits An array of splits to set for any number of groups. +/// @custom:member _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. +struct JBFundingCycleConfiguration { + JBFundingCycleData data; + JBFundingCycleMetadata metadata; + JBGroupedSplits[] groupedSplits; + JBFundAccessConstraints[] fundAccessConstraints; +} From 41ac91e1b8c67ddfa9f7912726b3a2194f9695c8 Mon Sep 17 00:00:00 2001 From: mejango Date: Sun, 10 Sep 2023 23:42:36 -0400 Subject: [PATCH 11/22] natspec --- contracts/JBController3_2.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/JBController3_2.sol b/contracts/JBController3_2.sol index 5132068e6..b97631726 100644 --- a/contracts/JBController3_2.sol +++ b/contracts/JBController3_2.sol @@ -672,6 +672,7 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl uint256 _numberOfConfigurations = _configurations.length; for (uint256 _i; _i < _numberOfConfigurations; ) { + // Get a reference to the configuration being iterated on. _configuration = _configurations[_i]; // Make sure the provided reserved rate is valid. From c728e82bd12f2be70a36bd3bb3efc84a53b3e3f8 Mon Sep 17 00:00:00 2001 From: mejango Date: Sun, 10 Sep 2023 23:43:30 -0400 Subject: [PATCH 12/22] more natspec --- contracts/structs/JBFundingCycleConfiguration.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/structs/JBFundingCycleConfiguration.sol b/contracts/structs/JBFundingCycleConfiguration.sol index a09e38969..941d4c89f 100644 --- a/contracts/structs/JBFundingCycleConfiguration.sol +++ b/contracts/structs/JBFundingCycleConfiguration.sol @@ -6,10 +6,10 @@ import {JBFundingCycleMetadata} from './JBFundingCycleMetadata.sol'; import {JBGroupedSplits} from './JBGroupedSplits.sol'; import {JBFundAccessConstraints} from './JBFundAccessConstraints.sol'; -/// @custom:member _data Data that defines the project's first funding cycle. These properties will remain fixed for the duration of the funding cycle. +/// @custom:member _data Data that defines the project's funding cycle. These properties will remain fixed for the duration of the funding cycle. /// @custom:member _metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. -/// @custom:member _groupedSplits An array of splits to set for any number of groups. -/// @custom:member _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. +/// @custom:member _groupedSplits An array of splits to set for any number of groups while the funding cycle configuration is active. +/// @custom:member _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal while the funding cycle configuration is active. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. struct JBFundingCycleConfiguration { JBFundingCycleData data; JBFundingCycleMetadata metadata; From 9883fda4cf33d688be19847a5080f2ac2bffecf8 Mon Sep 17 00:00:00 2001 From: mejango Date: Sun, 10 Sep 2023 23:45:35 -0400 Subject: [PATCH 13/22] imports --- contracts/JBController3_2.sol | 7 ++----- contracts/interfaces/IJBController3_2.sol | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/contracts/JBController3_2.sol b/contracts/JBController3_2.sol index b97631726..40e330e00 100644 --- a/contracts/JBController3_2.sol +++ b/contracts/JBController3_2.sol @@ -24,15 +24,12 @@ import {JBFundingCycleMetadataResolver} from './libraries/JBFundingCycleMetadata import {JBOperations} from './libraries/JBOperations.sol'; import {JBSplitsGroups} from './libraries/JBSplitsGroups.sol'; import {JBFundingCycle} from './structs/JBFundingCycle.sol'; -import {JBSplitAllocationData} from './structs/JBSplitAllocationData.sol'; -import {JBFundAccessConstraints} from './structs/JBFundAccessConstraints.sol'; import {JBFundingCycle} from './structs/JBFundingCycle.sol'; -import {JBFundingCycleData} from './structs/JBFundingCycleData.sol'; +import {JBFundingCycleConfiguration} from './structs/JBFundingCycleConfiguration.sol'; import {JBFundingCycleMetadata} from './structs/JBFundingCycleMetadata.sol'; -import {JBGroupedSplits} from './structs/JBGroupedSplits.sol'; import {JBProjectMetadata} from './structs/JBProjectMetadata.sol'; import {JBSplit} from './structs/JBSplit.sol'; -import {JBFundingCycleConfiguration} from './structs/JBFundingCycleConfiguration.sol'; +import {JBSplitAllocationData} from './structs/JBSplitAllocationData.sol'; /// @notice Stitches together funding cycles and project tokens, making sure all activity is accounted for and correct. /// @dev This Controller has the same functionality as JBController3_0_1, except it is not backwards compatible with the original IJBController view methods. diff --git a/contracts/interfaces/IJBController3_2.sol b/contracts/interfaces/IJBController3_2.sol index d5057f906..0f8a4cfd9 100644 --- a/contracts/interfaces/IJBController3_2.sol +++ b/contracts/interfaces/IJBController3_2.sol @@ -3,13 +3,10 @@ pragma solidity ^0.8.0; import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; import {JBBallotState} from './../enums/JBBallotState.sol'; -import {JBFundAccessConstraints} from './../structs/JBFundAccessConstraints.sol'; import {JBFundingCycle} from './../structs/JBFundingCycle.sol'; -import {JBFundingCycleData} from './../structs/JBFundingCycleData.sol'; +import {JBFundingCycleConfiguration} from './../structs/JBFundingCycleConfiguration.sol'; import {JBFundingCycleMetadata} from './../structs/JBFundingCycleMetadata.sol'; -import {JBGroupedSplits} from './../structs/JBGroupedSplits.sol'; import {JBProjectMetadata} from './../structs/JBProjectMetadata.sol'; -import {JBFundingCycleConfiguration} from './../structs/JBFundingCycleConfiguration.sol'; import {JBSplit} from './../structs/JBSplit.sol'; import {IJBController3_0_1} from './IJBController3_0_1.sol'; import {IJBDirectory} from './IJBDirectory.sol'; From d3d80aa49f7aef5b358efe7a5cba1551cc41141f Mon Sep 17 00:00:00 2001 From: mejango Date: Mon, 11 Sep 2023 07:25:23 -0400 Subject: [PATCH 14/22] done --- contracts/JBController3_2.sol | 18 +++++------------- contracts/interfaces/IJBController3_2.sol | 3 --- .../structs/JBFundingCycleConfiguration.sol | 10 ++++++---- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/contracts/JBController3_2.sol b/contracts/JBController3_2.sol index 40e330e00..8d8fd4ac6 100644 --- a/contracts/JBController3_2.sol +++ b/contracts/JBController3_2.sol @@ -243,7 +243,6 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl /// @param _owner The address to set as the owner of the project. The project ERC-721 will be owned by this address. /// @param _projectMetadata Metadata to associate with the project within a particular domain. This can be updated any time by the owner of the project. /// @param _configurations The funding cycle configurations to schedule. - /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. /// @param _terminals Payment terminals to add for the project. /// @param _memo A memo to pass along to the emitted event. /// @return projectId The ID of the project. @@ -251,7 +250,6 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl address _owner, JBProjectMetadata calldata _projectMetadata, JBFundingCycleConfiguration[] calldata _configurations, - uint256 _mustStartAtOrAfter, IJBPaymentTerminal[] memory _terminals, string memory _memo ) external virtual override returns (uint256 projectId) { @@ -265,7 +263,7 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl _directory.setControllerOf(projectId, address(this)); // Configure the first funding cycle. - uint256 _configuration = _configure(projectId, _configurations, _mustStartAtOrAfter); + uint256 _configuration = _configure(projectId, _configurations); // Add the provided terminals to the list of terminals. if (_terminals.length > 0) _directory.setTerminalsOf(projectId, _terminals); @@ -278,14 +276,12 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl /// @dev Only a project owner or operator can launch its funding cycles. /// @param _projectId The ID of the project to launch funding cycles for. /// @param _configurations The funding cycle configurations to schedule. - /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. /// @param _terminals Payment terminals to add for the project. /// @param _memo A memo to pass along to the emitted event. /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. function launchFundingCyclesFor( uint256 _projectId, JBFundingCycleConfiguration[] calldata _configurations, - uint256 _mustStartAtOrAfter, IJBPaymentTerminal[] memory _terminals, string memory _memo ) @@ -303,7 +299,7 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl directory.setControllerOf(_projectId, address(this)); // Configure the first funding cycle. - configured = _configure(_projectId, _configurations, _mustStartAtOrAfter); + configured = _configure(_projectId, _configurations); // Add the provided terminals to the list of terminals. if (_terminals.length > 0) directory.setTerminalsOf(_projectId, _terminals); @@ -315,13 +311,11 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl /// @dev Only a project's owner or a designated operator can configure its funding cycles. /// @param _projectId The ID of the project whose funding cycles are being reconfigured. /// @param _configurations The funding cycle configurations to schedule. - /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. /// @param _memo A memo to pass along to the emitted event. /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. function reconfigureFundingCyclesOf( uint256 _projectId, JBFundingCycleConfiguration[] calldata _configurations, - uint256 _mustStartAtOrAfter, string calldata _memo ) external @@ -331,7 +325,7 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl returns (uint256 configured) { // Configure the next funding cycle. - configured = _configure(_projectId, _configurations, _mustStartAtOrAfter); + configured = _configure(_projectId, _configurations); emit ReconfigureFundingCycles(configured, _projectId, _memo, msg.sender); } @@ -655,12 +649,10 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl /// @notice Configures a funding cycle and stores information pertinent to the configuration. /// @param _projectId The ID of the project whose funding cycles are being reconfigured. /// @param _configurations The funding cycle configurations to schedule. - /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. function _configure( uint256 _projectId, - JBFundingCycleConfiguration[] calldata _configurations, - uint256 _mustStartAtOrAfter + JBFundingCycleConfiguration[] calldata _configurations ) internal returns (uint256 configured) { // Keep a reference to the configuration being iterated on. JBFundingCycleConfiguration memory _configuration; @@ -689,7 +681,7 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl _projectId, _configuration.data, JBFundingCycleMetadataResolver.packFundingCycleMetadata(_configuration.metadata), - _mustStartAtOrAfter + _configuration.mustStartAtOrAfter ); // Set splits for the group. diff --git a/contracts/interfaces/IJBController3_2.sol b/contracts/interfaces/IJBController3_2.sol index 0f8a4cfd9..2653ada28 100644 --- a/contracts/interfaces/IJBController3_2.sol +++ b/contracts/interfaces/IJBController3_2.sol @@ -121,7 +121,6 @@ interface IJBController3_2 is IERC165 { address owner, JBProjectMetadata calldata projectMetadata, JBFundingCycleConfiguration[] calldata configurations, - uint256 mustStartAtOrAfter, IJBPaymentTerminal[] memory terminals, string calldata memo ) external returns (uint256 projectId); @@ -129,7 +128,6 @@ interface IJBController3_2 is IERC165 { function launchFundingCyclesFor( uint256 projectId, JBFundingCycleConfiguration[] calldata configurations, - uint256 mustStartAtOrAfter, IJBPaymentTerminal[] memory terminals, string calldata memo ) external returns (uint256 configured); @@ -137,7 +135,6 @@ interface IJBController3_2 is IERC165 { function reconfigureFundingCyclesOf( uint256 projectId, JBFundingCycleConfiguration[] calldata configurations, - uint256 mustStartAtOrAfter, string calldata memo ) external returns (uint256 configured); diff --git a/contracts/structs/JBFundingCycleConfiguration.sol b/contracts/structs/JBFundingCycleConfiguration.sol index 941d4c89f..2fc268345 100644 --- a/contracts/structs/JBFundingCycleConfiguration.sol +++ b/contracts/structs/JBFundingCycleConfiguration.sol @@ -6,11 +6,13 @@ import {JBFundingCycleMetadata} from './JBFundingCycleMetadata.sol'; import {JBGroupedSplits} from './JBGroupedSplits.sol'; import {JBFundAccessConstraints} from './JBFundAccessConstraints.sol'; -/// @custom:member _data Data that defines the project's funding cycle. These properties will remain fixed for the duration of the funding cycle. -/// @custom:member _metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. -/// @custom:member _groupedSplits An array of splits to set for any number of groups while the funding cycle configuration is active. -/// @custom:member _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal while the funding cycle configuration is active. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. +/// @custom:member mustStartAtOrAfter The time before which the configured funding cycle cannot start. +/// @custom:member data Data that defines the project's funding cycle. These properties will remain fixed for the duration of the funding cycle. +/// @custom:member metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. +/// @custom:member groupedSplits An array of splits to set for any number of groups while the funding cycle configuration is active. +/// @custom:member fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal while the funding cycle configuration is active. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. struct JBFundingCycleConfiguration { + uint256 mustStartAtOrAfter; JBFundingCycleData data; JBFundingCycleMetadata metadata; JBGroupedSplits[] groupedSplits; From 0756477a980ea4fb574842dbccef12cf56c59316 Mon Sep 17 00:00:00 2001 From: mejango Date: Mon, 11 Sep 2023 07:26:09 -0400 Subject: [PATCH 15/22] fixed return val --- contracts/JBController3_2.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/JBController3_2.sol b/contracts/JBController3_2.sol index 8d8fd4ac6..7cc739309 100644 --- a/contracts/JBController3_2.sol +++ b/contracts/JBController3_2.sol @@ -701,7 +701,5 @@ contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratabl ++_i; } } - - return 0; } } From f89b7a8d1517a18d595c4a16391fc96e7697ab5c Mon Sep 17 00:00:00 2001 From: mejango Date: Fri, 15 Sep 2023 14:38:29 -0400 Subject: [PATCH 16/22] allow overriding an APPROVAL_EXPECTED FC --- contracts/JBFundingCycleStore.sol | 3 +- .../configure_for.test.js | 164 +++++++++++++++++- 2 files changed, 162 insertions(+), 5 deletions(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index fa565d845..50938488d 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -369,8 +369,7 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { _ballotState != JBBallotState.Empty) || (block.timestamp < _baseFundingCycle.start && _mustStartAtOrAfter < _baseFundingCycle.start + _baseFundingCycle.duration && - _ballotState != JBBallotState.Approved && - _ballotState != JBBallotState.ApprovalExpected) || + _ballotState != JBBallotState.Approved) || (block.timestamp < _baseFundingCycle.start && _mustStartAtOrAfter >= _baseFundingCycle.start + _baseFundingCycle.duration && _ballotState != JBBallotState.Approved && diff --git a/test/jb_funding_cycle_store/configure_for.test.js b/test/jb_funding_cycle_store/configure_for.test.js index 0db537c61..f2e9b6df6 100644 --- a/test/jb_funding_cycle_store/configure_for.test.js +++ b/test/jb_funding_cycle_store/configure_for.test.js @@ -1040,6 +1040,164 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { }); }); + it('Should override a funding cycle with approval expected', async function () { + const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); + await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); + + const firstFundingCycleData = createFundingCycleData({ + ballot: mockBallot.address + }); + + // Set the ballot to have a short duration. + await mockBallot.mock.duration.withArgs().returns(0); + + // Configure first funding cycle + const firstConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + firstFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_1, + FUNDING_CYCLE_CAN_START_ASAP, + ); + + // The timestamp the first configuration was made during. + const firstConfigurationTimestamp = await getTimestamp(firstConfigureForTx.blockNumber); + + const expectedFirstFundingCycle = { + number: ethers.BigNumber.from(1), + configuration: firstConfigurationTimestamp, + basedOn: ethers.BigNumber.from(0), + start: firstConfigurationTimestamp, + duration: firstFundingCycleData.duration, + weight: firstFundingCycleData.weight, + discountRate: firstFundingCycleData.discountRate, + ballot: firstFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_1, + }; + + const secondFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(1), + weight: firstFundingCycleData.weight.add(1), + }); + + const thirdFundingCycleData = createFundingCycleData({ + duration: firstFundingCycleData.duration.add(2), + weight: firstFundingCycleData.weight.add(2), + }); + + const secondFundingCycleMustStartOnOrAfter = 0; // asap + + const thirdFundingCycleMustStartOnOrAfter = 0; // asap + + // Configure second funding cycle + const secondConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + secondFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + secondFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the second configuration was made during. + const secondConfigurationTimestamp = await getTimestamp(secondConfigureForTx.blockNumber); + + await expect(secondConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + secondConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.APPROVAL_EXPECTED); + + const expectedSecondFundingCycle = { + number: ethers.BigNumber.from(2), // second cycle + configuration: secondConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the second cycle + duration: secondFundingCycleData.duration, + weight: secondFundingCycleData.weight, + discountRate: secondFundingCycleData.discountRate, + ballot: secondFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + expect( + cleanFundingCycle(await jbFundingCycleStore.get(PROJECT_ID, secondConfigurationTimestamp)), + ).to.eql(expectedSecondFundingCycle); + + let [latestFundingCycle, ballotState] = await jbFundingCycleStore.latestConfiguredOf( + PROJECT_ID, + ); + expect(cleanFundingCycle(latestFundingCycle)).to.eql(expectedSecondFundingCycle); + expect(ballotState).to.deep.eql(ballotStatus.APPROVAL_EXPECTED); + + // Queued shows the second. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql(expectedSecondFundingCycle); + + // Configure third funding cycle to overwrite + const thirdConfigureForTx = await jbFundingCycleStore + .connect(controller) + .configureFor( + PROJECT_ID, + thirdFundingCycleData, + RANDOM_FUNDING_CYCLE_METADATA_2, + thirdFundingCycleMustStartOnOrAfter, + ); + + // The timestamp the second configuration was made during. + const thirdConfigurationTimestamp = await getTimestamp(thirdConfigureForTx.blockNumber); + + await expect(thirdConfigureForTx) + .to.emit(jbFundingCycleStore, `Init`) + .withArgs(thirdConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + thirdConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.APPROVAL_EXPECTED); + + const expectedThirdFundingCycle = { + number: ethers.BigNumber.from(2), // second cycle + configuration: thirdConfigurationTimestamp, + basedOn: firstConfigurationTimestamp, // based on the first cycle + start: firstConfigurationTimestamp.add(firstFundingCycleData.duration), // starts at the end of the second cycle + duration: thirdFundingCycleData.duration, + weight: thirdFundingCycleData.weight, + discountRate: thirdFundingCycleData.discountRate, + ballot: thirdFundingCycleData.ballot, + metadata: RANDOM_FUNDING_CYCLE_METADATA_2, + }; + + // Queued shows the third which overwrote the second. + expect(cleanFundingCycle(await jbFundingCycleStore.queuedOf(PROJECT_ID))).to.eql(expectedThirdFundingCycle); + + // fast forward to after the first cycle. + await fastForward(firstConfigureForTx.blockNumber, firstFundingCycleData.duration); + + // Mock the ballot on the failed funding cycle as approved. + await mockBallot.mock.stateOf + .withArgs( + PROJECT_ID, + thirdConfigurationTimestamp, + firstConfigurationTimestamp.add(firstFundingCycleData.duration), + ) + .returns(ballotStatus.APPROVED); + + // Current shows the third which overwrote the second. + expect(cleanFundingCycle(await jbFundingCycleStore.currentOf(PROJECT_ID))).to.eql(expectedThirdFundingCycle); + }); + it('Should configure multiple subsequent cycles that start in the future with standby ballots that turn failed with deeper nesting', async function () { const { controller, mockJbDirectory, jbFundingCycleStore, mockBallot } = await setup(); await mockJbDirectory.mock.controllerOf.withArgs(PROJECT_ID).returns(controller.address); @@ -1101,7 +1259,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { const fourthFundingCycleMustStartOnOrAfter = firstConfigurationTimestamp.add( firstFundingCycleData.duration, - ).add(secondFundingCycleData.duration).add(thirdFundingCycleData.duration); + ).add(firstFundingCycleData.duration).add(firstFundingCycleData.duration).add(thirdFundingCycleData.duration); // Configure second funding cycle const secondConfigureForTx = await jbFundingCycleStore @@ -1120,7 +1278,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { .to.emit(jbFundingCycleStore, `Init`) .withArgs(secondConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); - // Mock the ballot on the failed funding cycle as approved. + // Mock the ballot on the failed funding cycle as standby. await mockBallot.mock.stateOf .withArgs( PROJECT_ID, @@ -1175,7 +1333,7 @@ describe.only('JBFundingCycleStore::configureFor(...)', function () { .to.emit(jbFundingCycleStore, `Init`) .withArgs(thirdConfigurationTimestamp, PROJECT_ID, /*basedOn=*/ firstConfigurationTimestamp); - // Mock the ballot on the failed funding cycle as approved. + // Mock the ballot on the failed funding cycle as approval expected. await mockBallot.mock.stateOf .withArgs( PROJECT_ID, From 8132c7c29f74da66b941c0871db1f9ce86902f2e Mon Sep 17 00:00:00 2001 From: mejango Date: Fri, 15 Sep 2023 17:41:34 -0400 Subject: [PATCH 17/22] natspec --- contracts/JBFundingCycleStore.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/JBFundingCycleStore.sol b/contracts/JBFundingCycleStore.sol index 50938488d..2e612defd 100644 --- a/contracts/JBFundingCycleStore.sol +++ b/contracts/JBFundingCycleStore.sol @@ -361,7 +361,7 @@ contract JBFundingCycleStore is JBControllerUtility, IJBFundingCycleStore { // Get a reference to the ballot state. JBBallotState _ballotState = _ballotStateOf(_projectId, _baseFundingCycle); - // If the funding cycle has started but wasn't approved if a ballot exists OR it hasn't started but is currently approved, set the ID to be the funding cycle it's based on, + // If the base funding cycle has started but wasn't approved if a ballot exists OR it hasn't started but is currently approved OR it hasn't started but it is likely to be approved and takes place before the proposed one, set the ID to be the funding cycle it's based on, // which carries the latest approved configuration. if ( (block.timestamp >= _baseFundingCycle.start && From 154d0191aa4d8fda53a0080cca1b49873017909a Mon Sep 17 00:00:00 2001 From: Justin Pulley Date: Wed, 27 Sep 2023 08:33:37 -0500 Subject: [PATCH 18/22] test: Same block multiple config overwriting ApprovalExpected(s) --- forge_tests/TestReconfigure.sol | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/forge_tests/TestReconfigure.sol b/forge_tests/TestReconfigure.sol index f89d2272b..9bb62a9d6 100644 --- a/forge_tests/TestReconfigure.sol +++ b/forge_tests/TestReconfigure.sol @@ -565,4 +565,65 @@ contract TestReconfigureProject_Local is TestBaseWorkflow { assertEq(fundingCycle.number, 2); assertEq(fundingCycle.weight, _dataReconfiguration.weight); } + + function testSingleBlockOverwriteApprovalExpected() public { + uint256 weightFirstReconfiguration = 1234 * 10 ** 18; + uint256 weightSecondReconfiguration = 6969 * 10 ** 18; + + // Keep a reference to the expected timestamp after reconfigurations, starting now, incremented later in-line for readability. + uint256 expectedTimestamp = block.timestamp; + + uint256 projectId = controller.launchProjectFor( + multisig(), + _projectMetadata, + _data, + _metadata, + 0, // Start asap + _groupedSplits, + _fundAccessConstraints, + _terminals, + "" + ); + + JBFundingCycle memory fundingCycle = jbFundingCycleStore().currentOf(projectId); + + // Initial funding cycle data: will have a block.timestamp that is 2 less than the second reconfiguration (timestamps are incremented when queued in same block now) + assertEq(fundingCycle.number, 1); + assertEq(fundingCycle.weight, _data.weight); + + // Overwrites the initial configuration & will also be overwritten as 3 days will not pass and it's status is "ApprovalExpected" + vm.prank(multisig()); + controller.reconfigureFundingCyclesOf( + projectId, + JBFundingCycleData({duration: 6 days, weight: weightFirstReconfiguration, discountRate: 0, ballot: _ballot}), // 3days ballot + _metadata, + block.timestamp + 3 days, + _groupedSplits, + _fundAccessConstraints, + "" + ); + + expectedTimestamp += 1; + + // overwriting reconfiguration + vm.prank(multisig()); + controller.reconfigureFundingCyclesOf( + projectId, + JBFundingCycleData({duration: 6 days, weight: weightSecondReconfiguration, discountRate: 0, ballot: JBReconfigurationBufferBallot(address(0))}), // 3days ballot + _metadata, + block.timestamp + 3 days, + _groupedSplits, + _fundAccessConstraints, + "" + ); + + expectedTimestamp += 1; + + JBFundingCycle memory current = jbFundingCycleStore().queuedOf(projectId); + + assertEq(current.number, 2); + assertEq(current.configuration, expectedTimestamp); + assertEq(current.weight, weightSecondReconfiguration); + + } } From 6b8f587aa0ab899e9d3f50276e351b791e758a06 Mon Sep 17 00:00:00 2001 From: Justin Pulley Date: Wed, 27 Sep 2023 13:45:20 -0500 Subject: [PATCH 19/22] fix: comments, some extra assertions, test name --- forge_tests/TestReconfigure.sol | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/forge_tests/TestReconfigure.sol b/forge_tests/TestReconfigure.sol index 9bb62a9d6..70a6d1e44 100644 --- a/forge_tests/TestReconfigure.sol +++ b/forge_tests/TestReconfigure.sol @@ -566,7 +566,7 @@ contract TestReconfigureProject_Local is TestBaseWorkflow { assertEq(fundingCycle.weight, _dataReconfiguration.weight); } - function testSingleBlockOverwriteApprovalExpected() public { + function testSingleBlockOverwriteQueued() public { uint256 weightFirstReconfiguration = 1234 * 10 ** 18; uint256 weightSecondReconfiguration = 6969 * 10 ** 18; @@ -587,15 +587,15 @@ contract TestReconfigureProject_Local is TestBaseWorkflow { JBFundingCycle memory fundingCycle = jbFundingCycleStore().currentOf(projectId); - // Initial funding cycle data: will have a block.timestamp that is 2 less than the second reconfiguration (timestamps are incremented when queued in same block now) + // Initial funding cycle data: will have a block.timestamp (configuration) that is 2 less than the second reconfiguration (timestamps are incremented when queued in same block now) assertEq(fundingCycle.number, 1); assertEq(fundingCycle.weight, _data.weight); - // Overwrites the initial configuration & will also be overwritten as 3 days will not pass and it's status is "ApprovalExpected" + // Becomes queued & will be overwritten as 3 days will not pass and it's status is "ApprovalExpected" vm.prank(multisig()); controller.reconfigureFundingCyclesOf( projectId, - JBFundingCycleData({duration: 6 days, weight: weightFirstReconfiguration, discountRate: 0, ballot: _ballot}), // 3days ballot + JBFundingCycleData({duration: 6 days, weight: weightFirstReconfiguration, discountRate: 0, ballot: JBReconfigurationBufferBallot(_ballot)}), // 3days ballot _metadata, block.timestamp + 3 days, _groupedSplits, @@ -605,6 +605,12 @@ contract TestReconfigureProject_Local is TestBaseWorkflow { expectedTimestamp += 1; + JBFundingCycle memory queuedToOverwrite = jbFundingCycleStore().queuedOf(projectId); + + assertEq(queuedToOverwrite.number, 2); + assertEq(queuedToOverwrite.configuration, expectedTimestamp); + assertEq(queuedToOverwrite.weight, weightFirstReconfiguration); + // overwriting reconfiguration vm.prank(multisig()); controller.reconfigureFundingCyclesOf( @@ -619,11 +625,11 @@ contract TestReconfigureProject_Local is TestBaseWorkflow { expectedTimestamp += 1; - JBFundingCycle memory current = jbFundingCycleStore().queuedOf(projectId); + JBFundingCycle memory queued = jbFundingCycleStore().queuedOf(projectId); - assertEq(current.number, 2); - assertEq(current.configuration, expectedTimestamp); - assertEq(current.weight, weightSecondReconfiguration); + assertEq(queued.number, 2); + assertEq(queued.configuration, expectedTimestamp); + assertEq(queued.weight, weightSecondReconfiguration); } } From c92834e9a40ac1b0a6cb5d744421ffa201c5cae2 Mon Sep 17 00:00:00 2001 From: Justin Pulley Date: Fri, 29 Sep 2023 17:45:27 -0500 Subject: [PATCH 20/22] test: mixed mustStartAtOrAfter fc queueing --- forge_tests/TestReconfigure.sol | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/forge_tests/TestReconfigure.sol b/forge_tests/TestReconfigure.sol index 70a6d1e44..39d9d5ccd 100644 --- a/forge_tests/TestReconfigure.sol +++ b/forge_tests/TestReconfigure.sol @@ -566,6 +566,92 @@ contract TestReconfigureProject_Local is TestBaseWorkflow { assertEq(fundingCycle.weight, _dataReconfiguration.weight); } + function testMixedStarts() public { + // Keep references to our different weights for assertions + uint256 weightInitial = 1000 * 10 ** 18; + uint256 weightFirstReconfiguration = 1234 * 10 ** 18; + uint256 weightSecondReconfiguration = 6969 * 10 ** 18; + + // Keep a reference to the expected configuration timestamps + uint256 initialTimestamp = block.timestamp; + uint256 expectedTimestamp = block.timestamp; + + uint256 projectId = controller.launchProjectFor( + multisig(), + _projectMetadata, + JBFundingCycleData({duration: 6 days, weight: weightInitial, discountRate: 0, ballot: JBReconfigurationBufferBallot(_ballot)}), // 3days ballot + _metadata, + 0, + _groupedSplits, + _fundAccessConstraints, + _terminals, + "" + ); + + JBFundingCycle memory fundingCycle = jbFundingCycleStore().currentOf(projectId); + + // First cycle has begun + assertEq(fundingCycle.number, 1); + assertEq(fundingCycle.weight, weightInitial); + assertEq(fundingCycle.configuration, block.timestamp); + + // create a to-be overridden reconfiguration (will be in ApprovalExpected status due to ballot) + vm.prank(multisig()); + controller.reconfigureFundingCyclesOf( + projectId, + JBFundingCycleData({duration: 6 days, weight: weightFirstReconfiguration, discountRate: 0, ballot: JBReconfigurationBufferBallot(_ballot)}), // 3days ballot + _metadata, + 0, // Start asap, inherently accounts for ballot duration, so this is 9 days + _groupedSplits, + _fundAccessConstraints, + "" + ); + + // Confirm the configuration is queued + expectedTimestamp += 1; + JBFundingCycle memory queued = jbFundingCycleStore().queuedOf(projectId); + + assertEq(queued.number, 2); + assertEq(queued.configuration, expectedTimestamp); + assertEq(queued.weight, weightFirstReconfiguration); + + // Will follow the rolledover (FC #1) cycle, after overriding the above config, bc first reconfig is in ApprovalExpected status (3 days ballot has not passed) + // FC #1 rolls over bc our mustStartAtOrAfter occurs later than when FC #1 ends. + vm.prank(multisig()); + controller.reconfigureFundingCyclesOf( + projectId, + JBFundingCycleData({duration: 6 days, weight: weightSecondReconfiguration, discountRate: 0, ballot: JBReconfigurationBufferBallot(_ballot)}), // 3days ballot + _metadata, + block.timestamp + 9 days, // Starts 3 days into FC #2 + _groupedSplits, + _fundAccessConstraints, + "" + ); + + // Confirm that this latest reconfiguration implies a rolled over cycle of FC #1. + expectedTimestamp += 1; + JBFundingCycle memory requeued = jbFundingCycleStore().queuedOf(projectId); + + assertEq(requeued.number, 2); + assertEq(requeued.configuration, initialTimestamp); + assertEq(requeued.weight, weightInitial); + + // Warp to when the initial configuration rolls over and again becomes the current + vm.warp(block.timestamp + 6 days); + + // Rolled over configuration + JBFundingCycle memory initialIsCurrent = jbFundingCycleStore().currentOf(projectId); + assertEq(initialIsCurrent.number, 2); + assertEq(initialIsCurrent.configuration, initialTimestamp); + assertEq(initialIsCurrent.weight, weightInitial); + + // Queued second reconfiguration that replaced our first reconfiguration + JBFundingCycle memory requeued2 = jbFundingCycleStore().queuedOf(projectId); + assertEq(requeued2.number, 3); + assertEq(requeued2.configuration, expectedTimestamp); + assertEq(requeued2.weight, weightSecondReconfiguration); + } + function testSingleBlockOverwriteQueued() public { uint256 weightFirstReconfiguration = 1234 * 10 ** 18; uint256 weightSecondReconfiguration = 6969 * 10 ** 18; From 1154b6d9c19b9c4d9fe28d7b285357d92d013fe5 Mon Sep 17 00:00:00 2001 From: Justin Pulley Date: Sun, 1 Oct 2023 11:28:37 -0500 Subject: [PATCH 21/22] test: hacky Controller3_2 TestDelegates test for review --- contracts/JBController.sol | 140 +++++++++++++++++++++++ forge_tests/TestDelegates.sol | 14 ++- forge_tests/helpers/TestBaseWorkflow.sol | 26 ++++- 3 files changed, 170 insertions(+), 10 deletions(-) diff --git a/contracts/JBController.sol b/contracts/JBController.sol index 6b7c180c4..32a4f92aa 100644 --- a/contracts/JBController.sol +++ b/contracts/JBController.sol @@ -31,6 +31,8 @@ import {JBGroupedSplits} from './structs/JBGroupedSplits.sol'; import {JBProjectMetadata} from './structs/JBProjectMetadata.sol'; import {JBSplit} from './structs/JBSplit.sol'; +import {JBFundingCycleConfiguration} from './structs/JBFundingCycleConfiguration.sol'; + /// @notice Stitches together funding cycles and project tokens, making sure all activity is accounted for and correct. contract JBController is JBOperatable, ERC165, IJBController, IJBMigratable { // A library that parses the packed funding cycle metadata into a more friendly format. @@ -352,6 +354,40 @@ contract JBController is JBOperatable, ERC165, IJBController, IJBMigratable { emit LaunchProject(_configuration, projectId, _memo, msg.sender); } + /// @notice Creates a project. This will mint an ERC-721 into the specified owner's account, configure a first funding cycle, and set up any splits. + /// @dev Each operation within this transaction can be done in sequence separately. + /// @dev Anyone can deploy a project on an owner's behalf. + /// @param _owner The address to set as the owner of the project. The project ERC-721 will be owned by this address. + /// @param _projectMetadata Metadata to associate with the project within a particular domain. This can be updated any time by the owner of the project. + /// @param _configurations The funding cycle configurations to schedule. + /// @param _terminals Payment terminals to add for the project. + /// @param _memo A memo to pass along to the emitted event. + /// @return projectId The ID of the project. + function launchProjectFor( + address _owner, + JBProjectMetadata calldata _projectMetadata, + JBFundingCycleConfiguration[] calldata _configurations, + IJBPaymentTerminal[] memory _terminals, + string memory _memo + ) external virtual returns (uint256 projectId) { + // Keep a reference to the directory. + IJBDirectory _directory = directory; + + // Mint the project into the wallet of the owner. + projectId = projects.createFor(_owner, _projectMetadata); + + // Set this contract as the project's controller in the directory. + _directory.setControllerOf(projectId, address(this)); + + // Configure the first funding cycle. + uint256 _configuration = _configure(projectId, _configurations); + + // Add the provided terminals to the list of terminals. + if (_terminals.length > 0) _directory.setTerminalsOf(projectId, _terminals); + + emit LaunchProject(_configuration, projectId, _memo, msg.sender); + } + /// @notice Creates a funding cycle for an already existing project ERC-721. /// @dev Each operation within this transaction can be done in sequence separately. /// @dev Only a project owner or operator can launch its funding cycles. @@ -873,6 +909,110 @@ contract JBController is JBOperatable, ERC165, IJBController, IJBMigratable { return _fundingCycle.configuration; } + /// @notice Configures a funding cycle and stores information pertinent to the configuration. + /// @param _projectId The ID of the project whose funding cycles are being reconfigured. + /// @param _configurations The funding cycle configurations to schedule. + /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. + function _configure( + uint256 _projectId, + JBFundingCycleConfiguration[] calldata _configurations + ) internal returns (uint256 configured) { + // Keep a reference to the configuration being iterated on. + JBFundingCycleConfiguration memory _configuration; + + // Keep a reference to the number of configurations being scheduled. + uint256 _numberOfConfigurations = _configurations.length; + + for (uint256 _i; _i < _numberOfConfigurations; ) { + // Get a reference to the configuration being iterated on. + _configuration = _configurations[_i]; + + // Make sure the provided reserved rate is valid. + if (_configuration.metadata.reservedRate > JBConstants.MAX_RESERVED_RATE) + revert INVALID_RESERVED_RATE(); + + // Make sure the provided redemption rate is valid. + if (_configuration.metadata.redemptionRate > JBConstants.MAX_REDEMPTION_RATE) + revert INVALID_REDEMPTION_RATE(); + + // Make sure the provided ballot redemption rate is valid. + if (_configuration.metadata.ballotRedemptionRate > JBConstants.MAX_REDEMPTION_RATE) + revert INVALID_BALLOT_REDEMPTION_RATE(); + + // Configure the funding cycle's properties. + JBFundingCycle memory _fundingCycle = fundingCycleStore.configureFor( + _projectId, + _configuration.data, + JBFundingCycleMetadataResolver.packFundingCycleMetadata(_configuration.metadata), + _configuration.mustStartAtOrAfter + ); + + // Set splits for the group. + splitsStore.set(_projectId, _fundingCycle.configuration, _configuration.groupedSplits); + + // Set distribution limits if there are any. + for (uint256 i_; i_ < _configuration.fundAccessConstraints.length; ) { + JBFundAccessConstraints memory _constraints = _configuration.fundAccessConstraints[i_]; + + // If distribution limit value is larger than 232 bits, revert. + if (_constraints.distributionLimit > type(uint232).max) revert INVALID_DISTRIBUTION_LIMIT(); + + // If distribution limit currency value is larger than 24 bits, revert. + if (_constraints.distributionLimitCurrency > type(uint24).max) + revert INVALID_DISTRIBUTION_LIMIT_CURRENCY(); + + // If overflow allowance value is larger than 232 bits, revert. + if (_constraints.overflowAllowance > type(uint232).max) revert INVALID_OVERFLOW_ALLOWANCE(); + + // If overflow allowance currency value is larger than 24 bits, revert. + if (_constraints.overflowAllowanceCurrency > type(uint24).max) + revert INVALID_OVERFLOW_ALLOWANCE_CURRENCY(); + + // Set the distribution limit if there is one. + if (_constraints.distributionLimit > 0) + _packedDistributionLimitDataOf[_projectId][_fundingCycle.configuration][ + _constraints.terminal + ][_constraints.token] = + _constraints.distributionLimit | + (_constraints.distributionLimitCurrency << 232); + + // Set the overflow allowance if there is one. + if (_constraints.overflowAllowance > 0) + _packedOverflowAllowanceDataOf[_projectId][_fundingCycle.configuration][ + _constraints.terminal + ][_constraints.token] = + _constraints.overflowAllowance | + (_constraints.overflowAllowanceCurrency << 232); + + emit SetFundAccessConstraints( + _fundingCycle.configuration, + _fundingCycle.number, + _projectId, + _constraints, + msg.sender + ); + + unchecked { + ++i_; + } + } + + /* // Set the funds access constraints. + fundAccessConstraintsStore.setFor( + _projectId, + _fundingCycle.configuration, + _configuration.fundAccessConstraints + ); */ + + // Return the configured timestamp if this is the last configuration being scheduled. + if (_i == _numberOfConfigurations - 1) configured = _fundingCycle.configuration; + + unchecked { + ++_i; + } + } + } + /// @notice Gets the amount of reserved tokens currently tracked for a project given a reserved rate. /// @param _processedTokenTracker The tracker to make the calculation with. /// @param _reservedRate The reserved rate to use to make the calculation. diff --git a/forge_tests/TestDelegates.sol b/forge_tests/TestDelegates.sol index da030edfa..422cd3963 100644 --- a/forge_tests/TestDelegates.sol +++ b/forge_tests/TestDelegates.sol @@ -63,15 +63,19 @@ contract TestDelegates_Local is TestBaseWorkflow { metadata: 0 }); + JBFundingCycleConfiguration[] memory _cycleConfig = new JBFundingCycleConfiguration[](1); + + _cycleConfig[0].mustStartAtOrAfter = 0; + _cycleConfig[0].data = _data; + _cycleConfig[0].metadata = _metadata; + _cycleConfig[0].groupedSplits = _groupedSplits; + _cycleConfig[0].fundAccessConstraints = _fundAccessConstraints; + _terminals.push(jbETHPaymentTerminal()); _projectId = controller.launchProjectFor( _projectOwner, _projectMetadata, - _data, - _metadata, - block.timestamp, - _groupedSplits, - _fundAccessConstraints, + _cycleConfig, _terminals, "" ); diff --git a/forge_tests/helpers/TestBaseWorkflow.sol b/forge_tests/helpers/TestBaseWorkflow.sol index 571064439..498c4fe00 100644 --- a/forge_tests/helpers/TestBaseWorkflow.sol +++ b/forge_tests/helpers/TestBaseWorkflow.sol @@ -11,6 +11,7 @@ import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC16 import {JBController} from "@juicebox/JBController.sol"; import {JBController3_1} from "@juicebox/JBController3_1.sol"; +import {JBController3_2} from "@juicebox/JBController3_2.sol"; import {JBDirectory} from "@juicebox/JBDirectory.sol"; import {JBETHPaymentTerminal} from "@juicebox/JBETHPaymentTerminal.sol"; import {JBETHPaymentTerminal3_1} from "@juicebox/JBETHPaymentTerminal3_1.sol"; @@ -41,6 +42,7 @@ import {JBDidRedeemData} from "@juicebox/structs/JBDidRedeemData.sol"; import {JBFee} from "@juicebox/structs/JBFee.sol"; import {JBFundAccessConstraints} from "@juicebox/structs/JBFundAccessConstraints.sol"; import {JBFundingCycle} from "@juicebox/structs/JBFundingCycle.sol"; +import {JBFundingCycleConfiguration} from '@juicebox/structs/JBFundingCycleConfiguration.sol'; import {JBFundingCycleData} from "@juicebox/structs/JBFundingCycleData.sol"; import {JBFundingCycleMetadata} from "@juicebox/structs/JBFundingCycleMetadata.sol"; import {JBGroupedSplits} from "@juicebox/structs/JBGroupedSplits.sol"; @@ -134,6 +136,7 @@ contract TestBaseWorkflow is Test { // JBController(s) JBController internal _jbController; JBController3_1 internal _jbController3_1; + JBController3_2 internal _jbController3_2; JBFundAccessConstraintsStore internal _jbFundAccessConstraintsStore; @@ -189,12 +192,12 @@ contract TestBaseWorkflow is Test { function jbController() internal returns (JBController) { // If no controller is specified we use the newest one by default - string memory controller = vm.envOr("JBX_CONTROLLER_VERSION", string("3_1")); + string memory controller = vm.envOr("JBX_CONTROLLER_VERSION", string("3_2")); - if (strEqual(controller, "3_1")) { + if (strEqual(controller, "3_2")) { + return JBController(address(_jbController3_2)); + } else if (strEqual(controller, "3_1")) { return JBController(address(_jbController3_1)); - } else if (strEqual(controller, "3_0")) { - return _jbController; } else { revert("Invalid 'JBX_CONTROLLER_VERSION' specified"); } @@ -308,8 +311,21 @@ contract TestBaseWorkflow is Test { ); vm.label(address(_jbController3_1), "JBController3_1"); - vm.prank(_multisig); + _jbController3_2 = new JBController3_2( + _jbOperatorStore, + _jbProjects, + _jbDirectory, + _jbFundingCycleStore, + _jbTokenStore, + _jbSplitsStore, + _jbFundAccessConstraintsStore + ); + vm.label(address(_jbController3_2), "JBController3_2"); + + vm.startPrank(_multisig); _jbDirectory.setIsAllowedToSetFirstController(address(_jbController3_1), true); + _jbDirectory.setIsAllowedToSetFirstController(address(_jbController3_2), true); + vm.stopPrank(); // JBETHPaymentTerminalStore _jbPaymentTerminalStore = new JBSingleTokenPaymentTerminalStore( From fb85503277b82e03a564ac9b028e6451b3351108 Mon Sep 17 00:00:00 2001 From: Justin Pulley Date: Wed, 4 Oct 2023 12:10:39 -0500 Subject: [PATCH 22/22] Revert "test: hacky Controller3_2 TestDelegates test for review" This reverts commit 1154b6d9c19b9c4d9fe28d7b285357d92d013fe5. --- contracts/JBController.sol | 140 ----------------------- forge_tests/TestDelegates.sol | 14 +-- forge_tests/helpers/TestBaseWorkflow.sol | 26 +---- 3 files changed, 10 insertions(+), 170 deletions(-) diff --git a/contracts/JBController.sol b/contracts/JBController.sol index 32a4f92aa..6b7c180c4 100644 --- a/contracts/JBController.sol +++ b/contracts/JBController.sol @@ -31,8 +31,6 @@ import {JBGroupedSplits} from './structs/JBGroupedSplits.sol'; import {JBProjectMetadata} from './structs/JBProjectMetadata.sol'; import {JBSplit} from './structs/JBSplit.sol'; -import {JBFundingCycleConfiguration} from './structs/JBFundingCycleConfiguration.sol'; - /// @notice Stitches together funding cycles and project tokens, making sure all activity is accounted for and correct. contract JBController is JBOperatable, ERC165, IJBController, IJBMigratable { // A library that parses the packed funding cycle metadata into a more friendly format. @@ -354,40 +352,6 @@ contract JBController is JBOperatable, ERC165, IJBController, IJBMigratable { emit LaunchProject(_configuration, projectId, _memo, msg.sender); } - /// @notice Creates a project. This will mint an ERC-721 into the specified owner's account, configure a first funding cycle, and set up any splits. - /// @dev Each operation within this transaction can be done in sequence separately. - /// @dev Anyone can deploy a project on an owner's behalf. - /// @param _owner The address to set as the owner of the project. The project ERC-721 will be owned by this address. - /// @param _projectMetadata Metadata to associate with the project within a particular domain. This can be updated any time by the owner of the project. - /// @param _configurations The funding cycle configurations to schedule. - /// @param _terminals Payment terminals to add for the project. - /// @param _memo A memo to pass along to the emitted event. - /// @return projectId The ID of the project. - function launchProjectFor( - address _owner, - JBProjectMetadata calldata _projectMetadata, - JBFundingCycleConfiguration[] calldata _configurations, - IJBPaymentTerminal[] memory _terminals, - string memory _memo - ) external virtual returns (uint256 projectId) { - // Keep a reference to the directory. - IJBDirectory _directory = directory; - - // Mint the project into the wallet of the owner. - projectId = projects.createFor(_owner, _projectMetadata); - - // Set this contract as the project's controller in the directory. - _directory.setControllerOf(projectId, address(this)); - - // Configure the first funding cycle. - uint256 _configuration = _configure(projectId, _configurations); - - // Add the provided terminals to the list of terminals. - if (_terminals.length > 0) _directory.setTerminalsOf(projectId, _terminals); - - emit LaunchProject(_configuration, projectId, _memo, msg.sender); - } - /// @notice Creates a funding cycle for an already existing project ERC-721. /// @dev Each operation within this transaction can be done in sequence separately. /// @dev Only a project owner or operator can launch its funding cycles. @@ -909,110 +873,6 @@ contract JBController is JBOperatable, ERC165, IJBController, IJBMigratable { return _fundingCycle.configuration; } - /// @notice Configures a funding cycle and stores information pertinent to the configuration. - /// @param _projectId The ID of the project whose funding cycles are being reconfigured. - /// @param _configurations The funding cycle configurations to schedule. - /// @return configured The configuration timestamp of the funding cycle that was successfully reconfigured. - function _configure( - uint256 _projectId, - JBFundingCycleConfiguration[] calldata _configurations - ) internal returns (uint256 configured) { - // Keep a reference to the configuration being iterated on. - JBFundingCycleConfiguration memory _configuration; - - // Keep a reference to the number of configurations being scheduled. - uint256 _numberOfConfigurations = _configurations.length; - - for (uint256 _i; _i < _numberOfConfigurations; ) { - // Get a reference to the configuration being iterated on. - _configuration = _configurations[_i]; - - // Make sure the provided reserved rate is valid. - if (_configuration.metadata.reservedRate > JBConstants.MAX_RESERVED_RATE) - revert INVALID_RESERVED_RATE(); - - // Make sure the provided redemption rate is valid. - if (_configuration.metadata.redemptionRate > JBConstants.MAX_REDEMPTION_RATE) - revert INVALID_REDEMPTION_RATE(); - - // Make sure the provided ballot redemption rate is valid. - if (_configuration.metadata.ballotRedemptionRate > JBConstants.MAX_REDEMPTION_RATE) - revert INVALID_BALLOT_REDEMPTION_RATE(); - - // Configure the funding cycle's properties. - JBFundingCycle memory _fundingCycle = fundingCycleStore.configureFor( - _projectId, - _configuration.data, - JBFundingCycleMetadataResolver.packFundingCycleMetadata(_configuration.metadata), - _configuration.mustStartAtOrAfter - ); - - // Set splits for the group. - splitsStore.set(_projectId, _fundingCycle.configuration, _configuration.groupedSplits); - - // Set distribution limits if there are any. - for (uint256 i_; i_ < _configuration.fundAccessConstraints.length; ) { - JBFundAccessConstraints memory _constraints = _configuration.fundAccessConstraints[i_]; - - // If distribution limit value is larger than 232 bits, revert. - if (_constraints.distributionLimit > type(uint232).max) revert INVALID_DISTRIBUTION_LIMIT(); - - // If distribution limit currency value is larger than 24 bits, revert. - if (_constraints.distributionLimitCurrency > type(uint24).max) - revert INVALID_DISTRIBUTION_LIMIT_CURRENCY(); - - // If overflow allowance value is larger than 232 bits, revert. - if (_constraints.overflowAllowance > type(uint232).max) revert INVALID_OVERFLOW_ALLOWANCE(); - - // If overflow allowance currency value is larger than 24 bits, revert. - if (_constraints.overflowAllowanceCurrency > type(uint24).max) - revert INVALID_OVERFLOW_ALLOWANCE_CURRENCY(); - - // Set the distribution limit if there is one. - if (_constraints.distributionLimit > 0) - _packedDistributionLimitDataOf[_projectId][_fundingCycle.configuration][ - _constraints.terminal - ][_constraints.token] = - _constraints.distributionLimit | - (_constraints.distributionLimitCurrency << 232); - - // Set the overflow allowance if there is one. - if (_constraints.overflowAllowance > 0) - _packedOverflowAllowanceDataOf[_projectId][_fundingCycle.configuration][ - _constraints.terminal - ][_constraints.token] = - _constraints.overflowAllowance | - (_constraints.overflowAllowanceCurrency << 232); - - emit SetFundAccessConstraints( - _fundingCycle.configuration, - _fundingCycle.number, - _projectId, - _constraints, - msg.sender - ); - - unchecked { - ++i_; - } - } - - /* // Set the funds access constraints. - fundAccessConstraintsStore.setFor( - _projectId, - _fundingCycle.configuration, - _configuration.fundAccessConstraints - ); */ - - // Return the configured timestamp if this is the last configuration being scheduled. - if (_i == _numberOfConfigurations - 1) configured = _fundingCycle.configuration; - - unchecked { - ++_i; - } - } - } - /// @notice Gets the amount of reserved tokens currently tracked for a project given a reserved rate. /// @param _processedTokenTracker The tracker to make the calculation with. /// @param _reservedRate The reserved rate to use to make the calculation. diff --git a/forge_tests/TestDelegates.sol b/forge_tests/TestDelegates.sol index 422cd3963..da030edfa 100644 --- a/forge_tests/TestDelegates.sol +++ b/forge_tests/TestDelegates.sol @@ -63,19 +63,15 @@ contract TestDelegates_Local is TestBaseWorkflow { metadata: 0 }); - JBFundingCycleConfiguration[] memory _cycleConfig = new JBFundingCycleConfiguration[](1); - - _cycleConfig[0].mustStartAtOrAfter = 0; - _cycleConfig[0].data = _data; - _cycleConfig[0].metadata = _metadata; - _cycleConfig[0].groupedSplits = _groupedSplits; - _cycleConfig[0].fundAccessConstraints = _fundAccessConstraints; - _terminals.push(jbETHPaymentTerminal()); _projectId = controller.launchProjectFor( _projectOwner, _projectMetadata, - _cycleConfig, + _data, + _metadata, + block.timestamp, + _groupedSplits, + _fundAccessConstraints, _terminals, "" ); diff --git a/forge_tests/helpers/TestBaseWorkflow.sol b/forge_tests/helpers/TestBaseWorkflow.sol index 498c4fe00..571064439 100644 --- a/forge_tests/helpers/TestBaseWorkflow.sol +++ b/forge_tests/helpers/TestBaseWorkflow.sol @@ -11,7 +11,6 @@ import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC16 import {JBController} from "@juicebox/JBController.sol"; import {JBController3_1} from "@juicebox/JBController3_1.sol"; -import {JBController3_2} from "@juicebox/JBController3_2.sol"; import {JBDirectory} from "@juicebox/JBDirectory.sol"; import {JBETHPaymentTerminal} from "@juicebox/JBETHPaymentTerminal.sol"; import {JBETHPaymentTerminal3_1} from "@juicebox/JBETHPaymentTerminal3_1.sol"; @@ -42,7 +41,6 @@ import {JBDidRedeemData} from "@juicebox/structs/JBDidRedeemData.sol"; import {JBFee} from "@juicebox/structs/JBFee.sol"; import {JBFundAccessConstraints} from "@juicebox/structs/JBFundAccessConstraints.sol"; import {JBFundingCycle} from "@juicebox/structs/JBFundingCycle.sol"; -import {JBFundingCycleConfiguration} from '@juicebox/structs/JBFundingCycleConfiguration.sol'; import {JBFundingCycleData} from "@juicebox/structs/JBFundingCycleData.sol"; import {JBFundingCycleMetadata} from "@juicebox/structs/JBFundingCycleMetadata.sol"; import {JBGroupedSplits} from "@juicebox/structs/JBGroupedSplits.sol"; @@ -136,7 +134,6 @@ contract TestBaseWorkflow is Test { // JBController(s) JBController internal _jbController; JBController3_1 internal _jbController3_1; - JBController3_2 internal _jbController3_2; JBFundAccessConstraintsStore internal _jbFundAccessConstraintsStore; @@ -192,12 +189,12 @@ contract TestBaseWorkflow is Test { function jbController() internal returns (JBController) { // If no controller is specified we use the newest one by default - string memory controller = vm.envOr("JBX_CONTROLLER_VERSION", string("3_2")); + string memory controller = vm.envOr("JBX_CONTROLLER_VERSION", string("3_1")); - if (strEqual(controller, "3_2")) { - return JBController(address(_jbController3_2)); - } else if (strEqual(controller, "3_1")) { + if (strEqual(controller, "3_1")) { return JBController(address(_jbController3_1)); + } else if (strEqual(controller, "3_0")) { + return _jbController; } else { revert("Invalid 'JBX_CONTROLLER_VERSION' specified"); } @@ -311,21 +308,8 @@ contract TestBaseWorkflow is Test { ); vm.label(address(_jbController3_1), "JBController3_1"); - _jbController3_2 = new JBController3_2( - _jbOperatorStore, - _jbProjects, - _jbDirectory, - _jbFundingCycleStore, - _jbTokenStore, - _jbSplitsStore, - _jbFundAccessConstraintsStore - ); - vm.label(address(_jbController3_2), "JBController3_2"); - - vm.startPrank(_multisig); + vm.prank(_multisig); _jbDirectory.setIsAllowedToSetFirstController(address(_jbController3_1), true); - _jbDirectory.setIsAllowedToSetFirstController(address(_jbController3_2), true); - vm.stopPrank(); // JBETHPaymentTerminalStore _jbPaymentTerminalStore = new JBSingleTokenPaymentTerminalStore(