Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions script/deploy/DeployAuctionStateLens.s.sol

This file was deleted.

16 changes: 16 additions & 0 deletions script/deploy/DeployCCALens.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
3 changes: 3 additions & 0 deletions snapshots/TickDataLensTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"getInitializedTickData max buffer size": "6444733"
}
4 changes: 2 additions & 2 deletions src/lens/AuctionStateLens.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ 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);
}
}

/// @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,
Expand Down
10 changes: 10 additions & 0 deletions src/lens/CCALens.sol
Original file line number Diff line number Diff line change
@@ -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 {}
96 changes: 96 additions & 0 deletions src/lens/TickDataLens.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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 remainingMps = ConstantsLib.MPS - auction.latestCheckpoint().cumulativeMps;
uint256 price = auction.nextActiveTickPrice();

Tick memory t = auction.ticks(price);

uint256 sumCurrencyDemandAboveClearingQ96 = auction.sumCurrencyDemandAboveClearingQ96();
uint256 next = price;
uint256 idx;

uint256 fmp;
// Save the current free memory pointer
assembly {
fmp := mload(0x40)
}

while (idx < MAX_BUFFER_SIZE) {
uint256 requiredCurrencyDemandQ96 = totalSupply * next;
uint256 currencyDemandQ96 = t.currencyDemandQ96;
uint256 nextTick = t.next;
uint256 currencyRequiredQ96 = requiredCurrencyDemandQ96.saturatingSub(sumCurrencyDemandAboveClearingQ96)
.fullMulDivUp(remainingMps, ConstantsLib.MPS);

// Write the TickWithData struct fields (4 x 32 bytes = 0x80) directly to fmp + idx * 0x80,
// then advance the free memory pointer to protect this data from subsequent allocations
assembly {
let offset := add(fmp, mul(idx, 0x80))
mstore(offset, next)
mstore(add(offset, 0x20), currencyDemandQ96)
mstore(add(offset, 0x40), requiredCurrencyDemandQ96)
mstore(add(offset, 0x60), currencyRequiredQ96)
mstore(0x40, add(offset, 0x80))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be done once at the end, does not have to happen in every iteration. As far as I can see, there is no memory allocated other then this inside the while loop, only stack variables, so should be safe.

}

sumCurrencyDemandAboveClearingQ96 -= currencyDemandQ96;
unchecked {
++idx;
}
next = nextTick;
if (next == type(uint256).max) break;
t = auction.ticks(next);
}

// Assemble the Solidity memory layout for a TickWithData[] return value:
// [length][ptr_0..ptr_{n-1}][struct_0..struct_{n-1}]
assembly {
let headerSize := add(0x20, shl(5, idx)) // 32 + idx * 32
let dataSize := shl(7, idx) // idx * 128
mcopy(add(fmp, headerSize), fmp, dataSize)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mcopy is very expensive in this case, since it must copy all of the previously created data in memory to a different location. I understand where you are coming from with this, but you can handle this easier:
The memory setup for an array of a static struct is, as you have correctly documented:
[length][absolute pointer1][absolute pointer2][absolute pointer3][struct content1][struct content2][struct content3]

Reminder, these pointers are absolute values! You can simply continue at the updated free memory pointer with the array length and then point to the previously created structs in memory.
Would something like this:
[struct content1][struct content2][struct content3][length][absolute pointer1][absolute pointer2][absolute pointer3]

Afaik, this should work as well and skips this expensive operation

ticks := fmp
mstore(ticks, idx)
let dataOffset := add(fmp, headerSize)
for { let i := 0 } lt(i, idx) { i := add(i, 1) } {
mstore(add(add(ticks, 0x20), shl(5, i)), add(dataOffset, mul(i, 0x80)))
}
mstore(0x40, add(dataOffset, dataSize))
}
}
}
Loading