Skip to content
This repository has been archived by the owner on Nov 27, 2024. It is now read-only.

refactor: caster initializes with caster config #69

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a3d9e07
added set quorum fns
dd0sxx Dec 12, 2023
af9c1ef
SetQuorumPct
dd0sxx Dec 12, 2023
2bb3f4e
SetQuorumPct 721
dd0sxx Dec 12, 2023
cbce3da
compiles without tests
dd0sxx Dec 13, 2023
b48750e
tests work
dd0sxx Dec 13, 2023
cccded6
fmt
dd0sxx Dec 13, 2023
8f59376
pushing changes
dd0sxx Dec 13, 2023
c16d10e
builds w no tests
dd0sxx Dec 14, 2023
264218e
got rid of hardcoded 1/3 / 2/3 periods
dd0sxx Dec 14, 2023
228f830
added internal fn
dd0sxx Dec 14, 2023
83825c6
init to default value
dd0sxx Dec 14, 2023
66d1bdc
Merge branch 'main' into theo/update-submission-period
dd0sxx Dec 14, 2023
62e48c6
uint48
dd0sxx Dec 14, 2023
8e06643
added view fns
dd0sxx Dec 14, 2023
69117cf
so close
dd0sxx Dec 15, 2023
1550514
5 MOORE
dd0sxx Dec 15, 2023
aba8341
CLOSER
dd0sxx Dec 15, 2023
d2e978c
YES
dd0sxx Dec 15, 2023
1f2823f
new tests added
dd0sxx Dec 15, 2023
abe2676
Merge branch 'main' into theo/update-submission-period
dd0sxx Dec 15, 2023
a109d5d
fix
dd0sxx Dec 15, 2023
6dd31ff
comment
dd0sxx Dec 15, 2023
f028e5f
Update PeriodPctCheckpoints.sol
dd0sxx Dec 15, 2023
6960a96
Update PeriodPctCheckpoints.sol
dd0sxx Dec 15, 2023
84b362f
Update src/lib/QuorumCheckpoints.sol
0xrajath Dec 15, 2023
448bc49
delayPeriodTimestamp weight
dd0sxx Dec 15, 2023
3a4c010
added weight test
dd0sxx Dec 15, 2023
88946da
tests
dd0sxx Dec 15, 2023
a000c44
test
dd0sxx Dec 15, 2023
4eaf6a1
test
dd0sxx Dec 15, 2023
0d50295
comments and insert return fn
dd0sxx Dec 15, 2023
8e546f6
comments
dd0sxx Dec 15, 2023
ddd9839
fixed
dd0sxx Dec 15, 2023
a968f38
config
dd0sxx Dec 15, 2023
982eeb3
fix
dd0sxx Dec 15, 2023
9d98d61
fix
dd0sxx Dec 15, 2023
b3e60bc
Merge branch 'main' into theo/caster-config
dd0sxx Dec 15, 2023
474ebc4
comment
dd0sxx Dec 15, 2023
04d3310
Update src/token-voting/LlamaTokenCaster.sol
dd0sxx Dec 15, 2023
8d8fc6b
fmt
dd0sxx Dec 15, 2023
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
Prev Previous commit
Next Next commit
builds w no tests
  • Loading branch information
dd0sxx committed Dec 14, 2023
commit c16d10e4e8dbeca51380c635e805f76e9215b1f5
6 changes: 6 additions & 0 deletions src/lib/LlamaUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ library LlamaUtils {
return uint16(n);
}

/// @dev Reverts if `n` does not fit in a `uint48`.
function toUint48(uint256 n) internal pure returns (uint48) {
if (n > type(uint48).max) revert UnsafeCast(n);
return uint48(n);
}

