Skip to content

Commit 913c04d

Browse files
authored
Merge pull request #19 from cygnusv/checkpoints
Optimize storage of balance checkpoints in T token contract
2 parents f4b8f09 + 5c3979a commit 913c04d

File tree

6 files changed

+527
-314
lines changed

6 files changed

+527
-314
lines changed

contracts/governance/Checkpoints.sol

+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6+
import "@openzeppelin/contracts/utils/math/Math.sol";
7+
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
8+
9+
/// @title Checkpoints
10+
/// @dev Abstract contract to support checkpoints for Compound-like voting and
11+
/// delegation. This implementation supports token supply up to 2^96 - 1.
12+
/// This contract keeps a history (checkpoints) of each account's vote
13+
/// power. Vote power can be delegated either by calling the {delegate}
14+
/// function directly, or by providing a signature to be used with
15+
/// {delegateBySig}. Voting power can be publicly queried through
16+
/// {getVotes} and {getPastVotes}.
17+
/// NOTE: Extracted from OpenZeppelin ERCVotes.sol.
18+
abstract contract Checkpoints {
19+
struct Checkpoint {
20+
uint32 fromBlock;
21+
uint96 votes;
22+
}
23+
24+
mapping(address => address) internal _delegates;
25+
mapping(address => uint128[]) internal _checkpoints;
26+
uint128[] internal _totalSupplyCheckpoints;
27+
28+
/// @notice Emitted when an account changes their delegate.
29+
event DelegateChanged(
30+
address indexed delegator,
31+
address indexed fromDelegate,
32+
address indexed toDelegate
33+
);
34+
35+
/// @notice Emitted when a balance or delegate change results in changes
36+
/// to an account's voting power.
37+
event DelegateVotesChanged(
38+
address indexed delegate,
39+
uint256 previousBalance,
40+
uint256 newBalance
41+
);
42+
43+
function checkpoints(address account, uint32 pos)
44+
public
45+
view
46+
virtual
47+
returns (Checkpoint memory checkpoint)
48+
{
49+
(uint32 fromBlock, uint96 votes) = decodeCheckpoint(
50+
_checkpoints[account][pos]
51+
);
52+
checkpoint = Checkpoint(fromBlock, votes);
53+
}
54+
55+
/// @notice Get number of checkpoints for `account`.
56+
function numCheckpoints(address account)
57+
public
58+
view
59+
virtual
60+
returns (uint32)
61+
{
62+
return SafeCast.toUint32(_checkpoints[account].length);
63+
}
64+
65+
/// @notice Get the address `account` is currently delegating to.
66+
function delegates(address account) public view virtual returns (address) {
67+
return _delegates[account];
68+
}
69+
70+
/// @notice Gets the current votes balance for `account`.
71+
/// @param account The address to get votes balance
72+
/// @return The number of current votes for `account`
73+
function getVotes(address account) public view returns (uint96) {
74+
uint256 pos = _checkpoints[account].length;
75+
return pos == 0 ? 0 : decodeValue(_checkpoints[account][pos - 1]);
76+
}
77+
78+
/// @notice Determine the prior number of votes for an account as of
79+
/// a block number.
80+
/// @dev Block number must be a finalized block or else this function will
81+
/// revert to prevent misinformation.
82+
/// @param account The address of the account to check
83+
/// @param blockNumber The block number to get the vote balance at
84+
/// @return The number of votes the account had as of the given block
85+
function getPastVotes(address account, uint256 blockNumber)
86+
public
87+
view
88+
returns (uint96)
89+
{
90+
return lookupCheckpoint(_checkpoints[account], blockNumber);
91+
}
92+
93+
/// @notice Retrieve the `totalSupply` at the end of `blockNumber`.
94+
/// Note, this value is the sum of all balances, but it is NOT the
95+
/// sum of all the delegated votes!
96+
/// @param blockNumber The block number to get the total supply at
97+
/// @dev `blockNumber` must have been already mined
98+
function getPastTotalSupply(uint256 blockNumber)
99+
public
100+
view
101+
returns (uint96)
102+
{
103+
return lookupCheckpoint(_totalSupplyCheckpoints, blockNumber);
104+
}
105+
106+
/// @notice Change delegation for `delegator` to `delegatee`.
107+
function delegate(address delegator, address delegatee) internal virtual;
108+
109+
/// @notice Moves voting power from one delegate to another
110+
/// @param src Address of old delegate
111+
/// @param dst Address of new delegate
112+
/// @param amount Voting power amount to transfer between delegates
113+
function moveVotingPower(
114+
address src,
115+
address dst,
116+
uint256 amount
117+
) internal {
118+
if (src != dst && amount > 0) {
119+
if (src != address(0)) {
120+
(uint256 oldWeight, uint256 newWeight) = writeCheckpoint(
121+
_checkpoints[src],
122+
subtract,
123+
amount
124+
);
125+
emit DelegateVotesChanged(src, oldWeight, newWeight);
126+
}
127+
128+
if (dst != address(0)) {
129+
(uint256 oldWeight, uint256 newWeight) = writeCheckpoint(
130+
_checkpoints[dst],
131+
add,
132+
amount
133+
);
134+
emit DelegateVotesChanged(dst, oldWeight, newWeight);
135+
}
136+
}
137+
}
138+
139+
/// @notice Writes a new checkpoint based on operating last stored value
140+
/// with a `delta`. Usually, said operation is the `add` or
141+
/// `subtract` functions from this contract, but more complex
142+
/// functions can be passed as parameters.
143+
/// @param ckpts The checkpoints array to use
144+
/// @param op The function to apply over the last value and the `delta`
145+
/// @param delta Variation with respect to last stored value to be used
146+
/// for new checkpoint
147+
function writeCheckpoint(
148+
uint128[] storage ckpts,
149+
function(uint256, uint256) view returns (uint256) op,
150+
uint256 delta
151+
) internal returns (uint256 oldWeight, uint256 newWeight) {
152+
uint256 pos = ckpts.length;
153+
oldWeight = pos == 0 ? 0 : decodeValue(ckpts[pos - 1]);
154+
newWeight = op(oldWeight, delta);
155+
156+
if (pos > 0) {
157+
uint32 fromBlock = decodeBlockNumber(ckpts[pos - 1]);
158+
if (fromBlock == block.number) {
159+
ckpts[pos - 1] = encodeCheckpoint(
160+
fromBlock,
161+
SafeCast.toUint96(newWeight)
162+
);
163+
return (oldWeight, newWeight);
164+
}
165+
}
166+
167+
ckpts.push(
168+
encodeCheckpoint(
169+
SafeCast.toUint32(block.number),
170+
SafeCast.toUint96(newWeight)
171+
)
172+
);
173+
}
174+
175+
/// @notice Lookup a value in a list of (sorted) checkpoints.
176+
/// @param ckpts The checkpoints array to use
177+
/// @param blockNumber Block number when we want to get the checkpoint at
178+
function lookupCheckpoint(uint128[] storage ckpts, uint256 blockNumber)
179+
internal
180+
view
181+
returns (uint96)
182+
{
183+
// We run a binary search to look for the earliest checkpoint taken
184+
// after `blockNumber`. During the loop, the index of the wanted
185+
// checkpoint remains in the range [low-1, high). With each iteration,
186+
// either `low` or `high` is moved towards the middle of the range to
187+
// maintain the invariant.
188+
// - If the middle checkpoint is after `blockNumber`,
189+
// we look in [low, mid)
190+
// - If the middle checkpoint is before or equal to `blockNumber`,
191+
// we look in [mid+1, high)
192+
// Once we reach a single value (when low == high), we've found the
193+
// right checkpoint at the index high-1, if not out of bounds (in that
194+
// case we're looking too far in the past and the result is 0).
195+
// Note that if the latest checkpoint available is exactly for
196+
// `blockNumber`, we end up with an index that is past the end of the
197+
// array, so we technically don't find a checkpoint after
198+
// `blockNumber`, but it works out the same.
199+
require(blockNumber < block.number, "Block not yet determined");
200+
201+
uint256 high = ckpts.length;
202+
uint256 low = 0;
203+
while (low < high) {
204+
uint256 mid = Math.average(low, high);
205+
uint32 midBlock = decodeBlockNumber(ckpts[mid]);
206+
if (midBlock > blockNumber) {
207+
high = mid;
208+
} else {
209+
low = mid + 1;
210+
}
211+
}
212+
213+
return high == 0 ? 0 : decodeValue(ckpts[high - 1]);
214+
}
215+
216+
/// @notice Maximum token supply. Defaults to `type(uint96).max` (2^96 - 1)
217+
function maxSupply() internal view virtual returns (uint96) {
218+
return type(uint96).max;
219+
}
220+
221+
/// @notice Encodes a `blockNumber` and `value` into a single `uint128`
222+
/// checkpoint.
223+
/// @dev `blockNumber` is stored in the first 32 bits, while `value` in the
224+
/// remaining 96 bits.
225+
function encodeCheckpoint(uint32 blockNumber, uint96 value)
226+
internal
227+
pure
228+
returns (uint128)
229+
{
230+
return (uint128(blockNumber) << 96) | uint128(value);
231+
}
232+
233+
/// @notice Decodes a block number from a `uint128` `checkpoint`.
234+
function decodeBlockNumber(uint128 checkpoint)
235+
internal
236+
pure
237+
returns (uint32)
238+
{
239+
return uint32(bytes4(bytes16(checkpoint)));
240+
}
241+
242+
/// @notice Decodes a voting value from a `uint128` `checkpoint`.
243+
function decodeValue(uint128 checkpoint) internal pure returns (uint96) {
244+
return uint96(checkpoint);
245+
}
246+
247+
/// @notice Decodes a block number and voting value from a `uint128`
248+
/// `checkpoint`.
249+
function decodeCheckpoint(uint128 checkpoint)
250+
internal
251+
pure
252+
returns (uint32 blockNumber, uint96 value)
253+
{
254+
blockNumber = decodeBlockNumber(checkpoint);
255+
value = decodeValue(checkpoint);
256+
}
257+
258+
// slither-disable-next-line dead-code
259+
function add(uint256 a, uint256 b) internal pure returns (uint256) {
260+
return a + b;
261+
}
262+
263+
// slither-disable-next-line dead-code
264+
function subtract(uint256 a, uint256 b) internal pure returns (uint256) {
265+
return a - b;
266+
}
267+
}

0 commit comments

Comments
 (0)