Checkpoints.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. const format = require('../format-lines');
  2. const { OPTS } = require('./Checkpoints.opts.js');
  3. // TEMPLATE
  4. const header = `\
  5. pragma solidity ^0.8.19;
  6. import {Math} from "../math/Math.sol";
  7. /**
  8. * @dev This library defines the \`Trace*\` struct, for checkpointing values as they change at different points in
  9. * time, and later looking up past values by block number. See {Votes} as an example.
  10. *
  11. * To create a history of checkpoints define a variable type \`Checkpoints.Trace*\` in your contract, and store a new
  12. * checkpoint for the current transaction block using the {push} function.
  13. */
  14. `;
  15. const errors = `\
  16. /**
  17. * @dev A value was attempted to be inserted on a past checkpoint.
  18. */
  19. error CheckpointUnorderedInsertion();
  20. `;
  21. const template = opts => `\
  22. struct ${opts.historyTypeName} {
  23. ${opts.checkpointTypeName}[] ${opts.checkpointFieldName};
  24. }
  25. struct ${opts.checkpointTypeName} {
  26. ${opts.keyTypeName} ${opts.keyFieldName};
  27. ${opts.valueTypeName} ${opts.valueFieldName};
  28. }
  29. /**
  30. * @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint.
  31. *
  32. * Returns previous value and new value.
  33. *
  34. * IMPORTANT: Never accept \`key\` as a user input, since an arbitrary \`type(${opts.keyTypeName}).max\` key set will disable the library.
  35. */
  36. function push(
  37. ${opts.historyTypeName} storage self,
  38. ${opts.keyTypeName} key,
  39. ${opts.valueTypeName} value
  40. ) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
  41. return _insert(self.${opts.checkpointFieldName}, key, value);
  42. }
  43. /**
  44. * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none.
  45. */
  46. function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
  47. uint256 len = self.${opts.checkpointFieldName}.length;
  48. uint256 pos = _lowerBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
  49. return pos == len ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos).${opts.valueFieldName};
  50. }
  51. /**
  52. * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none.
  53. */
  54. function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
  55. uint256 len = self.${opts.checkpointFieldName}.length;
  56. uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
  57. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  58. }
  59. /**
  60. * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none.
  61. *
  62. * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys).
  63. */
  64. function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
  65. uint256 len = self.${opts.checkpointFieldName}.length;
  66. uint256 low = 0;
  67. uint256 high = len;
  68. if (len > 5) {
  69. uint256 mid = len - Math.sqrt(len);
  70. if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._key) {
  71. high = mid;
  72. } else {
  73. low = mid + 1;
  74. }
  75. }
  76. uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
  77. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  78. }
  79. /**
  80. * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
  81. */
  82. function latest(${opts.historyTypeName} storage self) internal view returns (${opts.valueTypeName}) {
  83. uint256 pos = self.${opts.checkpointFieldName}.length;
  84. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  85. }
  86. /**
  87. * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value
  88. * in the most recent checkpoint.
  89. */
  90. function latestCheckpoint(${opts.historyTypeName} storage self)
  91. internal
  92. view
  93. returns (
  94. bool exists,
  95. ${opts.keyTypeName} ${opts.keyFieldName},
  96. ${opts.valueTypeName} ${opts.valueFieldName}
  97. )
  98. {
  99. uint256 pos = self.${opts.checkpointFieldName}.length;
  100. if (pos == 0) {
  101. return (false, 0, 0);
  102. } else {
  103. ${opts.checkpointTypeName} memory ckpt = _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1);
  104. return (true, ckpt.${opts.keyFieldName}, ckpt.${opts.valueFieldName});
  105. }
  106. }
  107. /**
  108. * @dev Returns the number of checkpoint.
  109. */
  110. function length(${opts.historyTypeName} storage self) internal view returns (uint256) {
  111. return self.${opts.checkpointFieldName}.length;
  112. }
  113. /**
  114. * @dev Returns checkpoint at given position.
  115. */
  116. function at(${opts.historyTypeName} storage self, uint32 pos) internal view returns (${opts.checkpointTypeName} memory) {
  117. return self.${opts.checkpointFieldName}[pos];
  118. }
  119. /**
  120. * @dev Pushes a (\`key\`, \`value\`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
  121. * or by updating the last one.
  122. */
  123. function _insert(
  124. ${opts.checkpointTypeName}[] storage self,
  125. ${opts.keyTypeName} key,
  126. ${opts.valueTypeName} value
  127. ) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
  128. uint256 pos = self.length;
  129. if (pos > 0) {
  130. // Copying to memory is important here.
  131. ${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1);
  132. // Checkpoint keys must be non-decreasing.
  133. if(last.${opts.keyFieldName} > key) {
  134. revert CheckpointUnorderedInsertion();
  135. }
  136. // Update or push new checkpoint
  137. if (last.${opts.keyFieldName} == key) {
  138. _unsafeAccess(self, pos - 1).${opts.valueFieldName} = value;
  139. } else {
  140. self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
  141. }
  142. return (last.${opts.valueFieldName}, value);
  143. } else {
  144. self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
  145. return (0, value);
  146. }
  147. }
  148. /**
  149. * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or \`high\` if there is none.
  150. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
  151. *
  152. * WARNING: \`high\` should not be greater than the array's length.
  153. */
  154. function _upperBinaryLookup(
  155. ${opts.checkpointTypeName}[] storage self,
  156. ${opts.keyTypeName} key,
  157. uint256 low,
  158. uint256 high
  159. ) private view returns (uint256) {
  160. while (low < high) {
  161. uint256 mid = Math.average(low, high);
  162. if (_unsafeAccess(self, mid).${opts.keyFieldName} > key) {
  163. high = mid;
  164. } else {
  165. low = mid + 1;
  166. }
  167. }
  168. return high;
  169. }
  170. /**
  171. * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or \`high\` if there is none.
  172. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
  173. *
  174. * WARNING: \`high\` should not be greater than the array's length.
  175. */
  176. function _lowerBinaryLookup(
  177. ${opts.checkpointTypeName}[] storage self,
  178. ${opts.keyTypeName} key,
  179. uint256 low,
  180. uint256 high
  181. ) private view returns (uint256) {
  182. while (low < high) {
  183. uint256 mid = Math.average(low, high);
  184. if (_unsafeAccess(self, mid).${opts.keyFieldName} < key) {
  185. low = mid + 1;
  186. } else {
  187. high = mid;
  188. }
  189. }
  190. return high;
  191. }
  192. /**
  193. * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
  194. */
  195. function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
  196. private
  197. pure
  198. returns (${opts.checkpointTypeName} storage result)
  199. {
  200. assembly {
  201. mstore(0, self.slot)
  202. result.slot := add(keccak256(0, 0x20), pos)
  203. }
  204. }
  205. `;
  206. /* eslint-enable max-len */
  207. // GENERATE
  208. module.exports = format(
  209. header.trimEnd(),
  210. 'library Checkpoints {',
  211. errors,
  212. OPTS.flatMap(opts => template(opts)),
  213. '}',
  214. );