diff --git a/script/deploy/DeployAuctionStateLens.s.sol b/script/deploy/DeployAuctionStateLens.s.sol deleted file mode 100644 index 4a078651..00000000 --- a/script/deploy/DeployAuctionStateLens.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {IContinuousClearingAuction} from '../../src/interfaces/IContinuousClearingAuction.sol'; -import {AuctionStateLens} from '../../src/lens/AuctionStateLens.sol'; -import 'forge-std/Script.sol'; -import 'forge-std/console2.sol'; - -contract DeployAuctionStateLensMainnet is Script { - function run() public returns (address lens) { - vm.startBroadcast(); - - lens = address(new AuctionStateLens{salt: bytes32(0)}()); - console2.log('AuctionStateLens deployed to:', address(lens)); - vm.stopBroadcast(); - } -} diff --git a/script/deploy/DeployCCALens.s.sol b/script/deploy/DeployCCALens.s.sol new file mode 100644 index 00000000..1fb7c8c7 --- /dev/null +++ b/script/deploy/DeployCCALens.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {CCALens} from '../../src/lens/CCALens.sol'; +import 'forge-std/Script.sol'; +import 'forge-std/console2.sol'; + +contract DeployCCALensScript is Script { + function run() public returns (address lens) { + vm.startBroadcast(); + + lens = address(new CCALens{salt: bytes32(0)}()); + console2.log('CCALens deployed to:', address(lens)); + vm.stopBroadcast(); + } +} diff --git a/snapshots/TickDataLensTest.json b/snapshots/TickDataLensTest.json new file mode 100644 index 00000000..3d840161 --- /dev/null +++ b/snapshots/TickDataLensTest.json @@ -0,0 +1,3 @@ +{ + "getInitializedTickData max buffer size": "5796546" +} \ No newline at end of file diff --git a/src/ContinuousClearingAuction.sol b/src/ContinuousClearingAuction.sol index d8da392c..13462f83 100644 --- a/src/ContinuousClearingAuction.sol +++ b/src/ContinuousClearingAuction.sol @@ -38,7 +38,6 @@ contract ContinuousClearingAuction is StepStorage, TickStorage, TokenCurrencyStorage, - BlockNumberish, ReentrancyGuardTransient, IContinuousClearingAuction { @@ -53,8 +52,6 @@ contract ContinuousClearingAuction is /// @notice The maximum price which a bid can be submitted at /// @dev Set during construction using MaxBidPriceLib.maxBidPrice() based on TOTAL_SUPPLY uint256 public immutable MAX_BID_PRICE; - /// @notice The block at which purchased tokens can be claimed - uint64 internal immutable CLAIM_BLOCK; /// @notice An optional hook to be called before a bid is registered IValidationHook internal immutable VALIDATION_HOOK; @@ -73,7 +70,7 @@ contract ContinuousClearingAuction is bool private $_tokensReceived; constructor(address _token, uint128 _totalSupply, AuctionParameters memory _parameters) - StepStorage(_parameters.auctionStepsData, _parameters.startBlock, _parameters.endBlock) + StepStorage(_parameters.auctionStepsData, _parameters.startBlock, _parameters.endBlock, _parameters.claimBlock) TokenCurrencyStorage( _token, _parameters.currency, @@ -84,11 +81,8 @@ contract ContinuousClearingAuction is ) TickStorage(_parameters.tickSpacing, _parameters.floorPrice) { - CLAIM_BLOCK = _parameters.claimBlock; VALIDATION_HOOK = IValidationHook(_parameters.validationHook); - if (CLAIM_BLOCK < END_BLOCK) revert ClaimBlockIsBeforeEndBlock(); - // See MaxBidPriceLib library for more details on the bid price calculations. MAX_BID_PRICE = MaxBidPriceLib.maxBidPrice(TOTAL_SUPPLY); // The floor price and tick spacing must allow for at least one tick above the floor price to be initialized @@ -103,18 +97,6 @@ contract ContinuousClearingAuction is emit ClearingPriceUpdated(_getBlockNumberish(), $clearingPrice); } - /// @notice Modifier for functions which can only be called after the auction is over - modifier onlyAfterAuctionIsOver() { - if (_getBlockNumberish() < END_BLOCK) revert AuctionIsNotOver(); - _; - } - - /// @notice Modifier for claim related functions which can only be called after the claim block - modifier onlyAfterClaimBlock() { - if (_getBlockNumberish() < CLAIM_BLOCK) revert NotClaimable(); - _; - } - /// @notice Modifier for functions which can only be called after the auction is started and the tokens have been received modifier onlyActiveAuction() { _onlyActiveAuction(); @@ -281,35 +263,6 @@ contract ContinuousClearingAuction is return _checkpoint; } - /// @notice Fast forward to the start of the current step and return the number of `mps` sold since the last checkpoint - /// @param _blockNumber The current block number - /// @param _lastCheckpointedBlock The block number of the last checkpointed block - /// @return step The current step in the auction which contains `_blockNumber` - /// @return deltaMps The number of `mps` sold between the last checkpointed block and the start of the current step - function _advanceToStartOfCurrentStep(uint64 _blockNumber, uint64 _lastCheckpointedBlock) - internal - returns (AuctionStep memory step, uint24 deltaMps) - { - // Advance the current step until the current block is within the step - // Start at the larger of the last checkpointed block or the start block of the current step - step = $step; - uint64 start = uint64(FixedPointMathLib.max(step.startBlock, _lastCheckpointedBlock)); - uint64 end = step.endBlock; - - uint24 mps = step.mps; - while (_blockNumber > end) { - uint64 blockDelta = end - start; - unchecked { - deltaMps += uint24(blockDelta * mps); - } - start = end; - if (end == END_BLOCK) break; - step = _advanceStep(); - mps = step.mps; - end = step.endBlock; - } - } - /// @notice Iterate to find the tick where the total demand at and above it is strictly less than the remaining supply in the auction /// @dev If the loop reaches the highest tick in the book, `nextActiveTickPrice` will be set to MAX_TICK_PTR /// @param _untilTickPrice The tick price to iterate until diff --git a/src/StepStorage.sol b/src/StepStorage.sol index 686d9591..69277404 100644 --- a/src/StepStorage.sol +++ b/src/StepStorage.sol @@ -4,11 +4,13 @@ pragma solidity 0.8.26; import {IStepStorage} from './interfaces/IStepStorage.sol'; import {ConstantsLib} from './libraries/ConstantsLib.sol'; import {AuctionStep, StepLib} from './libraries/StepLib.sol'; +import {BlockNumberish} from 'blocknumberish/src/BlockNumberish.sol'; +import {FixedPointMathLib} from 'solady/utils/FixedPointMathLib.sol'; import {SSTORE2} from 'solady/utils/SSTORE2.sol'; /// @title StepStorage /// @notice Abstract contract to store and read information about the auction issuance schedule -abstract contract StepStorage is IStepStorage { +abstract contract StepStorage is BlockNumberish, IStepStorage { using StepLib for *; using SSTORE2 for *; @@ -16,6 +18,8 @@ abstract contract StepStorage is IStepStorage { uint64 internal immutable START_BLOCK; /// @notice The block at which the auction ends uint64 internal immutable END_BLOCK; + /// @notice The block at which purchased tokens can be claimed + uint64 internal immutable CLAIM_BLOCK; /// @notice Cached length of the auction steps data provided in the constructor uint256 internal immutable _LENGTH; @@ -26,10 +30,13 @@ abstract contract StepStorage is IStepStorage { /// @notice The current active auction step AuctionStep internal $step; - constructor(bytes memory _auctionStepsData, uint64 _startBlock, uint64 _endBlock) { + constructor(bytes memory _auctionStepsData, uint64 _startBlock, uint64 _endBlock, uint64 _claimBlock) { if (_startBlock >= _endBlock) revert InvalidEndBlock(); + if (_claimBlock < _endBlock) revert ClaimBlockIsBeforeEndBlock(); + START_BLOCK = _startBlock; END_BLOCK = _endBlock; + CLAIM_BLOCK = _claimBlock; _LENGTH = _auctionStepsData.length; address _pointer = _auctionStepsData.write(); @@ -39,6 +46,47 @@ abstract contract StepStorage is IStepStorage { _advanceStep(); } + /// @notice Modifier for functions which can only be called after the auction is over + modifier onlyAfterAuctionIsOver() { + if (_getBlockNumberish() < END_BLOCK) revert AuctionIsNotOver(); + _; + } + + /// @notice Modifier for claim related functions which can only be called after the claim block + modifier onlyAfterClaimBlock() { + if (_getBlockNumberish() < CLAIM_BLOCK) revert NotClaimable(); + _; + } + + /// @notice Fast forward to the start of the current step and return the number of `mps` sold since the last checkpoint + /// @param _blockNumber The current block number + /// @param _lastCheckpointedBlock The block number of the last checkpointed block + /// @return _step The current step in the auction which contains `_blockNumber` + /// @return deltaMps The number of `mps` sold between the last checkpointed block and the start of the current step + function _advanceToStartOfCurrentStep(uint64 _blockNumber, uint64 _lastCheckpointedBlock) + internal + returns (AuctionStep memory _step, uint24 deltaMps) + { + // Advance the current step until the current block is within the step + // Start at the larger of the last checkpointed block or the start block of the current step + _step = $step; + uint64 start = uint64(FixedPointMathLib.max(_step.startBlock, _lastCheckpointedBlock)); + uint64 end = _step.endBlock; + + uint24 mps = _step.mps; + while (_blockNumber > end) { + uint64 blockDelta = end - start; + unchecked { + deltaMps += uint24(blockDelta * mps); + } + start = end; + if (end == END_BLOCK) break; + _step = _advanceStep(); + mps = _step.mps; + end = _step.endBlock; + } + } + /// @notice Validate the data provided in the constructor /// @dev Checks that the contract was correctly deployed by SSTORE2 and that the total mps and blocks are valid function _validate(address _pointer) internal view { diff --git a/src/interfaces/IContinuousClearingAuction.sol b/src/interfaces/IContinuousClearingAuction.sol index 81367ff5..ceb96c56 100644 --- a/src/interfaces/IContinuousClearingAuction.sol +++ b/src/interfaces/IContinuousClearingAuction.sol @@ -58,8 +58,6 @@ interface IContinuousClearingAuction is error AuctionNotStarted(); /// @notice Error thrown when the tokens required for the auction have not been received error TokensNotReceived(); - /// @notice Error thrown when the claim block is before the end block - error ClaimBlockIsBeforeEndBlock(); /// @notice Error thrown when the floor price plus tick spacing is greater than the maximum bid price error FloorPriceAndTickSpacingGreaterThanMaxBidPrice(uint256 nextTick, uint256 maxBidPrice); /// @notice Error thrown when the floor price plus tick spacing would overflow a uint256 @@ -74,8 +72,6 @@ interface IContinuousClearingAuction is error InvalidLastFullyFilledCheckpointHint(); /// @notice Error thrown when the outbid block checkpoint hint is invalid error InvalidOutbidBlockCheckpointHint(); - /// @notice Error thrown when the bid is not claimable - error NotClaimable(); /// @notice Error thrown when the bids are not owned by the same owner error BatchClaimDifferentOwner(address expectedOwner, address receivedOwner); /// @notice Error thrown when the bid has not been exited @@ -84,8 +80,6 @@ interface IContinuousClearingAuction is error CannotPartiallyExitBidBeforeGraduation(); /// @notice Error thrown when the token transfer fails error TokenTransferFailed(); - /// @notice Error thrown when the auction is not over - error AuctionIsNotOver(); /// @notice Error thrown when the end block is not checkpointed error AuctionIsNotFinalized(); /// @notice Error thrown when the bid is too large diff --git a/src/interfaces/IStepStorage.sol b/src/interfaces/IStepStorage.sol index 49c5c979..f4843252 100644 --- a/src/interfaces/IStepStorage.sol +++ b/src/interfaces/IStepStorage.sol @@ -7,8 +7,14 @@ import {AuctionStep} from '../libraries/StepLib.sol'; interface IStepStorage { /// @notice Error thrown when the end block is equal to or before the start block error InvalidEndBlock(); + /// @notice Error thrown when the claim block is before the end block + error ClaimBlockIsBeforeEndBlock(); /// @notice Error thrown when the auction is over error AuctionIsOver(); + /// @notice Error thrown when the auction is not over + error AuctionIsNotOver(); + /// @notice Error thrown when the bid is not claimable + error NotClaimable(); /// @notice Error thrown when the auction data length is invalid error InvalidAuctionDataLength(); /// @notice Error thrown when the block delta in a step is zero diff --git a/src/lens/AuctionStateLens.sol b/src/lens/AuctionStateLens.sol index 63211749..eb98aa71 100644 --- a/src/lens/AuctionStateLens.sol +++ b/src/lens/AuctionStateLens.sol @@ -22,7 +22,7 @@ contract AuctionStateLens { error InvalidRevertReasonLength(); /// @notice Function which can be called from offchain to get the latest state of the auction - function state(IContinuousClearingAuction auction) external returns (AuctionState memory) { + function state(IContinuousClearingAuction auction) public returns (AuctionState memory) { try this.revertWithState(auction) {} catch (bytes memory reason) { return parseRevertReason(reason); @@ -30,7 +30,7 @@ contract AuctionStateLens { } /// @notice Function which checkpoints the auction, gets global values and encodes them into a revert string - function revertWithState(IContinuousClearingAuction auction) external { + function revertWithState(IContinuousClearingAuction auction) public { try auction.checkpoint() returns (Checkpoint memory checkpoint) { AuctionState memory _state = AuctionState({ checkpoint: checkpoint, diff --git a/src/lens/CCALens.sol b/src/lens/CCALens.sol new file mode 100644 index 00000000..57500ef0 --- /dev/null +++ b/src/lens/CCALens.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {AuctionStateLens} from './AuctionStateLens.sol'; +import {TickDataLens} from './TickDataLens.sol'; +import {Multicallable} from 'solady/utils/Multicallable.sol'; + +/// @title CCALens +/// @notice Lens contract for reading data from deployed CCA auctions +contract CCALens is Multicallable, AuctionStateLens, TickDataLens {} diff --git a/src/lens/TickDataLens.sol b/src/lens/TickDataLens.sol new file mode 100644 index 00000000..7ffa60cc --- /dev/null +++ b/src/lens/TickDataLens.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {TickStorage} from '../TickStorage.sol'; +import {IContinuousClearingAuction} from '../interfaces/IContinuousClearingAuction.sol'; +import {Tick} from '../interfaces/ITickStorage.sol'; +import {ConstantsLib} from '../libraries/ConstantsLib.sol'; +import {AuctionState} from './AuctionStateLens.sol'; +import {FixedPointMathLib} from 'solady/utils/FixedPointMathLib.sol'; + +/// @notice Tick data with additional computed values +/// @dev Use the ratio between `sumCurrencyDemandAboveClearingQ96` in the auction +/// and `requiredCurrencyDemandQ96` to calculate the progress towards the tick +/// Use `currencyRequiredQ96` to calculate the additional currency needed to move the clearing price to the tick. +struct TickWithData { + uint256 priceQ96; // the price of the tick + uint256 currencyDemandQ96; // the current demand at the tick + uint256 requiredCurrencyDemandQ96; // the required demand to move the clearing price to the tick + uint256 currencyRequiredQ96; // the additional currency required to move the clearing price to the tick +} + +/// @title TickDataLens +/// @notice Contract for reading data from initialized ticks of an auction +contract TickDataLens { + using FixedPointMathLib for *; + + /// @notice The maximum number of initialized ticks which can be returned + uint256 public constant MAX_BUFFER_SIZE = 1000; + + /// @notice Function to be called from offchain to get the data of all initialized ticks above a given price + /// @dev A maximum of `MAX_BUFFER_SIZE` ticks above the current clearing price will be returned + /// Returned values may be stale if the auction has not been recently checkpointed + function getInitializedTickData(IContinuousClearingAuction auction) + public + view + returns (TickWithData[] memory ticks) + { + uint256 totalSupply = auction.totalSupply(); + uint24 mps = ConstantsLib.MPS; + uint24 remainingMps = mps - auction.latestCheckpoint().cumulativeMps; + // Retrieve the sumCurrencyDemandAboveClearingQ96 from storage + uint256 sumCurrencyDemandAboveClearingQ96 = auction.sumCurrencyDemandAboveClearingQ96(); + // Get the next active tick price + uint256 next = auction.nextActiveTickPrice(); + + assembly { + let m := mload(0x40) + let offset := m + + // Cache the ticks function selector + let idx := 0 + mstore(0x00, 0x534cb30d) // ContinuousClearingAuction.ticks(uint256) + for {} lt(idx, MAX_BUFFER_SIZE) { idx := add(idx, 1) } { + // Store the next tick price as the first argument to the ticks function call + mstore(0x20, next) + // Call the ticks function - write the return value directly at the offset + let success := staticcall(gas(), auction, 0x1c, 0x24, offset, 0x40) + // If the call fails, revert + if iszero(success) { + revert(0x00, 0x00) + } + // Load the next tick price from the return value + let nextTick := mload(offset) + // Overwrite the next tick price with the current tick price + mstore(offset, next) + + // Calculate and store the requiredCurrencyDemandQ96 + let requiredCurrencyDemandQ96 := mul(totalSupply, next) + mstore(add(offset, 0x40), requiredCurrencyDemandQ96) + + // Calculate and store the currencyRequiredQ96: + // currencyRequiredQ96 = saturatingSub(required, sum).mulDivUp(remainingMps, MPS) + let currencyRequiredQ96 := + mulDivUp( + saturatingSub(requiredCurrencyDemandQ96, sumCurrencyDemandAboveClearingQ96), + remainingMps, + mps + ) + mstore(add(offset, 0x60), currencyRequiredQ96) + + // update sumCurrencyDemandAboveClearingQ96 by subtracting the currencyDemandQ96 at the current tick + let currencyDemandQ96 := mload(add(offset, 0x20)) + sumCurrencyDemandAboveClearingQ96 := sub(sumCurrencyDemandAboveClearingQ96, currencyDemandQ96) + + // update next to the next tick price returned by the ticks function + next := nextTick + + // update offset to the next tick data + offset := add(offset, 0x80) + + // If the next tick price is the maximum uint256 value or we have reached the maximum buffer size, break the loop + if eq(nextTick, not(0)) { + // Update idx to the number of ticks returned + idx := add(idx, 1) + break + } + } + + // Assign the return value to the right memory location + ticks := offset + // Assign the length of the TickWithData array to the first word of the return value + mstore(ticks, idx) + // Assign the absolute pointers after the length to the return value + for { let i := 0 } lt(i, idx) { i := add(i, 1) } { + offset := add(offset, 0x20) + let absolutePointer := add(m, mul(i, 0x80)) + mstore(offset, absolutePointer) + } + // Update the free memory pointer to the end of the data + mstore(0x40, add(offset, 0x20)) + + // ----- Inline functions ----- + // ------------------------------------------------------------ + + /// @dev Equivalent to FixedPointMathLib.saturatingSub: returns max(0, x - y). + function saturatingSub(x, y) -> z { + z := mul(gt(x, y), sub(x, y)) + } + + /// @dev Equivalent to FixedPointMathLib.fullMulDivUp: returns ceil(x * y / d). + /// Safe to use a plain `mul` instead of 512-bit math because `y` is + /// `remainingMps` (a uint24, max 1e7 ≈ 2^23), so x * y cannot overflow + /// uint256 for any realistic currency demand value. + function mulDivUp(x, y, d) -> result { + result := div(mul(x, y), d) + if mulmod(x, y, d) { + result := add(result, 1) + } + } + + // ------------------------------------------------------------ + } + } +} diff --git a/test/Auction.graduation.t.sol b/test/Auction.graduation.t.sol index e441951e..73b53f9f 100644 --- a/test/Auction.graduation.t.sol +++ b/test/Auction.graduation.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {IContinuousClearingAuction} from '../src/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from '../src/interfaces/IStepStorage.sol'; import {ITokenCurrencyStorage} from '../src/interfaces/ITokenCurrencyStorage.sol'; import {Bid, BidLib} from '../src/libraries/BidLib.sol'; import {CheckpointAccountingLib} from '../src/libraries/CheckpointAccountingLib.sol'; @@ -300,7 +301,7 @@ contract AuctionGraduationTest is AuctionBaseTest { vm.roll(auction.claimBlock() - 1); // Try to claim tokens before the claim block - vm.expectRevert(IContinuousClearingAuction.NotClaimable.selector); + vm.expectRevert(IStepStorage.NotClaimable.selector); auction.claimTokensBatch(alice, bids); } diff --git a/test/Auction.t.sol b/test/Auction.t.sol index fb71013e..7bb1080b 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -573,7 +573,7 @@ contract AuctionTest is AuctionBaseTest { // Before the auction ends, the bid should not be exitable since it is at the clearing price vm.roll(auction.endBlock() - 1); if ($maxPrice > checkpoint.clearingPrice) { - vm.expectRevert(IContinuousClearingAuction.AuctionIsNotOver.selector); + vm.expectRevert(IStepStorage.AuctionIsNotOver.selector); auction.exitBid(bidId); } else { vm.expectRevert(IContinuousClearingAuction.CannotPartiallyExitBidBeforeEndBlock.selector); @@ -1634,7 +1634,7 @@ contract AuctionTest is AuctionBaseTest { function test_auctionConstruction_revertsWithClaimBlockBeforeEndBlock() public { AuctionParameters memory paramsClaimBlockBeforeEndBlock = params.withClaimBlock(block.number + AUCTION_DURATION - 1).withEndBlock(block.number + AUCTION_DURATION); - vm.expectRevert(IContinuousClearingAuction.ClaimBlockIsBeforeEndBlock.selector); + vm.expectRevert(IStepStorage.ClaimBlockIsBeforeEndBlock.selector); new ContinuousClearingAuction(address(token), TOTAL_SUPPLY, paramsClaimBlockBeforeEndBlock); } @@ -1854,7 +1854,7 @@ contract AuctionTest is AuctionBaseTest { vm.roll(auction.claimBlock() - 1); // Try to claim tokens before the claim block - vm.expectRevert(IContinuousClearingAuction.NotClaimable.selector); + vm.expectRevert(IStepStorage.NotClaimable.selector); auction.claimTokens(bidId); } @@ -1959,7 +1959,7 @@ contract AuctionTest is AuctionBaseTest { } vm.roll(auction.claimBlock() - 1); - vm.expectRevert(IContinuousClearingAuction.NotClaimable.selector); + vm.expectRevert(IStepStorage.NotClaimable.selector); auction.claimTokensBatch(alice, bids); } @@ -2048,14 +2048,14 @@ contract AuctionTest is AuctionBaseTest { function test_sweepCurrency_beforeAuctionEnds_reverts() public { vm.startPrank(auction.fundsRecipient()); vm.roll(auction.endBlock() - 1); - vm.expectRevert(IContinuousClearingAuction.AuctionIsNotOver.selector); + vm.expectRevert(IStepStorage.AuctionIsNotOver.selector); auction.sweepCurrency(); vm.stopPrank(); } function test_sweepUnsoldTokens_beforeAuctionEnds_reverts() public { vm.roll(auction.endBlock() - 1); - vm.expectRevert(IContinuousClearingAuction.AuctionIsNotOver.selector); + vm.expectRevert(IStepStorage.AuctionIsNotOver.selector); auction.sweepUnsoldTokens(); } diff --git a/test/AuctionFactory.t.sol b/test/AuctionFactory.t.sol index 4c8fb2bf..6b583d7a 100644 --- a/test/AuctionFactory.t.sol +++ b/test/AuctionFactory.t.sol @@ -5,6 +5,7 @@ import {AuctionParameters, ContinuousClearingAuction} from '../src/ContinuousCle import {ContinuousClearingAuctionFactory} from '../src/ContinuousClearingAuctionFactory.sol'; import {IContinuousClearingAuction} from '../src/interfaces/IContinuousClearingAuction.sol'; import {IContinuousClearingAuctionFactory} from '../src/interfaces/IContinuousClearingAuctionFactory.sol'; +import {IStepStorage} from '../src/interfaces/IStepStorage.sol'; import {ITickStorage} from '../src/interfaces/ITickStorage.sol'; import {ITokenCurrencyStorage} from '../src/interfaces/ITokenCurrencyStorage.sol'; import {IDistributionContract} from '../src/interfaces/external/IDistributionContract.sol'; @@ -55,7 +56,7 @@ contract AuctionFactoryTest is AuctionBaseTest { function test_initializeDistribution_revertsWithInvalidClaimBlock() public { uint256 endBlock = block.number + AUCTION_DURATION; bytes memory configData = abi.encode(params.withClaimBlock(endBlock - 1)); - vm.expectRevert(IContinuousClearingAuction.ClaimBlockIsBeforeEndBlock.selector); + vm.expectRevert(IStepStorage.ClaimBlockIsBeforeEndBlock.selector); factory.initializeDistribution(address(token), TOTAL_SUPPLY, configData, bytes32(0)); } diff --git a/test/AuctionStepStorage.t.sol b/test/AuctionStepStorage.t.sol index 7dc1fce8..19d755bd 100644 --- a/test/AuctionStepStorage.t.sol +++ b/test/AuctionStepStorage.t.sol @@ -23,7 +23,7 @@ contract AuctionStepStorageTest is Test { internal returns (MockStepStorage) { - return new MockStepStorage(auctionStepsData, uint64(startBlock), uint64(endBlock)); + return new MockStepStorage(auctionStepsData, uint64(startBlock), uint64(endBlock), uint64(endBlock)); } function test_canBeConstructed_fuzz(uint8 numIterations) public { diff --git a/test/btt/auction/SweepUnsoldTokens.t.sol b/test/btt/auction/SweepUnsoldTokens.t.sol index 77b739aa..44ece5e4 100644 --- a/test/btt/auction/SweepUnsoldTokens.t.sol +++ b/test/btt/auction/SweepUnsoldTokens.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import {AuctionFuzzConstructorParams, BttBase} from 'btt/BttBase.sol'; import {MockContinuousClearingAuction} from 'btt/mocks/MockContinuousClearingAuction.sol'; import {IContinuousClearingAuction} from 'continuous-clearing-auction/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'continuous-clearing-auction/interfaces/IStepStorage.sol'; import {ITokenCurrencyStorage} from 'continuous-clearing-auction/interfaces/ITokenCurrencyStorage.sol'; import {IERC20Minimal} from 'continuous-clearing-auction/interfaces/external/IERC20Minimal.sol'; import {Checkpoint} from 'continuous-clearing-auction/libraries/CheckpointLib.sol'; @@ -23,7 +24,7 @@ contract SweepUnsoldTokensTest is BttBase { uint256 blockNumber = bound(_blockNumber, 0, mParams.parameters.endBlock - 1); vm.roll(blockNumber); - vm.expectRevert(IContinuousClearingAuction.AuctionIsNotOver.selector); + vm.expectRevert(IStepStorage.AuctionIsNotOver.selector); auction.sweepUnsoldTokens(); } diff --git a/test/btt/auction/claimTokens.t.sol b/test/btt/auction/claimTokens.t.sol index 8423623b..890f81ab 100644 --- a/test/btt/auction/claimTokens.t.sol +++ b/test/btt/auction/claimTokens.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import {AuctionFuzzConstructorParams, BttBase} from 'btt/BttBase.sol'; import {MockContinuousClearingAuction} from 'btt/mocks/MockContinuousClearingAuction.sol'; import {IContinuousClearingAuction} from 'continuous-clearing-auction/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'continuous-clearing-auction/interfaces/IStepStorage.sol'; import {ITokenCurrencyStorage} from 'continuous-clearing-auction/interfaces/ITokenCurrencyStorage.sol'; import {IERC20Minimal} from 'continuous-clearing-auction/interfaces/external/IERC20Minimal.sol'; import {Bid} from 'continuous-clearing-auction/libraries/BidLib.sol'; @@ -26,7 +27,7 @@ contract ClaimTokensTest is BttBase { uint256 blockNumber = bound(_blockNumber, 0, mParams.parameters.claimBlock - 1); vm.roll(blockNumber); - vm.expectRevert(IContinuousClearingAuction.NotClaimable.selector); + vm.expectRevert(IStepStorage.NotClaimable.selector); auction.claimTokens(0); } diff --git a/test/btt/auction/claimTokensBatch.t.sol b/test/btt/auction/claimTokensBatch.t.sol index 6fc60792..6c594608 100644 --- a/test/btt/auction/claimTokensBatch.t.sol +++ b/test/btt/auction/claimTokensBatch.t.sol @@ -7,6 +7,7 @@ import {ERC20Mock} from 'openzeppelin-contracts/contracts/mocks/token/ERC20Mock. import {FixedPointMathLib} from 'solady/utils/FixedPointMathLib.sol'; import {Checkpoint} from 'src/CheckpointStorage.sol'; import {IContinuousClearingAuction} from 'src/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'src/interfaces/IStepStorage.sol'; import {ITokenCurrencyStorage} from 'src/interfaces/ITokenCurrencyStorage.sol'; import {ConstantsLib} from 'src/libraries/ConstantsLib.sol'; import {FixedPoint96} from 'src/libraries/FixedPoint96.sol'; @@ -26,7 +27,7 @@ contract ClaimTokensBatchTest is BttBase { _blockNumber = uint64(bound(_blockNumber, 0, mParams.parameters.claimBlock - 1)); vm.roll(_blockNumber); - vm.expectRevert(IContinuousClearingAuction.NotClaimable.selector); + vm.expectRevert(IStepStorage.NotClaimable.selector); auction.claimTokensBatch(address(0), new uint256[](0)); } diff --git a/test/btt/auction/constructor.t.sol b/test/btt/auction/constructor.t.sol index 46acb454..5858e454 100644 --- a/test/btt/auction/constructor.t.sol +++ b/test/btt/auction/constructor.t.sol @@ -5,6 +5,7 @@ import {AuctionFuzzConstructorParams, BttBase} from '../BttBase.sol'; import {FixedPointMathLib} from 'solady/utils/FixedPointMathLib.sol'; import {ContinuousClearingAuction} from 'src/ContinuousClearingAuction.sol'; import {IContinuousClearingAuction} from 'src/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'src/interfaces/IStepStorage.sol'; import {ConstantsLib} from 'src/libraries/ConstantsLib.sol'; import {FixedPoint96} from 'src/libraries/FixedPoint96.sol'; import {MaxBidPriceLib} from 'src/libraries/MaxBidPriceLib.sol'; @@ -24,16 +25,6 @@ contract ConstructorTest is BttBase { */ uint256 MAX_LIQUIDITY_BOUND = 191_757_530_477_355_300_863_043_035_987_968; - function test_WhenClaimBlockLTEndBlock(AuctionFuzzConstructorParams memory _params) external { - // it reverts with {ClaimBlockIsBeforeEndBlock} - - AuctionFuzzConstructorParams memory mParams = validAuctionConstructorInputs(_params); - mParams.parameters.claimBlock = uint64(bound(mParams.parameters.claimBlock, 0, mParams.parameters.endBlock - 1)); - - vm.expectRevert(IContinuousClearingAuction.ClaimBlockIsBeforeEndBlock.selector); - new ContinuousClearingAuction(mParams.token, mParams.totalSupply, mParams.parameters); - } - function test_WhenClaimBlockGEEndBlock(AuctionFuzzConstructorParams memory _params, uint64 _claimBlock) external setupAuctionConstructorParams(_params) diff --git a/test/btt/auction/exitBid.t.sol b/test/btt/auction/exitBid.t.sol index e8e18602..fbbc8489 100644 --- a/test/btt/auction/exitBid.t.sol +++ b/test/btt/auction/exitBid.t.sol @@ -5,6 +5,7 @@ import {AuctionFuzzConstructorParams, BttBase} from 'btt/BttBase.sol'; import {MockContinuousClearingAuction} from 'btt/mocks/MockContinuousClearingAuction.sol'; import {ERC20Mock} from 'openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol'; import {IContinuousClearingAuction} from 'src/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'src/interfaces/IStepStorage.sol'; import {FixedPoint96} from 'src/libraries/FixedPoint96.sol'; contract ExitBidTest is BttBase { @@ -24,7 +25,7 @@ contract ExitBidTest is BttBase { vm.roll(_blockNumber); - vm.expectRevert(IContinuousClearingAuction.AuctionIsNotOver.selector); + vm.expectRevert(IStepStorage.AuctionIsNotOver.selector); auction.exitBid(0); } diff --git a/test/btt/auction/onlyAfterAuctionIsOver.t.sol b/test/btt/auction/onlyAfterAuctionIsOver.t.sol index 2e3fff7b..6abe43a8 100644 --- a/test/btt/auction/onlyAfterAuctionIsOver.t.sol +++ b/test/btt/auction/onlyAfterAuctionIsOver.t.sol @@ -5,6 +5,7 @@ import {AuctionFuzzConstructorParams, BttBase} from '../BttBase.sol'; import {MockContinuousClearingAuction} from 'btt/mocks/MockContinuousClearingAuction.sol'; import {IContinuousClearingAuction} from 'continuous-clearing-auction/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'continuous-clearing-auction/interfaces/IStepStorage.sol'; contract OnlyAfterAuctionIsOverTest is BttBase { function test_WhenBlockNumberLTEndBlock(AuctionFuzzConstructorParams memory _params, uint256 _blockNumber) @@ -19,7 +20,7 @@ contract OnlyAfterAuctionIsOverTest is BttBase { uint256 blockNumber = bound(_blockNumber, 0, mParams.parameters.endBlock - 1); vm.roll(blockNumber); - vm.expectRevert(IContinuousClearingAuction.AuctionIsNotOver.selector); + vm.expectRevert(IStepStorage.AuctionIsNotOver.selector); auction.modifier_onlyAfterAuctionIsOver(); } diff --git a/test/btt/auction/onlyAfterClaimBlock.t.sol b/test/btt/auction/onlyAfterClaimBlock.t.sol index d33d0f37..0eb05f4b 100644 --- a/test/btt/auction/onlyAfterClaimBlock.t.sol +++ b/test/btt/auction/onlyAfterClaimBlock.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.26; import {AuctionFuzzConstructorParams, BttBase} from '../BttBase.sol'; import {MockContinuousClearingAuction} from 'btt/mocks/MockContinuousClearingAuction.sol'; -import {IContinuousClearingAuction} from 'continuous-clearing-auction/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'continuous-clearing-auction/interfaces/IStepStorage.sol'; contract OnlyAfterClaimBlockTest is BttBase { function test_WhenBlockNumberLTClaimBlock(AuctionFuzzConstructorParams memory _params, uint256 _blockNumber) @@ -21,7 +21,7 @@ contract OnlyAfterClaimBlockTest is BttBase { uint256 blockNumber = bound(_blockNumber, 0, mParams.parameters.claimBlock - 1); vm.roll(blockNumber); - vm.expectRevert(IContinuousClearingAuction.NotClaimable.selector); + vm.expectRevert(IStepStorage.NotClaimable.selector); auction.modifier_onlyAfterClaimBlock(); } diff --git a/test/btt/auction/sweepCurrency.t.sol b/test/btt/auction/sweepCurrency.t.sol index 707178b1..e6c80e82 100644 --- a/test/btt/auction/sweepCurrency.t.sol +++ b/test/btt/auction/sweepCurrency.t.sol @@ -6,7 +6,7 @@ import {MockContinuousClearingAuction} from '../mocks/MockContinuousClearingAuct import {ERC20Mock} from 'openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol'; import {FixedPointMathLib} from 'solady/utils/FixedPointMathLib.sol'; import {Checkpoint} from 'src/CheckpointStorage.sol'; -import {IContinuousClearingAuction} from 'src/interfaces/IContinuousClearingAuction.sol'; +import {IStepStorage} from 'src/interfaces/IStepStorage.sol'; import {ITokenCurrencyStorage} from 'src/interfaces/ITokenCurrencyStorage.sol'; import {ConstantsLib} from 'src/libraries/ConstantsLib.sol'; import {FixedPoint96} from 'src/libraries/FixedPoint96.sol'; @@ -32,7 +32,7 @@ contract SweepCurrencyTest is BttBase { vm.roll(_blockNumber); - vm.expectRevert(IContinuousClearingAuction.AuctionIsNotOver.selector); + vm.expectRevert(IStepStorage.AuctionIsNotOver.selector); auction.sweepCurrency(); } diff --git a/test/btt/auctionStepStorage/constructor.tree b/test/btt/auctionStepStorage/constructor.tree deleted file mode 100644 index 640bec29..00000000 --- a/test/btt/auctionStepStorage/constructor.tree +++ /dev/null @@ -1,11 +0,0 @@ -ConstructorTest -├── when start block GE end block -│ └── it reverts with {InvalidEndBlock} -└── when start block LE end block - ├── it etches START_BLOCK - ├── it etches END_BLOCK - ├── it etches _LENGTH - ├── it etches $_pointer - ├── it writes $_offset - ├── it writes $step - └── it emits {AuctionStepRecorded} \ No newline at end of file diff --git a/test/btt/mocks/MockStepStorage.sol b/test/btt/mocks/MockStepStorage.sol index afb03f5f..2c364eba 100644 --- a/test/btt/mocks/MockStepStorage.sol +++ b/test/btt/mocks/MockStepStorage.sol @@ -5,8 +5,8 @@ import {StepStorage} from 'continuous-clearing-auction/StepStorage.sol'; import {AuctionStep} from 'continuous-clearing-auction/libraries/StepLib.sol'; contract MockStepStorage is StepStorage { - constructor(bytes memory _auctionStepsData, uint64 _startBlock, uint64 _endBlock) - StepStorage(_auctionStepsData, _startBlock, _endBlock) + constructor(bytes memory _auctionStepsData, uint64 _startBlock, uint64 _endBlock, uint64 _claimBlock) + StepStorage(_auctionStepsData, _startBlock, _endBlock, _claimBlock) {} function advanceStep() public returns (AuctionStep memory) { @@ -24,4 +24,8 @@ contract MockStepStorage is StepStorage { function endBlock() external view returns (uint64) { return END_BLOCK; } + + function claimBlock() external view returns (uint64) { + return CLAIM_BLOCK; + } } diff --git a/test/btt/auctionStepStorage/advanceStep.t.sol b/test/btt/stepStorage/advanceStep.t.sol similarity index 97% rename from test/btt/auctionStepStorage/advanceStep.t.sol rename to test/btt/stepStorage/advanceStep.t.sol index 393b4a15..514e59fc 100644 --- a/test/btt/auctionStepStorage/advanceStep.t.sol +++ b/test/btt/stepStorage/advanceStep.t.sol @@ -17,7 +17,7 @@ contract AdvanceStepTest is BttBase { uint64 startBlock = uint64(bound(_startBlock, 1, type(uint64).max - numberOfBlocks)); uint64 endBlock = startBlock + uint64(numberOfBlocks); - auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, endBlock); + auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, endBlock, endBlock); // Then proceed through the list until we are bast the end block. for (uint256 i = 8; i < auctionStepsData.length; i += 8) { @@ -50,7 +50,7 @@ contract AdvanceStepTest is BttBase { vm.expectEmit(true, true, true, true); emit IStepStorage.AuctionStepRecorded(startBlock, startBlock + steps[0].blockDelta, steps[0].mps); vm.record(); - auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, endBlock); + auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, endBlock, endBlock); (, bytes32[] memory writes) = vm.accesses(address(auctionStepStorage)); if (!isCoverage()) { @@ -84,7 +84,7 @@ contract AdvanceStepTest is BttBase { // For the very first step, we have not previously written any data to `step` so the `endBlock` is 0 // This is executed as part of the constructor. - auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, endBlock); + auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, endBlock, endBlock); AuctionStep memory prevStep = auctionStepStorage.step(); diff --git a/test/btt/auctionStepStorage/advanceStep.tree b/test/btt/stepStorage/advanceStep.tree similarity index 100% rename from test/btt/auctionStepStorage/advanceStep.tree rename to test/btt/stepStorage/advanceStep.tree diff --git a/test/btt/auctionStepStorage/constructor.t.sol b/test/btt/stepStorage/constructor.t.sol similarity index 55% rename from test/btt/auctionStepStorage/constructor.t.sol rename to test/btt/stepStorage/constructor.t.sol index d0b732e1..c40a6aac 100644 --- a/test/btt/auctionStepStorage/constructor.t.sol +++ b/test/btt/stepStorage/constructor.t.sol @@ -16,7 +16,7 @@ contract ConstructorTest is BttBase { // it reverts with {InvalidEndBlock} vm.expectRevert(IStepStorage.InvalidEndBlock.selector); - auctionStepStorage = new MockStepStorage(new bytes(0), 1, 0); + auctionStepStorage = new MockStepStorage(new bytes(0), 1, 0, 0); } function test_WhenStartBlockLEEndBlock(Step[] memory _steps, uint64 _startBlock) external { @@ -34,7 +34,9 @@ contract ConstructorTest is BttBase { vm.expectEmit(true, true, true, true); emit IStepStorage.AuctionStepRecorded(startBlock, startBlock + uint64(steps[0].blockDelta), steps[0].mps); vm.record(); - auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, startBlock + uint64(numberOfBlocks)); + auctionStepStorage = new MockStepStorage( + auctionStepsData, startBlock, startBlock + uint64(numberOfBlocks), startBlock + uint64(numberOfBlocks) + ); (, bytes32[] memory writes) = vm.accesses(address(auctionStepStorage)); @@ -54,4 +56,38 @@ contract ConstructorTest is BttBase { bytes memory expectedCode = bytes.concat(bytes1(0x00), auctionStepsData); assertEq(auctionStepStorage.pointer().code, expectedCode); } + + modifier givenStartBlockLEEndBlock() { + _; + } + + function test_WhenClaimBlockLTEndBlock(Step[] memory _steps, uint64 _startBlock, uint64 _claimBlock) + external + givenStartBlockLEEndBlock + { + // it reverts with {ClaimBlockIsBeforeEndBlock} + + (bytes memory auctionStepsData, uint256 numberOfBlocks,) = generateAuctionSteps(_steps); + uint64 startBlock = uint64(bound(_startBlock, 1, type(uint64).max - numberOfBlocks)); + uint64 claimBlock = uint64(bound(_claimBlock, 0, startBlock + uint64(numberOfBlocks) - 1)); + + vm.expectRevert(IStepStorage.ClaimBlockIsBeforeEndBlock.selector); + new MockStepStorage(auctionStepsData, startBlock, startBlock + uint64(numberOfBlocks), claimBlock); + } + + function test_WhenClaimBlockGTEEndBlock(Step[] memory _steps, uint64 _startBlock, uint64 _claimBlock) + external + givenStartBlockLEEndBlock + { + // it writes claim block + (bytes memory auctionStepsData, uint256 numberOfBlocks, Step[] memory steps) = generateAuctionSteps(_steps); + uint64 startBlock = uint64(bound(_startBlock, 1, type(uint64).max - numberOfBlocks)); + uint64 claimBlock = uint64(bound(_claimBlock, startBlock + uint64(numberOfBlocks), type(uint64).max)); + + vm.expectEmit(true, true, true, true); + emit IStepStorage.AuctionStepRecorded(startBlock, startBlock + uint64(steps[0].blockDelta), steps[0].mps); + auctionStepStorage = + new MockStepStorage(auctionStepsData, startBlock, startBlock + uint64(numberOfBlocks), claimBlock); + assertEq(auctionStepStorage.claimBlock(), claimBlock); + } } diff --git a/test/btt/stepStorage/constructor.tree b/test/btt/stepStorage/constructor.tree new file mode 100644 index 00000000..0e9873fc --- /dev/null +++ b/test/btt/stepStorage/constructor.tree @@ -0,0 +1,13 @@ +ConstructorTest +├── when start block GE end block +│ └── it reverts with {InvalidEndBlock} +├── when start block LE end block +│ ├── it etches START_BLOCK +│ ├── it etches END_BLOCK +│ ├── it etches _LENGTH +│ ├── it etches $_pointer +│ ├── it writes $_offset +│ ├── it writes $step +│ └── it emits {AuctionStepRecorded} +└── when claim block LT end block + └── it reverts with {ClaimBlockIsBeforeEndBlock} \ No newline at end of file diff --git a/test/btt/auctionStepStorage/validate.t.sol b/test/btt/stepStorage/validate.t.sol similarity index 93% rename from test/btt/auctionStepStorage/validate.t.sol rename to test/btt/stepStorage/validate.t.sol index a3a3e5d3..55b29506 100644 --- a/test/btt/auctionStepStorage/validate.t.sol +++ b/test/btt/stepStorage/validate.t.sol @@ -18,7 +18,7 @@ contract ValidateTest is BttBase { // it reverts with {InvalidAuctionDataLength} vm.expectRevert(IStepStorage.InvalidAuctionDataLength.selector); - auctionStepStorage = new MockStepStorage(bytes(''), 1, 2); + auctionStepStorage = new MockStepStorage(bytes(''), 1, 2, 2); } modifier whenAuctionStepsDataLengthNEQ0() { @@ -29,7 +29,7 @@ contract ValidateTest is BttBase { // it reverts with {InvalidAuctionDataLength} vm.expectRevert(IStepStorage.InvalidAuctionDataLength.selector); - auctionStepStorage = new MockStepStorage(new bytes(7), 1, 2); + auctionStepStorage = new MockStepStorage(new bytes(7), 1, 2, 2); } modifier whenAuctionStepsDataLengthIsMultipleOfUINT64_SIZE() { @@ -48,7 +48,8 @@ contract ValidateTest is BttBase { steps[0].blockDelta = 1; (bytes memory auctionStepsData, uint256 numberOfBlocks,) = generateAuctionSteps(steps); - auctionStepStorage = new MockStepStorage(auctionStepsData, 1, 1 + uint64(numberOfBlocks)); + auctionStepStorage = + new MockStepStorage(auctionStepsData, 1, 1 + uint64(numberOfBlocks), 1 + uint64(numberOfBlocks)); address pointer = new bytes(16).write(); @@ -72,7 +73,7 @@ contract ValidateTest is BttBase { bytes memory auctionStepsData = CompactStepLib.pack(steps); vm.expectRevert(IStepStorage.StepBlockDeltaCannotBeZero.selector); - auctionStepStorage = new MockStepStorage(auctionStepsData, 1, 2); + auctionStepStorage = new MockStepStorage(auctionStepsData, 1, 2, 2); } modifier whenNoAuctionStepWithDeltaEQ0() { @@ -93,7 +94,7 @@ contract ValidateTest is BttBase { bytes memory auctionStepsData = CompactStepLib.pack(steps); vm.expectRevert(abi.encodeWithSelector(IStepStorage.InvalidStepDataMps.selector, 1e7 - 1, ConstantsLib.MPS)); - auctionStepStorage = new MockStepStorage(auctionStepsData, 1, 2); + auctionStepStorage = new MockStepStorage(auctionStepsData, 1, 2, 2); } modifier whenSumOfMpsTimesDeltaEQMPS() { @@ -114,7 +115,7 @@ contract ValidateTest is BttBase { bytes memory auctionStepsData = CompactStepLib.pack(steps); vm.expectRevert(abi.encodeWithSelector(IStepStorage.InvalidEndBlockGivenStepData.selector, 2, 3)); - auctionStepStorage = new MockStepStorage(auctionStepsData, 1, 3); + auctionStepStorage = new MockStepStorage(auctionStepsData, 1, 3, 3); } function test_WhenSumOfBlockDeltaAndStartBlockEQEndBlock(Step[] memory _steps, uint64 _startBlock) @@ -129,7 +130,9 @@ contract ValidateTest is BttBase { (bytes memory auctionStepsData, uint256 numberOfBlocks,) = generateAuctionSteps(_steps); uint64 startBlock = uint64(bound(_startBlock, 1, type(uint64).max - numberOfBlocks)); - auctionStepStorage = new MockStepStorage(auctionStepsData, startBlock, startBlock + uint64(numberOfBlocks)); + auctionStepStorage = new MockStepStorage( + auctionStepsData, startBlock, startBlock + uint64(numberOfBlocks), startBlock + uint64(numberOfBlocks) + ); address pointer = auctionStepStorage.pointer(); vm.record(); diff --git a/test/btt/auctionStepStorage/validate.tree b/test/btt/stepStorage/validate.tree similarity index 100% rename from test/btt/auctionStepStorage/validate.tree rename to test/btt/stepStorage/validate.tree diff --git a/test/lens/TickDataLens.t.sol b/test/lens/TickDataLens.t.sol new file mode 100644 index 00000000..ecb5bb75 --- /dev/null +++ b/test/lens/TickDataLens.t.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IContinuousClearingAuction} from '../../src/interfaces/IContinuousClearingAuction.sol'; +import {Tick} from '../../src/interfaces/ITickStorage.sol'; +import {TickDataLens, TickWithData} from '../../src/lens/TickDataLens.sol'; +import {ConstantsLib} from '../../src/libraries/ConstantsLib.sol'; +import {FixedPoint96} from '../../src/libraries/FixedPoint96.sol'; +import {AuctionUnitTest} from '../unit/AuctionUnitTest.sol'; +import {FixedPointMathLib} from 'solady/utils/FixedPointMathLib.sol'; + +contract TickDataLensTest is AuctionUnitTest { + using FixedPointMathLib for *; + + TickDataLens public lens; + + function setUp() public { + setUpMockAuction(); + lens = new TickDataLens(); + } + + function test_getInitializedTickData_revertsWhenNoTicksAboveClearing() public { + vm.expectRevert(); + lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + } + + function test_getInitializedTickData_singleTick() public { + uint256 price = params.floorPrice + params.tickSpacing; + uint256 demand = 1e18 << FixedPoint96.RESOLUTION; + + _initializeTick(price, demand); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, 1); + assertEq(ticks[0].priceQ96, price); + assertEq(ticks[0].currencyDemandQ96, demand); + + uint256 expectedRequired = uint256(TOTAL_SUPPLY) * price; + assertEq(ticks[0].requiredCurrencyDemandQ96, expectedRequired); + + uint256 expectedCurrencyRequired = expectedRequired.saturatingSub(demand); + assertEq(ticks[0].currencyRequiredQ96, expectedCurrencyRequired); + } + + function test_getInitializedTickData_multipleTicks() public { + uint256 price1 = params.floorPrice + params.tickSpacing; + uint256 price2 = params.floorPrice + 2 * params.tickSpacing; + uint256 demand1 = 1e18 << FixedPoint96.RESOLUTION; + uint256 demand2 = 2e18 << FixedPoint96.RESOLUTION; + uint256 totalDemand = demand1 + demand2; + + mockAuction.uncheckedInitializeTickIfNeeded(params.floorPrice, price1); + mockAuction.uncheckedInitializeTickIfNeeded(price1, price2); + mockAuction.uncheckedUpdateTickDemand(price1, demand1); + mockAuction.uncheckedUpdateTickDemand(price2, demand2); + mockAuction.uncheckedSetNextActiveTickPrice(price1); + mockAuction.uncheckedSetSumDemandAboveClearing(totalDemand); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, 2); + + // First tick: sumDemand includes demand at both ticks + assertEq(ticks[0].priceQ96, price1); + assertEq(ticks[0].currencyDemandQ96, demand1); + uint256 required1 = uint256(TOTAL_SUPPLY) * price1; + assertEq(ticks[0].requiredCurrencyDemandQ96, required1); + assertEq(ticks[0].currencyRequiredQ96, required1.saturatingSub(totalDemand)); + + // Second tick: sumDemand has been reduced by demand1 + assertEq(ticks[1].priceQ96, price2); + assertEq(ticks[1].currencyDemandQ96, demand2); + uint256 required2 = uint256(TOTAL_SUPPLY) * price2; + assertEq(ticks[1].requiredCurrencyDemandQ96, required2); + assertEq(ticks[1].currencyRequiredQ96, required2.saturatingSub(demand2)); + } + + function test_getInitializedTickData_currencyRequiredIsZeroWhenDemandExceedsRequired() public { + uint256 price = params.floorPrice + params.tickSpacing; + uint256 requiredDemand = uint256(TOTAL_SUPPLY) * price; + uint256 demand = requiredDemand * 2; + + _initializeTick(price, demand); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, 1); + assertEq(ticks[0].currencyRequiredQ96, 0); + } + + function test_getInitializedTickData_scalesByRemainingMps() public { + // Advance 1 block to create a checkpoint with non-zero cumulativeMps + vm.roll(block.number + 1); + mockAuction.checkpoint(); + + uint256 price = params.floorPrice + params.tickSpacing; + uint256 demand = 1e18 << FixedPoint96.RESOLUTION; + + _initializeTick(price, demand); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, 1); + + // After 1 block at STANDARD_MPS_1_PERCENT, cumulativeMps = STANDARD_MPS_1_PERCENT + uint24 expectedRemainingMps = ConstantsLib.MPS - STANDARD_MPS_1_PERCENT; + uint256 shortfall = (uint256(TOTAL_SUPPLY) * price).saturatingSub(demand); + uint256 expectedCurrencyRequired = shortfall.fullMulDivUp(expectedRemainingMps, ConstantsLib.MPS); + assertEq(ticks[0].currencyRequiredQ96, expectedCurrencyRequired); + } + + function test_getInitializedTickData_afterAuctionEnds() public { + uint256 price = params.floorPrice + params.tickSpacing; + uint256 demand = 1e18 << FixedPoint96.RESOLUTION; + + _initializeTick(price, demand); + + // Roll past end of auction so cumulativeMps == MPS and remainingMps == 0 + vm.roll(block.number + AUCTION_DURATION + 1); + mockAuction.checkpoint(); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, 1); + assertEq(ticks[0].priceQ96, price); + assertEq(ticks[0].currencyDemandQ96, demand); + assertEq(ticks[0].requiredCurrencyDemandQ96, uint256(TOTAL_SUPPLY) * price); + // With remainingMps == 0, currencyRequired scales to 0 + assertEq(ticks[0].currencyRequiredQ96, 0); + } + + function test_getInitializedTickData_ticksReturnedInOrder() public { + uint256 price1 = params.floorPrice + params.tickSpacing; + uint256 price2 = params.floorPrice + 2 * params.tickSpacing; + uint256 price3 = params.floorPrice + 3 * params.tickSpacing; + uint256 demandPerTick = 1e18 << FixedPoint96.RESOLUTION; + + mockAuction.uncheckedInitializeTickIfNeeded(params.floorPrice, price1); + mockAuction.uncheckedInitializeTickIfNeeded(price1, price2); + mockAuction.uncheckedInitializeTickIfNeeded(price2, price3); + mockAuction.uncheckedUpdateTickDemand(price1, demandPerTick); + mockAuction.uncheckedUpdateTickDemand(price2, demandPerTick); + mockAuction.uncheckedUpdateTickDemand(price3, demandPerTick); + mockAuction.uncheckedSetNextActiveTickPrice(price1); + mockAuction.uncheckedSetSumDemandAboveClearing(demandPerTick * 3); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, 3); + assertEq(ticks[0].priceQ96, price1); + assertEq(ticks[1].priceQ96, price2); + assertEq(ticks[2].priceQ96, price3); + // Prices are strictly increasing + assertLt(ticks[0].priceQ96, ticks[1].priceQ96); + assertLt(ticks[1].priceQ96, ticks[2].priceQ96); + } + + function test_getInitializedTickData_requiredCurrencyDemandIncreases() public { + uint256 price1 = params.floorPrice + params.tickSpacing; + uint256 price2 = params.floorPrice + 2 * params.tickSpacing; + uint256 demandPerTick = 1e18 << FixedPoint96.RESOLUTION; + + mockAuction.uncheckedInitializeTickIfNeeded(params.floorPrice, price1); + mockAuction.uncheckedInitializeTickIfNeeded(price1, price2); + mockAuction.uncheckedUpdateTickDemand(price1, demandPerTick); + mockAuction.uncheckedUpdateTickDemand(price2, demandPerTick); + mockAuction.uncheckedSetNextActiveTickPrice(price1); + mockAuction.uncheckedSetSumDemandAboveClearing(demandPerTick * 2); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, 2); + assertLt(ticks[0].requiredCurrencyDemandQ96, ticks[1].requiredCurrencyDemandQ96); + } + + /// forge-config: default.fuzz.runs = 1000 + /// forge-config: ci.fuzz.runs = 1000 + function test_getInitializedTickData_fuzz(uint256 numTicks) public { + numTicks = bound(numTicks, 1, lens.MAX_BUFFER_SIZE()); + + (uint256[] memory prices, uint256[] memory demands, uint256 totalDemand) = _initializeTicks(numTicks); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + + assertEq(ticks.length, numTicks); + + uint256 runningDemand = totalDemand; + for (uint256 i = 0; i < numTicks; i++) { + uint256 required = uint256(TOTAL_SUPPLY) * prices[i]; + assertEq(ticks[i].priceQ96, prices[i]); + assertEq(ticks[i].currencyDemandQ96, demands[i]); + assertEq(ticks[i].requiredCurrencyDemandQ96, required); + assertEq(ticks[i].currencyRequiredQ96, required.saturatingSub(runningDemand)); + runningDemand -= demands[i]; + } + } + + /// forge-config: default.isolate = true + /// forge-config: ci.isolate = true + function test_getInitializedTickData_MaxBufferSize_gas() public { + uint256 numTicks = lens.MAX_BUFFER_SIZE(); + (uint256[] memory prices, uint256[] memory demands, uint256 totalDemand) = _initializeTicks(numTicks); + + TickWithData[] memory ticks = lens.getInitializedTickData(IContinuousClearingAuction(address(mockAuction))); + vm.snapshotGasLastCall('getInitializedTickData max buffer size'); + + assertEq(ticks.length, numTicks); + + uint256 runningDemand = totalDemand; + for (uint256 i = 0; i < numTicks; i++) { + uint256 required = uint256(TOTAL_SUPPLY) * prices[i]; + assertEq(ticks[i].priceQ96, prices[i]); + assertEq(ticks[i].currencyDemandQ96, demands[i]); + assertEq(ticks[i].requiredCurrencyDemandQ96, required); + assertEq(ticks[i].currencyRequiredQ96, required.saturatingSub(runningDemand)); + runningDemand -= demands[i]; + } + } + + // ============================================ + // Helpers + // ============================================ + + function _initializeTick(uint256 price, uint256 demand) internal { + mockAuction.uncheckedInitializeTickIfNeeded(params.floorPrice, price); + mockAuction.uncheckedUpdateTickDemand(price, demand); + mockAuction.uncheckedSetNextActiveTickPrice(price); + mockAuction.uncheckedSetSumDemandAboveClearing(demand); + } + + function _initializeTicks(uint256 numTicks) + internal + returns (uint256[] memory prices, uint256[] memory demands, uint256 totalDemand) + { + prices = new uint256[](numTicks); + demands = new uint256[](numTicks); + + uint256 prevPrice = params.floorPrice; + for (uint256 i = 0; i < numTicks; i++) { + prices[i] = params.floorPrice + (i + 1) * params.tickSpacing; + demands[i] = (i + 1) * (1e18 << FixedPoint96.RESOLUTION); + totalDemand += demands[i]; + mockAuction.uncheckedInitializeTickIfNeeded(prevPrice, prices[i]); + mockAuction.uncheckedUpdateTickDemand(prices[i], demands[i]); + prevPrice = prices[i]; + } + mockAuction.uncheckedSetNextActiveTickPrice(prices[0]); + mockAuction.uncheckedSetSumDemandAboveClearing(totalDemand); + } +} diff --git a/test/utils/MockStepStorage.sol b/test/utils/MockStepStorage.sol index 8a474908..d4ae9d67 100644 --- a/test/utils/MockStepStorage.sol +++ b/test/utils/MockStepStorage.sol @@ -5,8 +5,8 @@ import {StepStorage} from '../../src/StepStorage.sol'; /// @notice Mock auction step storage for testing contract MockStepStorage is StepStorage { - constructor(bytes memory _auctionStepsData, uint64 _startBlock, uint64 _endBlock) - StepStorage(_auctionStepsData, _startBlock, _endBlock) + constructor(bytes memory _auctionStepsData, uint64 _startBlock, uint64 _endBlock, uint64 _claimBlock) + StepStorage(_auctionStepsData, _startBlock, _endBlock, _claimBlock) {} function advanceStep() public {