/// @dev Reverts if `n` does not fit in a `uint64`.
function toUint64(uint256 n) internal pure returns (uint64) {
if (n > type(uint64).max) revert UnsafeCast(n);
Expand Down
289 changes: 289 additions & 0 deletions src/lib/PeriodPctCheckpoints.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
// SPDX-License-Identifier: MIT
// forgefmt: disable-start
pragma solidity ^0.8.0;

import {LlamaUtils} from "src/lib/LlamaUtils.sol";

/**
* @dev This library defines the `History` struct, for checkpointing values as they change at different points in
* time, and later looking up past values by block timestamp.
*
* To create a history of checkpoints define a variable type `PolicyholderCheckpoints.History` in your contract, and store a new
* checkpoint for the current transaction timestamp using the {push} function.
*
* @dev This was created by modifying then running the OpenZeppelin `Checkpoints.js` script, which generated a version
* of this library that uses a 64 bit `timestamp` and 96 bit `quantity` field in the `Checkpoint` struct. The struct
* was then modified to add two uint16 quorum fields. For simplicity, safe cast and math methods were inlined from
* the OpenZeppelin versions at the same commit. We disable forge-fmt for this file to simplify diffing against the
* original OpenZeppelin version: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d00acef4059807535af0bd0dd0ddf619747a044b/contracts/utils/Checkpoints.sol
*/
library PeriodPctCheckpoints {
struct History {
Checkpoint[] _checkpoints;
}

struct Checkpoint {
uint48 timestamp;
uint16 delayPeriodPct;
uint16 castingPeriodPct;
uint16 submissionPeriodPct;
}

/**
* @dev Returns the quorums at a given block timestamp. If a checkpoint is not available at that time, the closest
* one before it is returned, or zero otherwise. Similar to {upperLookup} but optimized for the case when the
* searched checkpoint is probably "recent", defined as being among the last sqrt(N) checkpoints where N is the
* timestamp of checkpoints.
*/
function getAtProbablyRecentTimestamp(History storage self, uint256 timestamp) internal view returns (uint16, uint16, uint16) {
require(timestamp < block.timestamp, "PolicyholderCheckpoints: timestamp is not in the past");
uint48 _timestamp = LlamaUtils.toUint48(timestamp);

uint256 len = self._checkpoints.length;

uint256 low = 0;
uint256 high = len;

if (len > 5) {
uint256 mid = len - sqrt(len);
if (_timestamp < _unsafeAccess(self._checkpoints, mid).timestamp) {
high = mid;
} else {
low = mid + 1;
}
}

uint256 pos = _upperBinaryLookup(self._checkpoints, _timestamp, low, high);

if (pos == 0) return (0, 0, 0);
Checkpoint memory ckpt = _unsafeAccess(self._checkpoints, pos - 1);
return (ckpt.delayPeriodPct, ckpt.castingPeriodPct, ckpt.submissionPeriodPct);
}

/**
* @dev Pushes a `voteQuorumPct` and `vetoQuorumPct` onto a History so that it is stored as the checkpoint for the current
* `timestamp`.
*
* Returns previous quorum and new quorum.
*
* @dev Note that the order of the `voteQuorumPct` and `vetoQuorumPct` parameters is reversed from the ordering used
* everywhere else in this file. The struct and other methods have the order as `(voteQuorumPct, vetoQuorumPct)` but this
* method has it as `(voteQuorumPct, vetoQuorumPct)`. As a result, use caution when editing this method to avoid
* accidentally introducing a bug or breaking change.
*/
function push(History storage self, uint16 delayPeriodPct, uint16 castingPeriodPct, uint16 submissionPeriodPct) internal returns (uint16, uint16, uint16) {
return _insert(self._checkpoints, LlamaUtils.toUint48(block.timestamp), LlamaUtils.toUint16(delayPeriodPct), LlamaUtils.toUint16(castingPeriodPct), LlamaUtils.toUint16(submissionPeriodPct));
}

/**
* @dev Returns the quorum in the most recent checkpoint, or zero if there are no checkpoints.
*/
function latest(History storage self) internal view returns (uint16, uint16, uint16) {
uint256 pos = self._checkpoints.length;
if (pos == 0) return (0, 0, 0);
Checkpoint memory ckpt = _unsafeAccess(self._checkpoints, pos - 1);
return (ckpt.delayPeriodPct, ckpt.castingPeriodPct, ckpt.submissionPeriodPct);
}

/**
* @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the timestamp and
* quorum in the most recent checkpoint.
*/
function latestCheckpoint(History storage self)
internal
view
returns (
bool exists,
uint48 timestamp,
uint16 delayPeriodPct,
uint16 castingPeriodPct,
uint16 submissionPeriodPct
)
{
uint256 pos = self._checkpoints.length;
if (pos == 0) {
return (false, 0, 0, 0, 0);
} else {
Checkpoint memory ckpt = _unsafeAccess(self._checkpoints, pos - 1);
return (true, ckpt.timestamp, ckpt.delayPeriodPct, ckpt.castingPeriodPct, ckpt.submissionPeriodPct);
}
}

/**
* @dev Returns the number of checkpoints.
*/
function length(History storage self) internal view returns (uint256) {
return self._checkpoints.length;
}

/**
* @dev Pushes a (`timestamp`, `voteQuorumPct`, `vetoQuorumPct`) pair into an ordered list of checkpoints, either by inserting a new
* checkpoint, or by updating the last one.
*/
function _insert(
Checkpoint[] storage self,
uint48 timestamp,
uint16 delayPeriodPct,
uint16 castingPeriodPct,
uint16 submissionPeriodPct
) private returns (uint16, uint16, uint16) {
uint256 pos = self.length;

if (pos > 0) {
// Copying to memory is important here.
Checkpoint memory last = _unsafeAccess(self, pos - 1);

// Checkpoints timestamps must be increasing.
require(last.timestamp <= timestamp, "Role Checkpoint: invalid timestamp");

// Update or push new checkpoint
if (last.timestamp == timestamp) {
Checkpoint storage ckpt = _unsafeAccess(self, pos - 1);
ckpt.delayPeriodPct = delayPeriodPct;
ckpt.castingPeriodPct = castingPeriodPct;
ckpt.submissionPeriodPct = submissionPeriodPct;
} else {
self.push(Checkpoint({timestamp: timestamp, delayPeriodPct: delayPeriodPct, castingPeriodPct: castingPeriodPct, submissionPeriodPct: submissionPeriodPct}));
}
return (delayPeriodPct, castingPeriodPct, submissionPeriodPct);
} else {
self.push(Checkpoint({timestamp: timestamp, delayPeriodPct: delayPeriodPct, castingPeriodPct: castingPeriodPct, submissionPeriodPct: submissionPeriodPct}));
return (delayPeriodPct, castingPeriodPct, submissionPeriodPct);
}
}

/**
* @dev Return the index of the oldest checkpoint whose timestamp is greater than the search timestamp, or `high`
* if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive
* `high`.
*
* WARNING: `high` should not be greater than the array's length.
*/
function _upperBinaryLookup(
Checkpoint[] storage self,
uint48 timestamp,
uint256 low,
uint256 high
) private view returns (uint256) {
while (low < high) {
uint256 mid = average(low, high);
if (_unsafeAccess(self, mid).timestamp > timestamp) {
high = mid;
} else {
low = mid + 1;
}
}
return high;
}

/**
* @dev Return the index of the oldest checkpoint whose timestamp is greater or equal than the search timestamp, or
* `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and
* exclusive `high`.
*
* WARNING: `high` should not be greater than the array's length.
*/
function _lowerBinaryLookup(
Checkpoint[] storage self,
uint48 timestamp,
uint256 low,
uint256 high
) private view returns (uint256) {
while (low < high) {
uint256 mid = average(low, high);
if (_unsafeAccess(self, mid).timestamp < timestamp) {
low = mid + 1;
} else {
high = mid;
}
}
return high;
}

function _unsafeAccess(Checkpoint[] storage self, uint256 pos)
private
pure
returns (Checkpoint storage result)
{
assembly {
mstore(0, self.slot)
result.slot := add(keccak256(0, 0x20), pos)
}
}

/**
* @dev Returns the average of two numbers. The result is rounded towards
* zero.
*/
function average(uint256 a, uint256 b) private pure returns (uint256) {
return (a & b) + (a ^ b) / 2; // (a + b) / 2 can overflow.
}

/**
* @dev This was copied from Solmate v7 https://github.com/transmissions11/solmate/blob/e8f96f25d48fe702117ce76c79228ca4f20206cb/src/utils/FixedPointMathLib.sol
* @notice The math utils in solmate v7 were reviewed/audited by spearbit as part of the art gobblers audit, and are more efficient than the v6 versions.
*/
function sqrt(uint256 x) internal pure returns (uint256 z) {
assembly {
let y := x // We start y at x, which will help us make our initial estimate.

z := 181 // The "correct" value is 1, but this saves a multiplication later.

// This segment is to get a reasonable initial estimate for the Babylonian method. With a bad
// start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.

// We check y >= 2^(k + 8) but shift right by k bits
// each branch to ensure that if x >= 256, then y >= 256.
if iszero(lt(y, 0x10000000000000000000000000000000000)) {
y := shr(128, y)
z := shl(64, z)
}
if iszero(lt(y, 0x1000000000000000000)) {
y := shr(64, y)
z := shl(32, z)
}
if iszero(lt(y, 0x10000000000)) {
y := shr(32, y)
z := shl(16, z)
}
if iszero(lt(y, 0x1000000)) {
y := shr(16, y)
z := shl(8, z)
}

// Goal was to get z*z*y within a small factor of x. More iterations could
// get y in a tighter range. Currently, we will have y in [256, 256*2^16).
// We ensured y >= 256 so that the relative difference between y and y+1 is small.
// That's not possible if x < 256 but we can just verify those cases exhaustively.

// Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256.
// Correctness can be checked exhaustively for x < 256, so we assume y >= 256.
// Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps.

// For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range
// (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256.

// Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate
// sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18.

// There is no overflow risk here since y < 2^136 after the first branch above.
z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181.

// Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))

// If x+1 is a perfect square, the Babylonian method cycles between
// floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor.
// See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division
// Since the ceil is rare, we save gas on the assignment and repeat division in the rare case.
// If you don't care whether the floor or ceil square root is returned, you can remove this statement.
z := sub(z, lt(div(x, z), z))
}
}
}
32 changes: 22 additions & 10 deletions src/token-voting/LlamaTokenCaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {FixedPointMathLib} from "@solmate/utils/FixedPointMathLib.sol";
import {ILlamaCore} from "src/interfaces/ILlamaCore.sol";
import {ActionState, VoteType} from "src/lib/Enums.sol";
import {LlamaUtils} from "src/lib/LlamaUtils.sol";
import {PeriodPctCheckpoints} from "src/lib/PeriodPctCheckpoints.sol";
import {QuorumCheckpoints} from "src/lib/QuorumCheckpoints.sol";
import {Action, ActionInfo} from "src/lib/Structs.sol";

Expand All @@ -19,6 +20,7 @@ import {Action, ActionInfo} from "src/lib/Structs.sol";
/// contract does not verify that it holds the correct policy when voting and relies on `LlamaCore` to
/// verify that during submission.
abstract contract LlamaTokenCaster is Initializable {
using PeriodPctCheckpoints for PeriodPctCheckpoints.History;
using QuorumCheckpoints for QuorumCheckpoints.History;
// =========================
// ======== Structs ========
Expand Down Expand Up @@ -78,6 +80,9 @@ abstract contract LlamaTokenCaster is Initializable {
/// @dev Thrown when a user tries to cast but does not have enough tokens.
error InsufficientBalance(uint256 balance);

/// @dev Thrown when an invalid `castingPeriodPct` and `submissionPeriodPct` are set.
error InvalidPeriodPcts(uint16 delayPeriodPct, uint16 castingPeriodPct, uint16 submissionPeriodPct);

/// @dev Thrown when an invalid `voteQuorumPct` is passed to the constructor.
error InvalidVoteQuorumPct(uint16 voteQuorumPct);

Expand Down Expand Up @@ -109,25 +114,28 @@ abstract contract LlamaTokenCaster is Initializable {
// ======== Events ========
// ========================

/// @dev Emitted when a vote is cast.
event VoteCast(uint256 id, address indexed tokenholder, uint8 indexed support, uint256 quantity, string reason);

/// @dev Emitted when a cast approval is submitted to the `LlamaCore` contract.
event ApprovalSubmitted(
uint256 id, address indexed caller, uint96 quantityFor, uint96 quantityAgainst, uint96 quantityAbstain
);

/// @dev Emitted when a veto is cast.
event VetoCast(uint256 id, address indexed tokenholder, uint8 indexed support, uint256 quantity, string reason);

/// @dev Emitted when a cast disapproval is submitted to the `LlamaCore` contract.
event DisapprovalSubmitted(
uint256 id, address indexed caller, uint96 quantityFor, uint96 quantityAgainst, uint96 quantityAbstain
);

/// @dev Emitted when the casting and submission period ratio is set.
event PeriodsPctSet(uint16 delayPeriodPct, uint16 castingPeriodPct, uint16 submissionPeriodPct);

/// @dev Emitted when the voting quorum and/or vetoing quorum is set.
event QuorumSet(uint16 voteQuorumPct, uint16 vetoQuorumPct);

/// @dev Emitted when a veto is cast.
event VetoCast(uint256 id, address indexed tokenholder, uint8 indexed support, uint256 quantity, string reason);

/// @dev Emitted when a vote is cast.
event VoteCast(uint256 id, address indexed tokenholder, uint8 indexed support, uint256 quantity, string reason);

// =================================================
// ======== Constants and Storage Variables ========
// =================================================
Expand Down Expand Up @@ -165,6 +173,8 @@ abstract contract LlamaTokenCaster is Initializable {

QuorumCheckpoints.History internal quorumCheckpoints;

PeriodPctCheckpoints.History internal periodPctsCheckpoint;

/// @notice The role used by this contract to cast approvals and disapprovals.
/// @dev This role is expected to have the ability to force approve and disapprove actions.
uint8 public role;
Expand Down Expand Up @@ -351,11 +361,13 @@ abstract contract LlamaTokenCaster is Initializable {
/// @dev `_castingPeriodPct` + `_submissionPeriodPct` must be equal to `ONE_HUNDRED_IN_BPS`
/// @param _castingPeriodPct The minimum % of votes required to submit an approval to `LlamaCore`.
/// @param _submissionPeriodPct The minimum % of vetoes required to submit a disapproval to `LlamaCore`.
function setCastingAndSubmissionPeriodRatio(uint16 _castingPeriodPct, uint16 _submissionPeriodPct) external {
function setPeriodPcts(uint16 _delayPeriodPct, uint16 _castingPeriodPct, uint16 _submissionPeriodPct) external {
if (msg.sender != llamaCore.executor()) revert OnlyLlamaExecutor();
if (_castingPeriodPct + _submissionPeriodPct != ONE_HUNDRED_IN_BPS) revert InvalidSubmissionRatio(_castingPeriodPct, _submissionPeriodPct);
// quorumCheckpoints.push(_voteQuorumPct, _vetoQuorumPct);
// emit QuorumSet(_voteQuorumPct, _vetoQuorumPct);
if (_delayPeriodPct + _castingPeriodPct + _submissionPeriodPct != ONE_HUNDRED_IN_BPS) {
revert InvalidPeriodPcts(_delayPeriodPct, _castingPeriodPct, _submissionPeriodPct);
}
periodPctsCheckpoint.push(_delayPeriodPct, _castingPeriodPct, _submissionPeriodPct);
emit PeriodsPctSet(_delayPeriodPct, _castingPeriodPct, _submissionPeriodPct);
}

// -------- User Nonce Management --------
Expand Down