Files
34
Lines
6829
Coverage
90%
1775 / 1968
Actions
87%
lib/openzeppelin-contracts/contracts/metatx/ERC2771Context.sol
Lines covered: 7 / 8 (87%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v4.7.0) (metatx/ERC2771Context.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.9; |
|
| 5 | ||
| 6 | import "../utils/Context.sol"; |
|
| 7 | ||
| 8 | /** |
|
| 9 | * @dev Context variant with ERC2771 support. |
|
| 10 | */ |
|
| 11 | abstract contract ERC2771Context is Context { |
|
| 12 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable |
|
| 13 | address private immutable _trustedForwarder; |
|
| 14 | ||
| 15 | /// @custom:oz-upgrades-unsafe-allow constructor |
|
| 16 | 1× | constructor(address trustedForwarder) { |
| 17 | 11× | _trustedForwarder = trustedForwarder; |
| 18 | } |
|
| 19 | ||
| 20 | 12× | function isTrustedForwarder(address forwarder) public view virtual returns (bool) { |
| 21 | 18× | return forwarder == _trustedForwarder; |
| 22 | } |
|
| 23 | ||
| 24 | 10× | function _msgSender() internal view virtual override returns (address sender) { |
| 25 | 18× | if (isTrustedForwarder(msg.sender)) { |
| 26 | // The assembly code is more direct than the Solidity version using `abi.decode`. |
|
| 27 | /// @solidity memory-safe-assembly |
|
| 28 | assembly { |
|
| 29 | 0 | sender := shr(96, calldataload(sub(calldatasize(), 20))) |
| 30 | } |
|
| 31 | } else { |
|
| 32 | 16× | return super._msgSender(); |
| 33 | } |
|
| 34 | } |
|
| 35 | ||
| 36 | function _msgData() internal view virtual override returns (bytes calldata) { |
|
| 37 | if (isTrustedForwarder(msg.sender)) { |
|
| 38 | return msg.data[:msg.data.length - 20]; |
|
| 39 | } else { |
|
| 40 | return super._msgData(); |
|
| 41 | } |
|
| 42 | } |
|
| 43 | } |
8%
lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol
Lines covered: 1 / 12 (8%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/UUPSUpgradeable.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.0; |
|
| 5 | ||
| 6 | import "../../interfaces/draft-IERC1822.sol"; |
|
| 7 | import "../ERC1967/ERC1967Upgrade.sol"; |
|
| 8 | ||
| 9 | /** |
|
| 10 | * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an |
|
| 11 | * {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy. |
|
| 12 | * |
|
| 13 | * A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is |
|
| 14 | * reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing |
|
| 15 | * `UUPSUpgradeable` with a custom implementation of upgrades. |
|
| 16 | * |
|
| 17 | * The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism. |
|
| 18 | * |
|
| 19 | * _Available since v4.1._ |
|
| 20 | */ |
|
| 21 | abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { |
|
| 22 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment |
|
| 23 | 20× | address private immutable __self = address(this); |
| 24 | ||
| 25 | /** |
|
| 26 | * @dev Check that the execution is being performed through a delegatecall call and that the execution context is |
|
| 27 | * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case |
|
| 28 | * for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a |
|
| 29 | * function through ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to |
|
| 30 | * fail. |
|
| 31 | */ |
|
| 32 | modifier onlyProxy() { |
|
| 33 | 0 | require(address(this) != __self, "Function must be called through delegatecall"); |
| 34 | 0 | require(_getImplementation() == __self, "Function must be called through active proxy"); |
| 35 | _; |
|
| 36 | } |
|
| 37 | ||
| 38 | /** |
|
| 39 | * @dev Check that the execution is not being performed through a delegate call. This allows a function to be |
|
| 40 | * callable on the implementing contract but not through proxies. |
|
| 41 | */ |
|
| 42 | modifier notDelegated() { |
|
| 43 | 0 | require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall"); |
| 44 | _; |
|
| 45 | } |
|
| 46 | ||
| 47 | /** |
|
| 48 | * @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the |
|
| 49 | * implementation. It is used to validate the implementation's compatibility when performing an upgrade. |
|
| 50 | * |
|
| 51 | * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks |
|
| 52 | * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this |
|
| 53 | * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier. |
|
| 54 | */ |
|
| 55 | 0 | function proxiableUUID() external view virtual override notDelegated returns (bytes32) { |
| 56 | 0 | return _IMPLEMENTATION_SLOT; |
| 57 | } |
|
| 58 | ||
| 59 | /** |
|
| 60 | * @dev Upgrade the implementation of the proxy to `newImplementation`. |
|
| 61 | * |
|
| 62 | * Calls {_authorizeUpgrade}. |
|
| 63 | * |
|
| 64 | * Emits an {Upgraded} event. |
|
| 65 | * |
|
| 66 | * @custom:oz-upgrades-unsafe-allow-reachable delegatecall |
|
| 67 | */ |
|
| 68 | 0 | function upgradeTo(address newImplementation) public virtual onlyProxy { |
| 69 | 0 | _authorizeUpgrade(newImplementation); |
| 70 | 0 | _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); |
| 71 | } |
|
| 72 | ||
| 73 | /** |
|
| 74 | * @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call |
|
| 75 | * encoded in `data`. |
|
| 76 | * |
|
| 77 | * Calls {_authorizeUpgrade}. |
|
| 78 | * |
|
| 79 | * Emits an {Upgraded} event. |
|
| 80 | * |
|
| 81 | * @custom:oz-upgrades-unsafe-allow-reachable delegatecall |
|
| 82 | */ |
|
| 83 | 0 | function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy { |
| 84 | 0 | _authorizeUpgrade(newImplementation); |
| 85 | 0 | _upgradeToAndCallUUPS(newImplementation, data, true); |
| 86 | } |
|
| 87 | ||
| 88 | /** |
|
| 89 | * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by |
|
| 90 | * {upgradeTo} and {upgradeToAndCall}. |
|
| 91 | * |
|
| 92 | * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. |
|
| 93 | * |
|
| 94 | * ```solidity |
|
| 95 | * function _authorizeUpgrade(address) internal override onlyOwner {} |
|
| 96 | * ``` |
|
| 97 | */ |
|
| 98 | function _authorizeUpgrade(address newImplementation) internal virtual; |
|
| 99 | } |
36%
lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol
Lines covered: 28 / 77 (36%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.0; |
|
| 5 | ||
| 6 | import "./IERC20.sol"; |
|
| 7 | import "./extensions/IERC20Metadata.sol"; |
|
| 8 | import "../../utils/Context.sol"; |
|
| 9 | ||
| 10 | /** |
|
| 11 | * @dev Implementation of the {IERC20} interface. |
|
| 12 | * |
|
| 13 | * This implementation is agnostic to the way tokens are created. This means |
|
| 14 | * that a supply mechanism has to be added in a derived contract using {_mint}. |
|
| 15 | * For a generic mechanism see {ERC20PresetMinterPauser}. |
|
| 16 | * |
|
| 17 | * TIP: For a detailed writeup see our guide |
|
| 18 | * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How |
|
| 19 | * to implement supply mechanisms]. |
|
| 20 | * |
|
| 21 | * The default value of {decimals} is 18. To change this, you should override |
|
| 22 | * this function so it returns a different value. |
|
| 23 | * |
|
| 24 | * We have followed general OpenZeppelin Contracts guidelines: functions revert |
|
| 25 | * instead returning `false` on failure. This behavior is nonetheless |
|
| 26 | * conventional and does not conflict with the expectations of ERC20 |
|
| 27 | * applications. |
|
| 28 | * |
|
| 29 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. |
|
| 30 | * This allows applications to reconstruct the allowance for all accounts just |
|
| 31 | * by listening to said events. Other implementations of the EIP may not emit |
|
| 32 | * these events, as it isn't required by the specification. |
|
| 33 | * |
|
| 34 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} |
|
| 35 | * functions have been added to mitigate the well-known issues around setting |
|
| 36 | * allowances. See {IERC20-approve}. |
|
| 37 | */ |
|
| 38 | 0 | contract ERC20 is Context, IERC20, IERC20Metadata { |
| 39 | mapping(address => uint256) private _balances; |
|
| 40 | ||
| 41 | mapping(address => mapping(address => uint256)) private _allowances; |
|
| 42 | ||
| 43 | uint256 private _totalSupply; |
|
| 44 | ||
| 45 | string private _name; |
|
| 46 | string private _symbol; |
|
| 47 | ||
| 48 | /** |
|
| 49 | * @dev Sets the values for {name} and {symbol}. |
|
| 50 | * |
|
| 51 | * All two of these values are immutable: they can only be set once during |
|
| 52 | * construction. |
|
| 53 | */ |
|
| 54 | 0 | constructor(string memory name_, string memory symbol_) { |
| 55 | 0 | _name = name_; |
| 56 | 0 | _symbol = symbol_; |
| 57 | } |
|
| 58 | ||
| 59 | /** |
|
| 60 | * @dev Returns the name of the token. |
|
| 61 | */ |
|
| 62 | 0 | function name() public view virtual override returns (string memory) { |
| 63 | 0 | return _name; |
| 64 | } |
|
| 65 | ||
| 66 | /** |
|
| 67 | * @dev Returns the symbol of the token, usually a shorter version of the |
|
| 68 | * name. |
|
| 69 | */ |
|
| 70 | 0 | function symbol() public view virtual override returns (string memory) { |
| 71 | 0 | return _symbol; |
| 72 | } |
|
| 73 | ||
| 74 | /** |
|
| 75 | * @dev Returns the number of decimals used to get its user representation. |
|
| 76 | * For example, if `decimals` equals `2`, a balance of `505` tokens should |
|
| 77 | * be displayed to a user as `5.05` (`505 / 10 ** 2`). |
|
| 78 | * |
|
| 79 | * Tokens usually opt for a value of 18, imitating the relationship between |
|
| 80 | * Ether and Wei. This is the default value returned by this function, unless |
|
| 81 | * it's overridden. |
|
| 82 | * |
|
| 83 | * NOTE: This information is only used for _display_ purposes: it in |
|
| 84 | * no way affects any of the arithmetic of the contract, including |
|
| 85 | * {IERC20-balanceOf} and {IERC20-transfer}. |
|
| 86 | */ |
|
| 87 | 0 | function decimals() public view virtual override returns (uint8) { |
| 88 | 0 | return 18; |
| 89 | } |
|
| 90 | ||
| 91 | /** |
|
| 92 | * @dev See {IERC20-totalSupply}. |
|
| 93 | */ |
|
| 94 | 0 | function totalSupply() public view virtual override returns (uint256) { |
| 95 | 0 | return _totalSupply; |
| 96 | } |
|
| 97 | ||
| 98 | /** |
|
| 99 | * @dev See {IERC20-balanceOf}. |
|
| 100 | */ |
|
| 101 | 156× | function balanceOf(address account) public view virtual override returns (uint256) { |
| 102 | 84× | return _balances[account]; |
| 103 | } |
|
| 104 | ||
| 105 | /** |
|
| 106 | * @dev See {IERC20-transfer}. |
|
| 107 | * |
|
| 108 | * Requirements: |
|
| 109 | * |
|
| 110 | * - `to` cannot be the zero address. |
|
| 111 | * - the caller must have a balance of at least `amount`. |
|
| 112 | */ |
|
| 113 | 80× | function transfer(address to, uint256 amount) public virtual override returns (bool) { |
| 114 | 14× | address owner = _msgSender(); |
| 115 | 14× | _transfer(owner, to, amount); |
| 116 | 8× | return true; |
| 117 | } |
|
| 118 | ||
| 119 | /** |
|
| 120 | * @dev See {IERC20-allowance}. |
|
| 121 | */ |
|
| 122 | 14× | function allowance(address owner, address spender) public view virtual override returns (uint256) { |
| 123 | 76× | return _allowances[owner][spender]; |
| 124 | } |
|
| 125 | ||
| 126 | /** |
|
| 127 | * @dev See {IERC20-approve}. |
|
| 128 | * |
|
| 129 | * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on |
|
| 130 | * `transferFrom`. This is semantically equivalent to an infinite approval. |
|
| 131 | * |
|
| 132 | * Requirements: |
|
| 133 | * |
|
| 134 | * - `spender` cannot be the zero address. |
|
| 135 | */ |
|
| 136 | 0 | function approve(address spender, uint256 amount) public virtual override returns (bool) { |
| 137 | 0 | address owner = _msgSender(); |
| 138 | 0 | _approve(owner, spender, amount); |
| 139 | 0 | return true; |
| 140 | } |
|
| 141 | ||
| 142 | /** |
|
| 143 | * @dev See {IERC20-transferFrom}. |
|
| 144 | * |
|
| 145 | * Emits an {Approval} event indicating the updated allowance. This is not |
|
| 146 | * required by the EIP. See the note at the beginning of {ERC20}. |
|
| 147 | * |
|
| 148 | * NOTE: Does not update the allowance if the current allowance |
|
| 149 | * is the maximum `uint256`. |
|
| 150 | * |
|
| 151 | * Requirements: |
|
| 152 | * |
|
| 153 | * - `from` and `to` cannot be the zero address. |
|
| 154 | * - `from` must have a balance of at least `amount`. |
|
| 155 | * - the caller must have allowance for ``from``'s tokens of at least |
|
| 156 | * `amount`. |
|
| 157 | */ |
|
| 158 | 82× | function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { |
| 159 | 14× | address spender = _msgSender(); |
| 160 | 14× | _spendAllowance(from, spender, amount); |
| 161 | 14× | _transfer(from, to, amount); |
| 162 | 8× | return true; |
| 163 | } |
|
| 164 | ||
| 165 | /** |
|
| 166 | * @dev Atomically increases the allowance granted to `spender` by the caller. |
|
| 167 | * |
|
| 168 | * This is an alternative to {approve} that can be used as a mitigation for |
|
| 169 | * problems described in {IERC20-approve}. |
|
| 170 | * |
|
| 171 | * Emits an {Approval} event indicating the updated allowance. |
|
| 172 | * |
|
| 173 | * Requirements: |
|
| 174 | * |
|
| 175 | * - `spender` cannot be the zero address. |
|
| 176 | */ |
|
| 177 | 0 | function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { |
| 178 | 0 | address owner = _msgSender(); |
| 179 | 0 | _approve(owner, spender, allowance(owner, spender) + addedValue); |
| 180 | 0 | return true; |
| 181 | } |
|
| 182 | ||
| 183 | /** |
|
| 184 | * @dev Atomically decreases the allowance granted to `spender` by the caller. |
|
| 185 | * |
|
| 186 | * This is an alternative to {approve} that can be used as a mitigation for |
|
| 187 | * problems described in {IERC20-approve}. |
|
| 188 | * |
|
| 189 | * Emits an {Approval} event indicating the updated allowance. |
|
| 190 | * |
|
| 191 | * Requirements: |
|
| 192 | * |
|
| 193 | * - `spender` cannot be the zero address. |
|
| 194 | * - `spender` must have allowance for the caller of at least |
|
| 195 | * `subtractedValue`. |
|
| 196 | */ |
|
| 197 | 0 | function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { |
| 198 | 0 | address owner = _msgSender(); |
| 199 | 0 | uint256 currentAllowance = allowance(owner, spender); |
| 200 | 0 | require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); |
| 201 | unchecked { |
|
| 202 | 0 | _approve(owner, spender, currentAllowance - subtractedValue); |
| 203 | } |
|
| 204 | ||
| 205 | 0 | return true; |
| 206 | } |
|
| 207 | ||
| 208 | /** |
|
| 209 | * @dev Moves `amount` of tokens from `from` to `to`. |
|
| 210 | * |
|
| 211 | * This internal function is equivalent to {transfer}, and can be used to |
|
| 212 | * e.g. implement automatic token fees, slashing mechanisms, etc. |
|
| 213 | * |
|
| 214 | * Emits a {Transfer} event. |
|
| 215 | * |
|
| 216 | * Requirements: |
|
| 217 | * |
|
| 218 | * - `from` cannot be the zero address. |
|
| 219 | * - `to` cannot be the zero address. |
|
| 220 | * - `from` must have a balance of at least `amount`. |
|
| 221 | */ |
|
| 222 | 18× | function _transfer(address from, address to, uint256 amount) internal virtual { |
| 223 | 30× | require(from != address(0), "ERC20: transfer from the zero address"); |
| 224 | 30× | require(to != address(0), "ERC20: transfer to the zero address"); |
| 225 | ||
| 226 | 21× | _beforeTokenTransfer(from, to, amount); |
| 227 | ||
| 228 | 66× | uint256 fromBalance = _balances[from]; |
| 229 | 40× | require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); |
| 230 | unchecked { |
|
| 231 | 75× | _balances[from] = fromBalance - amount; |
| 232 | // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by |
|
| 233 | // decrementing then incrementing. |
|
| 234 | 93× | _balances[to] += amount; |
| 235 | } |
|
| 236 | ||
| 237 | 69× | emit Transfer(from, to, amount); |
| 238 | ||
| 239 | 21× | _afterTokenTransfer(from, to, amount); |
| 240 | } |
|
| 241 | ||
| 242 | /** @dev Creates `amount` tokens and assigns them to `account`, increasing |
|
| 243 | * the total supply. |
|
| 244 | * |
|
| 245 | * Emits a {Transfer} event with `from` set to the zero address. |
|
| 246 | * |
|
| 247 | * Requirements: |
|
| 248 | * |
|
| 249 | * - `account` cannot be the zero address. |
|
| 250 | */ |
|
| 251 | 0 | function _mint(address account, uint256 amount) internal virtual { |
| 252 | 0 | require(account != address(0), "ERC20: mint to the zero address"); |
| 253 | ||
| 254 | 0 | _beforeTokenTransfer(address(0), account, amount); |
| 255 | ||
| 256 | 0 | _totalSupply += amount; |
| 257 | unchecked { |
|
| 258 | // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. |
|
| 259 | 0 | _balances[account] += amount; |
| 260 | } |
|
| 261 | 0 | emit Transfer(address(0), account, amount); |
| 262 | ||
| 263 | 0 | _afterTokenTransfer(address(0), account, amount); |
| 264 | } |
|
| 265 | ||
| 266 | /** |
|
| 267 | * @dev Destroys `amount` tokens from `account`, reducing the |
|
| 268 | * total supply. |
|
| 269 | * |
|
| 270 | * Emits a {Transfer} event with `to` set to the zero address. |
|
| 271 | * |
|
| 272 | * Requirements: |
|
| 273 | * |
|
| 274 | * - `account` cannot be the zero address. |
|
| 275 | * - `account` must have at least `amount` tokens. |
|
| 276 | */ |
|
| 277 | 0 | function _burn(address account, uint256 amount) internal virtual { |
| 278 | 0 | require(account != address(0), "ERC20: burn from the zero address"); |
| 279 | ||
| 280 | 0 | _beforeTokenTransfer(account, address(0), amount); |
| 281 | ||
| 282 | 0 | uint256 accountBalance = _balances[account]; |
| 283 | 0 | require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); |
| 284 | unchecked { |
|
| 285 | 0 | _balances[account] = accountBalance - amount; |
| 286 | // Overflow not possible: amount <= accountBalance <= totalSupply. |
|
| 287 | 0 | _totalSupply -= amount; |
| 288 | } |
|
| 289 | ||
| 290 | 0 | emit Transfer(account, address(0), amount); |
| 291 | ||
| 292 | 0 | _afterTokenTransfer(account, address(0), amount); |
| 293 | } |
|
| 294 | ||
| 295 | /** |
|
| 296 | * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. |
|
| 297 | * |
|
| 298 | * This internal function is equivalent to `approve`, and can be used to |
|
| 299 | * e.g. set automatic allowances for certain subsystems, etc. |
|
| 300 | * |
|
| 301 | * Emits an {Approval} event. |
|
| 302 | * |
|
| 303 | * Requirements: |
|
| 304 | * |
|
| 305 | * - `owner` cannot be the zero address. |
|
| 306 | * - `spender` cannot be the zero address. |
|
| 307 | */ |
|
| 308 | 0 | function _approve(address owner, address spender, uint256 amount) internal virtual { |
| 309 | 0 | require(owner != address(0), "ERC20: approve from the zero address"); |
| 310 | 0 | require(spender != address(0), "ERC20: approve to the zero address"); |
| 311 | ||
| 312 | 0 | _allowances[owner][spender] = amount; |
| 313 | 0 | emit Approval(owner, spender, amount); |
| 314 | } |
|
| 315 | ||
| 316 | /** |
|
| 317 | * @dev Updates `owner` s allowance for `spender` based on spent `amount`. |
|
| 318 | * |
|
| 319 | * Does not update the allowance amount in case of infinite allowance. |
|
| 320 | * Revert if not enough allowance is available. |
|
| 321 | * |
|
| 322 | * Might emit an {Approval} event. |
|
| 323 | */ |
|
| 324 | 12× | function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { |
| 325 | 18× | uint256 currentAllowance = allowance(owner, spender); |
| 326 | 12× | if (currentAllowance != type(uint256).max) { |
| 327 | 0 | require(currentAllowance >= amount, "ERC20: insufficient allowance"); |
| 328 | unchecked { |
|
| 329 | 0 | _approve(owner, spender, currentAllowance - amount); |
| 330 | } |
|
| 331 | } |
|
| 332 | } |
|
| 333 | ||
| 334 | /** |
|
| 335 | * @dev Hook that is called before any transfer of tokens. This includes |
|
| 336 | * minting and burning. |
|
| 337 | * |
|
| 338 | * Calling conditions: |
|
| 339 | * |
|
| 340 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens |
|
| 341 | * will be transferred to `to`. |
|
| 342 | * - when `from` is zero, `amount` tokens will be minted for `to`. |
|
| 343 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned. |
|
| 344 | * - `from` and `to` are never both zero. |
|
| 345 | * |
|
| 346 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. |
|
| 347 | */ |
|
| 348 | 15× | function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} |
| 349 | ||
| 350 | /** |
|
| 351 | * @dev Hook that is called after any transfer of tokens. This includes |
|
| 352 | * minting and burning. |
|
| 353 | * |
|
| 354 | * Calling conditions: |
|
| 355 | * |
|
| 356 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens |
|
| 357 | * has been transferred to `to`. |
|
| 358 | * - when `from` is zero, `amount` tokens have been minted for `to`. |
|
| 359 | * - when `to` is zero, `amount` of ``from``'s tokens have been burned. |
|
| 360 | * - `from` and `to` are never both zero. |
|
| 361 | * |
|
| 362 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. |
|
| 363 | */ |
|
| 364 | 15× | function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} |
| 365 | } |
33%
lib/openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol
Lines covered: 1 / 3 (33%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/presets/ERC20PresetFixedSupply.sol) |
|
| 3 | pragma solidity ^0.8.0; |
|
| 4 | ||
| 5 | import "../extensions/ERC20Burnable.sol"; |
|
| 6 | ||
| 7 | /** |
|
| 8 | * @dev {ERC20} token, including: |
|
| 9 | * |
|
| 10 | * - Preminted initial supply |
|
| 11 | * - Ability for holders to burn (destroy) their tokens |
|
| 12 | * - No access control mechanism (for minting/pausing) and hence no governance |
|
| 13 | * |
|
| 14 | * This contract uses {ERC20Burnable} to include burn capabilities - head to |
|
| 15 | * its documentation for details. |
|
| 16 | * |
|
| 17 | * _Available since v3.4._ |
|
| 18 | * |
|
| 19 | * _Deprecated in favor of https://wizard.openzeppelin.com/[Contracts Wizard]._ |
|
| 20 | */ |
|
| 21 | 232× | contract ERC20PresetFixedSupply is ERC20Burnable { |
| 22 | /** |
|
| 23 | * @dev Mints `initialSupply` amount of token and transfers them to `owner`. |
|
| 24 | * |
|
| 25 | * See {ERC20-constructor}. |
|
| 26 | */ |
|
| 27 | 0 | constructor(string memory name, string memory symbol, uint256 initialSupply, address owner) ERC20(name, symbol) { |
| 28 | 0 | _mint(owner, initialSupply); |
| 29 | } |
|
| 30 | } |
87%
lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
Lines covered: 7 / 8 (87%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/SafeERC20.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.0; |
|
| 5 | ||
| 6 | import "../IERC20.sol"; |
|
| 7 | import "../extensions/IERC20Permit.sol"; |
|
| 8 | import "../../../utils/Address.sol"; |
|
| 9 | ||
| 10 | /** |
|
| 11 | * @title SafeERC20 |
|
| 12 | * @dev Wrappers around ERC20 operations that throw on failure (when the token |
|
| 13 | * contract returns false). Tokens that return no value (and instead revert or |
|
| 14 | * throw on failure) are also supported, non-reverting calls are assumed to be |
|
| 15 | * successful. |
|
| 16 | * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, |
|
| 17 | * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. |
|
| 18 | */ |
|
| 19 | 0 | library SafeERC20 { |
| 20 | using Address for address; |
|
| 21 | ||
| 22 | /** |
|
| 23 | * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, |
|
| 24 | * non-reverting calls are assumed to be successful. |
|
| 25 | */ |
|
| 26 | 10× | function safeTransfer(IERC20 token, address to, uint256 value) internal { |
| 27 | 108× | _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); |
| 28 | } |
|
| 29 | ||
| 30 | /** |
|
| 31 | * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the |
|
| 32 | * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. |
|
| 33 | */ |
|
| 34 | 12× | function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { |
| 35 | 112× | _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); |
| 36 | } |
|
| 37 | ||
| 38 | /** |
|
| 39 | * @dev Deprecated. This function has issues similar to the ones found in |
|
| 40 | * {IERC20-approve}, and its usage is discouraged. |
|
| 41 | * |
|
| 42 | * Whenever possible, use {safeIncreaseAllowance} and |
|
| 43 | * {safeDecreaseAllowance} instead. |
|
| 44 | */ |
|
| 45 | function safeApprove(IERC20 token, address spender, uint256 value) internal { |
|
| 46 | // safeApprove should only be called when setting an initial allowance, |
|
| 47 | // or when resetting it to zero. To increase and decrease it, use |
|
| 48 | // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' |
|
| 49 | require( |
|
| 50 | (value == 0) || (token.allowance(address(this), spender) == 0), |
|
| 51 | "SafeERC20: approve from non-zero to non-zero allowance" |
|
| 52 | ); |
|
| 53 | _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); |
|
| 54 | } |
|
| 55 | ||
| 56 | /** |
|
| 57 | * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, |
|
| 58 | * non-reverting calls are assumed to be successful. |
|
| 59 | */ |
|
| 60 | function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { |
|
| 61 | uint256 oldAllowance = token.allowance(address(this), spender); |
|
| 62 | _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value)); |
|
| 63 | } |
|
| 64 | ||
| 65 | /** |
|
| 66 | * @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value, |
|
| 67 | * non-reverting calls are assumed to be successful. |
|
| 68 | */ |
|
| 69 | function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { |
|
| 70 | unchecked { |
|
| 71 | uint256 oldAllowance = token.allowance(address(this), spender); |
|
| 72 | require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); |
|
| 73 | _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value)); |
|
| 74 | } |
|
| 75 | } |
|
| 76 | ||
| 77 | /** |
|
| 78 | * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, |
|
| 79 | * non-reverting calls are assumed to be successful. Compatible with tokens that require the approval to be set to |
|
| 80 | * 0 before setting it to a non-zero value. |
|
| 81 | */ |
|
| 82 | function forceApprove(IERC20 token, address spender, uint256 value) internal { |
|
| 83 | bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value); |
|
| 84 | ||
| 85 | if (!_callOptionalReturnBool(token, approvalCall)) { |
|
| 86 | _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0)); |
|
| 87 | _callOptionalReturn(token, approvalCall); |
|
| 88 | } |
|
| 89 | } |
|
| 90 | ||
| 91 | /** |
|
| 92 | * @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`. |
|
| 93 | * Revert on invalid signature. |
|
| 94 | */ |
|
| 95 | function safePermit( |
|
| 96 | IERC20Permit token, |
|
| 97 | address owner, |
|
| 98 | address spender, |
|
| 99 | uint256 value, |
|
| 100 | uint256 deadline, |
|
| 101 | uint8 v, |
|
| 102 | bytes32 r, |
|
| 103 | bytes32 s |
|
| 104 | ) internal { |
|
| 105 | uint256 nonceBefore = token.nonces(owner); |
|
| 106 | token.permit(owner, spender, value, deadline, v, r, s); |
|
| 107 | uint256 nonceAfter = token.nonces(owner); |
|
| 108 | require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed"); |
|
| 109 | } |
|
| 110 | ||
| 111 | /** |
|
| 112 | * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement |
|
| 113 | * on the return value: the return value is optional (but if data is returned, it must not be false). |
|
| 114 | * @param token The token targeted by the call. |
|
| 115 | * @param data The call data (encoded using abi.encode or one of its variants). |
|
| 116 | */ |
|
| 117 | 20× | function _callOptionalReturn(IERC20 token, bytes memory data) private { |
| 118 | // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since |
|
| 119 | // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that |
|
| 120 | // the target address contains contract code and also asserts for success in the low-level call. |
|
| 121 | ||
| 122 | 136× | bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); |
| 123 | 108× | require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); |
| 124 | } |
|
| 125 | ||
| 126 | /** |
|
| 127 | * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement |
|
| 128 | * on the return value: the return value is optional (but if data is returned, it must not be false). |
|
| 129 | * @param token The token targeted by the call. |
|
| 130 | * @param data The call data (encoded using abi.encode or one of its variants). |
|
| 131 | * |
|
| 132 | * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. |
|
| 133 | */ |
|
| 134 | function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { |
|
| 135 | // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since |
|
| 136 | // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false |
|
| 137 | // and not revert is the subcall reverts. |
|
| 138 | ||
| 139 | (bool success, bytes memory returndata) = address(token).call(data); |
|
| 140 | return |
|
| 141 | success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token)); |
|
| 142 | } |
|
| 143 | } |
62%
lib/openzeppelin-contracts/contracts/utils/Address.sol
Lines covered: 18 / 29 (62%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.1; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @dev Collection of functions related to the address type |
|
| 8 | */ |
|
| 9 | 0 | library Address { |
| 10 | /** |
|
| 11 | * @dev Returns true if `account` is a contract. |
|
| 12 | * |
|
| 13 | * [IMPORTANT] |
|
| 14 | * ==== |
|
| 15 | * It is unsafe to assume that an address for which this function returns |
|
| 16 | * false is an externally-owned account (EOA) and not a contract. |
|
| 17 | * |
|
| 18 | * Among others, `isContract` will return false for the following |
|
| 19 | * types of addresses: |
|
| 20 | * |
|
| 21 | * - an externally-owned account |
|
| 22 | * - a contract in construction |
|
| 23 | * - an address where a contract will be created |
|
| 24 | * - an address where a contract lived, but was destroyed |
|
| 25 | * |
|
| 26 | * Furthermore, `isContract` will also return true if the target contract within |
|
| 27 | * the same transaction is already scheduled for destruction by `SELFDESTRUCT`, |
|
| 28 | * which only has an effect at the end of a transaction. |
|
| 29 | * ==== |
|
| 30 | * |
|
| 31 | * [IMPORTANT] |
|
| 32 | * ==== |
|
| 33 | * You shouldn't rely on `isContract` to protect against flash loan attacks! |
|
| 34 | * |
|
| 35 | * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets |
|
| 36 | * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract |
|
| 37 | * constructor. |
|
| 38 | * ==== |
|
| 39 | */ |
|
| 40 | 0 | function isContract(address account) internal view returns (bool) { |
| 41 | // This method relies on extcodesize/address.code.length, which returns 0 |
|
| 42 | // for contracts in construction, since the code is only stored at the end |
|
| 43 | // of the constructor execution. |
|
| 44 | ||
| 45 | 0 | return account.code.length > 0; |
| 46 | } |
|
| 47 | ||
| 48 | /** |
|
| 49 | * @dev Replacement for Solidity's `transfer`: sends `amount` wei to |
|
| 50 | * `recipient`, forwarding all available gas and reverting on errors. |
|
| 51 | * |
|
| 52 | * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost |
|
| 53 | * of certain opcodes, possibly making contracts go over the 2300 gas limit |
|
| 54 | * imposed by `transfer`, making them unable to receive funds via |
|
| 55 | * `transfer`. {sendValue} removes this limitation. |
|
| 56 | * |
|
| 57 | * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. |
|
| 58 | * |
|
| 59 | * IMPORTANT: because control is transferred to `recipient`, care must be |
|
| 60 | * taken to not create reentrancy vulnerabilities. Consider using |
|
| 61 | * {ReentrancyGuard} or the |
|
| 62 | * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. |
|
| 63 | */ |
|
| 64 | function sendValue(address payable recipient, uint256 amount) internal { |
|
| 65 | require(address(this).balance >= amount, "Address: insufficient balance"); |
|
| 66 | ||
| 67 | (bool success, ) = recipient.call{value: amount}(""); |
|
| 68 | require(success, "Address: unable to send value, recipient may have reverted"); |
|
| 69 | } |
|
| 70 | ||
| 71 | /** |
|
| 72 | * @dev Performs a Solidity function call using a low level `call`. A |
|
| 73 | * plain `call` is an unsafe replacement for a function call: use this |
|
| 74 | * function instead. |
|
| 75 | * |
|
| 76 | * If `target` reverts with a revert reason, it is bubbled up by this |
|
| 77 | * function (like regular Solidity function calls). |
|
| 78 | * |
|
| 79 | * Returns the raw returned data. To convert to the expected return value, |
|
| 80 | * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. |
|
| 81 | * |
|
| 82 | * Requirements: |
|
| 83 | * |
|
| 84 | * - `target` must be a contract. |
|
| 85 | * - calling `target` with `data` must not revert. |
|
| 86 | * |
|
| 87 | * _Available since v3.1._ |
|
| 88 | */ |
|
| 89 | function functionCall(address target, bytes memory data) internal returns (bytes memory) { |
|
| 90 | return functionCallWithValue(target, data, 0, "Address: low-level call failed"); |
|
| 91 | } |
|
| 92 | ||
| 93 | /** |
|
| 94 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with |
|
| 95 | * `errorMessage` as a fallback revert reason when `target` reverts. |
|
| 96 | * |
|
| 97 | * _Available since v3.1._ |
|
| 98 | */ |
|
| 99 | 28× | function functionCall( |
| 100 | address target, |
|
| 101 | bytes memory data, |
|
| 102 | string memory errorMessage |
|
| 103 | 4× | ) internal returns (bytes memory) { |
| 104 | 40× | return functionCallWithValue(target, data, 0, errorMessage); |
| 105 | } |
|
| 106 | ||
| 107 | /** |
|
| 108 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
|
| 109 | * but also transferring `value` wei to `target`. |
|
| 110 | * |
|
| 111 | * Requirements: |
|
| 112 | * |
|
| 113 | * - the calling contract must have an ETH balance of at least `value`. |
|
| 114 | * - the called Solidity function must be `payable`. |
|
| 115 | * |
|
| 116 | * _Available since v3.1._ |
|
| 117 | */ |
|
| 118 | function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { |
|
| 119 | return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); |
|
| 120 | } |
|
| 121 | ||
| 122 | /** |
|
| 123 | * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but |
|
| 124 | * with `errorMessage` as a fallback revert reason when `target` reverts. |
|
| 125 | * |
|
| 126 | * _Available since v3.1._ |
|
| 127 | */ |
|
| 128 | 32× | function functionCallWithValue( |
| 129 | address target, |
|
| 130 | bytes memory data, |
|
| 131 | uint256 value, |
|
| 132 | string memory errorMessage |
|
| 133 | 4× | ) internal returns (bytes memory) { |
| 134 | 28× | require(address(this).balance >= value, "Address: insufficient balance for call"); |
| 135 | 272× | (bool success, bytes memory returndata) = target.call{value: value}(data); |
| 136 | 48× | return verifyCallResultFromTarget(target, success, returndata, errorMessage); |
| 137 | } |
|
| 138 | ||
| 139 | /** |
|
| 140 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
|
| 141 | * but performing a static call. |
|
| 142 | * |
|
| 143 | * _Available since v3.3._ |
|
| 144 | */ |
|
| 145 | function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { |
|
| 146 | return functionStaticCall(target, data, "Address: low-level static call failed"); |
|
| 147 | } |
|
| 148 | ||
| 149 | /** |
|
| 150 | * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], |
|
| 151 | * but performing a static call. |
|
| 152 | * |
|
| 153 | * _Available since v3.3._ |
|
| 154 | */ |
|
| 155 | function functionStaticCall( |
|
| 156 | address target, |
|
| 157 | bytes memory data, |
|
| 158 | string memory errorMessage |
|
| 159 | ) internal view returns (bytes memory) { |
|
| 160 | (bool success, bytes memory returndata) = target.staticcall(data); |
|
| 161 | return verifyCallResultFromTarget(target, success, returndata, errorMessage); |
|
| 162 | } |
|
| 163 | ||
| 164 | /** |
|
| 165 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
|
| 166 | * but performing a delegate call. |
|
| 167 | * |
|
| 168 | * _Available since v3.4._ |
|
| 169 | */ |
|
| 170 | 0 | function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { |
| 171 | 0 | return functionDelegateCall(target, data, "Address: low-level delegate call failed"); |
| 172 | } |
|
| 173 | ||
| 174 | /** |
|
| 175 | * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], |
|
| 176 | * but performing a delegate call. |
|
| 177 | * |
|
| 178 | * _Available since v3.4._ |
|
| 179 | */ |
|
| 180 | 0 | function functionDelegateCall( |
| 181 | address target, |
|
| 182 | bytes memory data, |
|
| 183 | string memory errorMessage |
|
| 184 | 0 | ) internal returns (bytes memory) { |
| 185 | 0 | (bool success, bytes memory returndata) = target.delegatecall(data); |
| 186 | 0 | return verifyCallResultFromTarget(target, success, returndata, errorMessage); |
| 187 | } |
|
| 188 | ||
| 189 | /** |
|
| 190 | * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling |
|
| 191 | * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. |
|
| 192 | * |
|
| 193 | * _Available since v4.8._ |
|
| 194 | */ |
|
| 195 | 36× | function verifyCallResultFromTarget( |
| 196 | address target, |
|
| 197 | bool success, |
|
| 198 | bytes memory returndata, |
|
| 199 | string memory errorMessage |
|
| 200 | 4× | ) internal view returns (bytes memory) { |
| 201 | 17× | if (success) { |
| 202 | 28× | if (returndata.length == 0) { |
| 203 | // only check isContract if the call was successful and the return data is empty |
|
| 204 | // otherwise we already know that it was a contract |
|
| 205 | 0 | require(isContract(target), "Address: call to non-contract"); |
| 206 | } |
|
| 207 | 20× | return returndata; |
| 208 | } else { |
|
| 209 | 5× | _revert(returndata, errorMessage); |
| 210 | } |
|
| 211 | } |
|
| 212 | ||
| 213 | /** |
|
| 214 | * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the |
|
| 215 | * revert reason or using the provided one. |
|
| 216 | * |
|
| 217 | * _Available since v4.3._ |
|
| 218 | */ |
|
| 219 | function verifyCallResult( |
|
| 220 | bool success, |
|
| 221 | bytes memory returndata, |
|
| 222 | string memory errorMessage |
|
| 223 | ) internal pure returns (bytes memory) { |
|
| 224 | if (success) { |
|
| 225 | return returndata; |
|
| 226 | } else { |
|
| 227 | _revert(returndata, errorMessage); |
|
| 228 | } |
|
| 229 | } |
|
| 230 | ||
| 231 | 1× | function _revert(bytes memory returndata, string memory errorMessage) private pure { |
| 232 | // Look for revert reason and bubble it up if present |
|
| 233 | 7× | if (returndata.length > 0) { |
| 234 | // The easiest way to bubble the revert reason is using memory via assembly |
|
| 235 | /// @solidity memory-safe-assembly |
|
| 236 | assembly { |
|
| 237 | 2× | let returndata_size := mload(returndata) |
| 238 | 5× | revert(add(32, returndata), returndata_size) |
| 239 | } |
|
| 240 | } else { |
|
| 241 | 0 | revert(errorMessage); |
| 242 | } |
|
| 243 | } |
|
| 244 | } |
100%
lib/openzeppelin-contracts/contracts/utils/Context.sol
Lines covered: 2 / 2 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.0; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @dev Provides information about the current execution context, including the |
|
| 8 | * sender of the transaction and its data. While these are generally available |
|
| 9 | * via msg.sender and msg.data, they should not be accessed in such a direct |
|
| 10 | * manner, since when dealing with meta-transactions the account sending and |
|
| 11 | * paying for execution may not be the actual sender (as far as an application |
|
| 12 | * is concerned). |
|
| 13 | * |
|
| 14 | * This contract is only required for intermediate, library-like contracts. |
|
| 15 | */ |
|
| 16 | abstract contract Context { |
|
| 17 | 20× | function _msgSender() internal view virtual returns (address) { |
| 18 | 15× | return msg.sender; |
| 19 | } |
|
| 20 | ||
| 21 | function _msgData() internal view virtual returns (bytes calldata) { |
|
| 22 | return msg.data; |
|
| 23 | } |
|
| 24 | } |
81%
src/AddressDriver.sol
Lines covered: 26 / 32 (81%)
| 1 | // SPDX-License-Identifier: GPL-3.0-only |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import {AccountMetadata, Drips, StreamReceiver, IERC20, SplitsReceiver} from "./Drips.sol"; |
|
| 5 | import {Managed} from "./Managed.sol"; |
|
| 6 | import {DriverTransferUtils} from "./DriverTransferUtils.sol"; |
|
| 7 | ||
| 8 | /// @notice A Drips driver implementing address-based account identification. |
|
| 9 | /// Each address can use `AddressDriver` to control a single account ID derived from that address. |
|
| 10 | /// No registration is required, an `AddressDriver`-based account ID |
|
| 11 | /// for each address is available upfront. |
|
| 12 | 0 | contract AddressDriver is DriverTransferUtils, Managed { |
| 13 | /// @notice The Drips address used by this driver. |
|
| 14 | 0 | Drips public immutable drips; |
| 15 | /// @notice The driver ID which this driver uses when calling Drips. |
|
| 16 | 0 | uint32 public immutable driverId; |
| 17 | ||
| 18 | /// @param drips_ The Drips contract to use. |
|
| 19 | /// @param forwarder The ERC-2771 forwarder to trust. May be the zero address. |
|
| 20 | /// @param driverId_ The driver ID to use when calling Drips. |
|
| 21 | 4× | constructor(Drips drips_, address forwarder, uint32 driverId_) DriverTransferUtils(forwarder) { |
| 22 | 11× | drips = drips_; |
| 23 | 11× | driverId = driverId_; |
| 24 | } |
|
| 25 | ||
| 26 | /// @notice Calculates the account ID for an address. |
|
| 27 | /// Every account ID is a 256-bit integer constructed by concatenating: |
|
| 28 | /// `driverId (32 bits) | zeros (64 bits) | addr (160 bits)`. |
|
| 29 | /// @param addr The address |
|
| 30 | /// @return accountId The account ID |
|
| 31 | 138× | function calcAccountId(address addr) public view returns (uint256 accountId) { |
| 32 | // By assignment we get `accountId` value: |
|
| 33 | // `zeros (224 bits) | driverId (32 bits)` |
|
| 34 | 15× | accountId = driverId; |
| 35 | // By bit shifting we get `accountId` value: |
|
| 36 | // `driverId (32 bits) | zeros (224 bits)` |
|
| 37 | // By bit masking we get `accountId` value: |
|
| 38 | // `driverId (32 bits) | zeros (64 bits) | addr (160 bits)` |
|
| 39 | 30× | accountId = (accountId << 224) | uint160(addr); |
| 40 | } |
|
| 41 | ||
| 42 | /// @notice Calculates the account ID for the message sender |
|
| 43 | /// @return accountId The account ID |
|
| 44 | 8× | function _callerAccountId() internal view returns (uint256 accountId) { |
| 45 | 20× | return calcAccountId(_msgSender()); |
| 46 | } |
|
| 47 | ||
| 48 | /// @notice Collects the account's received already split funds |
|
| 49 | /// and transfers them out of the Drips contract. |
|
| 50 | /// @param erc20 The used ERC-20 token. |
|
| 51 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 52 | /// an address, then later the same amount must be transferable from that address. |
|
| 53 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 54 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 55 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 56 | /// @param transferTo The address to send collected funds to |
|
| 57 | /// @return amt The collected amount |
|
| 58 | 47× | function collect(IERC20 erc20, address transferTo) public whenNotPaused returns (uint128 amt) { |
| 59 | 13× | return _collectAndTransfer(drips, _callerAccountId(), erc20, transferTo); |
| 60 | } |
|
| 61 | ||
| 62 | /// @notice Gives funds from the message sender to the receiver. |
|
| 63 | /// The receiver can split and collect them immediately. |
|
| 64 | /// Transfers the funds to be given from the message sender's wallet to the Drips contract. |
|
| 65 | /// @param receiver The receiver account ID. |
|
| 66 | /// @param erc20 The used ERC-20 token. |
|
| 67 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 68 | /// an address, then later the same amount must be transferable from that address. |
|
| 69 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 70 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 71 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 72 | /// @param amt The given amount |
|
| 73 | 31× | function give(uint256 receiver, IERC20 erc20, uint128 amt) public whenNotPaused { |
| 74 | 12× | _giveAndTransfer(drips, _callerAccountId(), receiver, erc20, amt); |
| 75 | } |
|
| 76 | ||
| 77 | /// @notice Sets the message sender's streams configuration. |
|
| 78 | /// Transfers funds between the message sender's wallet and the Drips contract |
|
| 79 | /// to fulfil the change of the streams balance. |
|
| 80 | /// @param erc20 The used ERC-20 token. |
|
| 81 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 82 | /// an address, then later the same amount must be transferable from that address. |
|
| 83 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 84 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 85 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 86 | /// @param currReceivers The current streams receivers list. |
|
| 87 | /// It must be exactly the same as the last list set for the sender with `setStreams`. |
|
| 88 | /// If this is the first update, pass an empty array. |
|
| 89 | /// @param balanceDelta The streams balance change to be applied. |
|
| 90 | /// If it's positive, the balance is increased by `balanceDelta`. |
|
| 91 | /// If it's zero, the balance doesn't change. |
|
| 92 | /// If it's negative, the balance is decreased by `balanceDelta`, |
|
| 93 | /// but the change is capped at the current balance amount, so it doesn't go below 0. |
|
| 94 | /// Passing `type(int128).min` always decreases the current balance to 0. |
|
| 95 | /// @param newReceivers The list of the streams receivers of the sender to be set. |
|
| 96 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 97 | /// without identical elements and without 0 amtPerSecs. |
|
| 98 | /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 99 | /// The first hint for finding the maximum end time when all streams stop due to funds |
|
| 100 | /// running out after the balance is updated and the new receivers list is applied. |
|
| 101 | /// Hints have no effect on the results of calling this function, except potentially saving gas. |
|
| 102 | /// Hints are Unix timestamps used as the starting points for binary search for the time |
|
| 103 | /// when funds run out in the range of timestamps from the current block's to `2^32`. |
|
| 104 | /// Hints lower than the current timestamp are ignored. |
|
| 105 | /// You can provide zero, one or two hints. The order of hints doesn't matter. |
|
| 106 | /// Hints are the most effective when one of them is lower than or equal to |
|
| 107 | /// the last timestamp when funds are still streamed, and the other one is strictly larger |
|
| 108 | /// than that timestamp,the smaller the difference between such hints, the higher gas savings. |
|
| 109 | /// The savings are the highest possible when one of the hints is equal to |
|
| 110 | /// the last timestamp when funds are still streamed, and the other one is larger by 1. |
|
| 111 | /// It's worth noting that the exact timestamp of the block in which this function is executed |
|
| 112 | /// may affect correctness of the hints, especially if they're precise. |
|
| 113 | /// Hints don't provide any benefits when balance is not enough to cover |
|
| 114 | /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. |
|
| 115 | /// Even inaccurate hints can be useful, and providing a single hint |
|
| 116 | /// or two hints that don't enclose the time when funds run out can still save some gas. |
|
| 117 | /// Providing poor hints that don't reduce the number of binary search steps |
|
| 118 | /// may cause slightly higher gas usage than not providing any hints. |
|
| 119 | /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 120 | /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. |
|
| 121 | /// @param transferTo The address to send funds to in case of decreasing balance |
|
| 122 | /// @return realBalanceDelta The actually applied streams balance change. |
|
| 123 | /// It's equal to the passed `balanceDelta`, unless it's negative |
|
| 124 | /// and it gets capped at the current balance amount. |
|
| 125 | 106× | function setStreams( |
| 126 | IERC20 erc20, |
|
| 127 | StreamReceiver[] calldata currReceivers, |
|
| 128 | int128 balanceDelta, |
|
| 129 | StreamReceiver[] calldata newReceivers, |
|
| 130 | // slither-disable-next-line similar-names |
|
| 131 | uint32 maxEndHint1, |
|
| 132 | uint32 maxEndHint2, |
|
| 133 | address transferTo |
|
| 134 | 2× | ) public whenNotPaused returns (int128 realBalanceDelta) { |
| 135 | 12× | return _setStreamsAndTransfer( |
| 136 | 2× | drips, |
| 137 | 8× | _callerAccountId(), |
| 138 | 2× | erc20, |
| 139 | 4× | currReceivers, |
| 140 | 2× | balanceDelta, |
| 141 | 4× | newReceivers, |
| 142 | 2× | maxEndHint1, |
| 143 | 2× | maxEndHint2, |
| 144 | 2× | transferTo |
| 145 | ); |
|
| 146 | } |
|
| 147 | ||
| 148 | /// @notice Sets the account splits configuration. |
|
| 149 | /// The configuration is common for all ERC-20 tokens. |
|
| 150 | /// Nothing happens to the currently splittable funds, but when they are split |
|
| 151 | /// after this function finishes, the new splits configuration will be used. |
|
| 152 | /// Because anybody can call `split` on `Drips`, calling this function may be frontrun |
|
| 153 | /// and all the currently splittable funds will be split using the old splits configuration. |
|
| 154 | /// @param receivers The list of the account's splits receivers to be set. |
|
| 155 | /// Must be sorted by the account IDs, without duplicate account IDs and without 0 weights. |
|
| 156 | /// Each splits receiver will be getting `weight / TOTAL_SPLITS_WEIGHT` |
|
| 157 | /// share of the funds collected by the account. |
|
| 158 | /// If the sum of weights of all receivers is less than `_TOTAL_SPLITS_WEIGHT`, |
|
| 159 | /// some funds won't be split, but they will be left for the account to collect. |
|
| 160 | /// Fractions of tokens are always rounder either up or down depending on the amount |
|
| 161 | /// being split, the receiver's position on the list and the other receivers' weights. |
|
| 162 | /// It's valid to include the account's own `accountId` in the list of receivers, |
|
| 163 | /// but funds split to themselves return to their splittable balance and are not collectable. |
|
| 164 | /// This is usually unwanted, because if splitting is repeated, |
|
| 165 | /// funds split to themselves will be again split using the current configuration. |
|
| 166 | /// Splitting 100% to self effectively blocks splitting unless the configuration is updated. |
|
| 167 | 60× | function setSplits(SplitsReceiver[] calldata receivers) public whenNotPaused { |
| 168 | 125× | drips.setSplits(_callerAccountId(), receivers); |
| 169 | } |
|
| 170 | ||
| 171 | /// @notice Emits the account metadata for the message sender. |
|
| 172 | /// The keys and the values are not standardized by the protocol, it's up to the users |
|
| 173 | /// to establish and follow conventions to ensure compatibility with the consumers. |
|
| 174 | /// @param accountMetadata The list of account metadata. |
|
| 175 | 0 | function emitAccountMetadata(AccountMetadata[] calldata accountMetadata) public whenNotPaused { |
| 176 | 0 | if (accountMetadata.length != 0) { |
| 177 | 0 | drips.emitAccountMetadata(_callerAccountId(), accountMetadata); |
| 178 | } |
|
| 179 | } |
|
| 180 | } |
80%
src/Drips.sol
Lines covered: 117 / 146 (80%)
| 1 | // SPDX-License-Identifier: GPL-3.0-only |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import { |
|
| 5 | Streams, StreamConfig, StreamsHistory, StreamConfigImpl, StreamReceiver |
|
| 6 | } from "./Streams.sol"; |
|
| 7 | import {Managed} from "./Managed.sol"; |
|
| 8 | import {Splits, SplitsReceiver} from "./Splits.sol"; |
|
| 9 | import {IERC20, SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; |
|
| 10 | ||
| 11 | using SafeERC20 for IERC20; |
|
| 12 | ||
| 13 | /// @notice The account metadata. |
|
| 14 | /// The key and the value are not standardized by the protocol, it's up to the users |
|
| 15 | /// to establish and follow conventions to ensure compatibility with the consumers. |
|
| 16 | struct AccountMetadata { |
|
| 17 | /// @param key The metadata key |
|
| 18 | bytes32 key; |
|
| 19 | /// @param value The metadata value |
|
| 20 | bytes value; |
|
| 21 | } |
|
| 22 | ||
| 23 | /// @notice Drips protocol contract. Automatically streams and splits funds between accounts. |
|
| 24 | /// |
|
| 25 | /// The account can transfer some funds to their streams balance in the contract |
|
| 26 | /// and configure a list of receivers, to whom they want to stream these funds. |
|
| 27 | /// As soon as the streams balance is enough to cover at least 1 second of streaming |
|
| 28 | /// to the configured receivers, the funds start streaming automatically. |
|
| 29 | /// Every second funds are deducted from the streams balance and moved to their receivers. |
|
| 30 | /// The process stops automatically when the streams balance is not enough to cover another second. |
|
| 31 | /// |
|
| 32 | /// Every account has a receiver balance, in which they have funds received from other accounts. |
|
| 33 | /// The streamed funds are added to the receiver balances in global cycles. |
|
| 34 | /// Every `cycleSecs` seconds the protocol adds streamed funds to the receivers' balances, |
|
| 35 | /// so recently streamed funds may not be receivable immediately. |
|
| 36 | /// `cycleSecs` is a constant configured when the Drips contract is deployed. |
|
| 37 | /// The receiver balance is independent from the streams balance, |
|
| 38 | /// to stream received funds they need to be first collected and then added to the streams balance. |
|
| 39 | /// |
|
| 40 | /// The account can share collected funds with other accounts by using splits. |
|
| 41 | /// When collecting, the account gives each of their splits receivers |
|
| 42 | /// a fraction of the received funds. |
|
| 43 | /// Funds received from splits are available for collection immediately regardless of the cycle. |
|
| 44 | /// They aren't exempt from being split, so they too can be split when collected. |
|
| 45 | /// Accounts can build chains and networks of splits between each other. |
|
| 46 | /// Anybody can request collection of funds for any account, |
|
| 47 | /// which can be used to enforce the flow of funds in the network of splits. |
|
| 48 | /// |
|
| 49 | /// The concept of something happening periodically, e.g. every second or every `cycleSecs` are |
|
| 50 | /// only high-level abstractions for the account, Ethereum isn't really capable of scheduling work. |
|
| 51 | /// The actual implementation emulates that behavior by calculating the results of the scheduled |
|
| 52 | /// events based on how many seconds have passed and only when the account needs their outcomes. |
|
| 53 | /// |
|
| 54 | /// The contract can store at most `type(int128).max` which is `2 ^ 127 - 1` units of each token. |
|
| 55 | 0 | contract Drips is Managed, Streams, Splits { |
| 56 | /// @notice Maximum number of streams receivers of a single account. |
|
| 57 | /// Limits cost of changes in streams configuration. |
|
| 58 | 0 | uint256 public constant MAX_STREAMS_RECEIVERS = _MAX_STREAMS_RECEIVERS; |
| 59 | /// @notice The additional decimals for all amtPerSec values. |
|
| 60 | 0 | uint8 public constant AMT_PER_SEC_EXTRA_DECIMALS = _AMT_PER_SEC_EXTRA_DECIMALS; |
| 61 | /// @notice The multiplier for all amtPerSec values. |
|
| 62 | 0 | uint160 public constant AMT_PER_SEC_MULTIPLIER = _AMT_PER_SEC_MULTIPLIER; |
| 63 | /// @notice Maximum number of splits receivers of a single account. |
|
| 64 | /// Limits the cost of splitting. |
|
| 65 | 0 | uint256 public constant MAX_SPLITS_RECEIVERS = _MAX_SPLITS_RECEIVERS; |
| 66 | /// @notice The total splits weight of an account |
|
| 67 | 60× | uint32 public constant TOTAL_SPLITS_WEIGHT = _TOTAL_SPLITS_WEIGHT; |
| 68 | /// @notice The offset of the controlling driver ID in the account ID. |
|
| 69 | /// In other words the controlling driver ID is the highest 32 bits of the account ID. |
|
| 70 | /// Every account ID is a 256-bit integer constructed by concatenating: |
|
| 71 | /// `driverId (32 bits) | driverCustomData (224 bits)`. |
|
| 72 | 6× | uint8 public constant DRIVER_ID_OFFSET = 224; |
| 73 | /// @notice The total amount the protocol can store of each token. |
|
| 74 | /// It's the minimum of _MAX_STREAMS_BALANCE and _MAX_SPLITS_BALANCE. |
|
| 75 | 0 | uint128 public constant MAX_TOTAL_BALANCE = _MAX_STREAMS_BALANCE; |
| 76 | /// @notice On every timestamp `T`, which is a multiple of `cycleSecs`, the receivers |
|
| 77 | /// gain access to steams received during `T - cycleSecs` to `T - 1`. |
|
| 78 | /// Always higher than 1. |
|
| 79 | 0 | uint32 public immutable cycleSecs; |
| 80 | /// @notice The minimum amtPerSec of a stream. It's 1 token per cycle. |
|
| 81 | 62× | uint160 public immutable minAmtPerSec; |
| 82 | /// @notice The ERC-1967 storage slot holding a single `DripsStorage` structure. |
|
| 83 | 30× | bytes32 private immutable _dripsStorageSlot = _erc1967Slot("eip1967.drips.storage"); |
| 84 | ||
| 85 | /// @notice Emitted when a driver is registered |
|
| 86 | /// @param driverId The driver ID |
|
| 87 | /// @param driverAddr The driver address |
|
| 88 | event DriverRegistered(uint32 indexed driverId, address indexed driverAddr); |
|
| 89 | ||
| 90 | /// @notice Emitted when a driver address is updated |
|
| 91 | /// @param driverId The driver ID |
|
| 92 | /// @param oldDriverAddr The old driver address |
|
| 93 | /// @param newDriverAddr The new driver address |
|
| 94 | event DriverAddressUpdated( |
|
| 95 | uint32 indexed driverId, address indexed oldDriverAddr, address indexed newDriverAddr |
|
| 96 | ); |
|
| 97 | ||
| 98 | /// @notice Emitted when funds are withdrawn. |
|
| 99 | /// @param erc20 The used ERC-20 token. |
|
| 100 | /// @param receiver The address that the funds are sent to. |
|
| 101 | /// @param amt The withdrawn amount. |
|
| 102 | event Withdrawn(IERC20 indexed erc20, address indexed receiver, uint256 amt); |
|
| 103 | ||
| 104 | /// @notice Emitted by the account to broadcast metadata. |
|
| 105 | /// The key and the value are not standardized by the protocol, it's up to the users |
|
| 106 | /// to establish and follow conventions to ensure compatibility with the consumers. |
|
| 107 | /// @param accountId The ID of the account emitting metadata |
|
| 108 | /// @param key The metadata key |
|
| 109 | /// @param value The metadata value |
|
| 110 | event AccountMetadataEmitted(uint256 indexed accountId, bytes32 indexed key, bytes value); |
|
| 111 | ||
| 112 | struct DripsStorage { |
|
| 113 | /// @notice The next driver ID that will be used when registering. |
|
| 114 | uint32 nextDriverId; |
|
| 115 | /// @notice Driver addresses. |
|
| 116 | mapping(uint32 driverId => address) driverAddresses; |
|
| 117 | /// @notice The balance of each token currently stored in the protocol. |
|
| 118 | mapping(IERC20 erc20 => Balance) balances; |
|
| 119 | } |
|
| 120 | ||
| 121 | /// @notice The balance currently stored in the protocol. |
|
| 122 | struct Balance { |
|
| 123 | /// @notice The balance currently stored in the protocol in streaming. |
|
| 124 | /// It's the sum of all the funds of all the users |
|
| 125 | /// that are in the streams balances, are squeezable or are receivable. |
|
| 126 | uint128 streams; |
|
| 127 | /// @notice The balance currently stored in the protocol in splitting. |
|
| 128 | /// It's the sum of all the funds of all the users that are splittable or are collectable. |
|
| 129 | uint128 splits; |
|
| 130 | } |
|
| 131 | ||
| 132 | /// @param cycleSecs_ The length of cycleSecs to be used in the contract instance. |
|
| 133 | /// Low value makes funds more available by shortening the average time |
|
| 134 | /// of funds being frozen between being taken from the accounts' |
|
| 135 | /// streams balance and being receivable by their receivers. |
|
| 136 | /// High value makes receiving cheaper by making it process less cycles for a given time range. |
|
| 137 | /// Must be higher than 1. |
|
| 138 | 1× | constructor(uint32 cycleSecs_) |
| 139 | 26× | Streams(cycleSecs_, _erc1967Slot("eip1967.streams.storage")) |
| 140 | 25× | Splits(_erc1967Slot("eip1967.splits.storage")) |
| 141 | { |
|
| 142 | 12× | cycleSecs = Streams._cycleSecs; |
| 143 | 12× | minAmtPerSec = Streams._minAmtPerSec; |
| 144 | } |
|
| 145 | ||
| 146 | /// @notice A modifier making functions callable only by the driver controlling the account. |
|
| 147 | /// @param accountId The account ID. |
|
| 148 | 6× | modifier onlyDriver(uint256 accountId) { |
| 149 | // `accountId` has value: |
|
| 150 | // `driverId (32 bits) | driverCustomData (224 bits)` |
|
| 151 | // By bit shifting we get value: |
|
| 152 | // `zeros (224 bits) | driverId (32 bits)` |
|
| 153 | // By casting down we get value: |
|
| 154 | // `driverId (32 bits)` |
|
| 155 | 48× | uint32 driverId = uint32(accountId >> DRIVER_ID_OFFSET); |
| 156 | 30× | _assertCallerIsDriver(driverId); |
| 157 | _; |
|
| 158 | } |
|
| 159 | ||
| 160 | /// @notice Verifies that the caller controls the given driver ID and reverts otherwise. |
|
| 161 | /// @param driverId The driver ID. |
|
| 162 | 6× | function _assertCallerIsDriver(uint32 driverId) internal view { |
| 163 | 28× | require(driverAddress(driverId) == msg.sender, "Callable only by the driver"); |
| 164 | } |
|
| 165 | ||
| 166 | /// @notice Registers a driver. |
|
| 167 | /// The driver is assigned a unique ID and a range of account IDs it can control. |
|
| 168 | /// That range consists of all 2^224 account IDs with highest 32 bits equal to the driver ID. |
|
| 169 | /// Every account ID is a 256-bit integer constructed by concatenating: |
|
| 170 | /// `driverId (32 bits) | driverCustomData (224 bits)`. |
|
| 171 | /// Every driver ID is assigned only to a single address, |
|
| 172 | /// but a single address can have multiple driver IDs assigned to it. |
|
| 173 | /// @param driverAddr The address of the driver. Must not be zero address. |
|
| 174 | /// It should be a smart contract capable of dealing with the Drips API. |
|
| 175 | /// It shouldn't be an EOA because the API requires making multiple calls per transaction. |
|
| 176 | /// @return driverId The registered driver ID. |
|
| 177 | 0 | function registerDriver(address driverAddr) public whenNotPaused returns (uint32 driverId) { |
| 178 | 0 | require(driverAddr != address(0), "Driver registered for 0 address"); |
| 179 | 0 | DripsStorage storage dripsStorage = _dripsStorage(); |
| 180 | 0 | driverId = dripsStorage.nextDriverId++; |
| 181 | 0 | dripsStorage.driverAddresses[driverId] = driverAddr; |
| 182 | 0 | emit DriverRegistered(driverId, driverAddr); |
| 183 | } |
|
| 184 | ||
| 185 | /// @notice Returns the driver address. |
|
| 186 | /// @param driverId The driver ID to look up. |
|
| 187 | /// @return driverAddr The address of the driver. |
|
| 188 | /// If the driver hasn't been registered yet, returns address 0. |
|
| 189 | 12× | function driverAddress(uint32 driverId) public view returns (address driverAddr) { |
| 190 | 70× | return _dripsStorage().driverAddresses[driverId]; |
| 191 | } |
|
| 192 | ||
| 193 | /// @notice Updates the driver address. Must be called from the current driver address. |
|
| 194 | /// @param driverId The driver ID. |
|
| 195 | /// @param newDriverAddr The new address of the driver. |
|
| 196 | /// It should be a smart contract capable of dealing with the Drips API. |
|
| 197 | /// It shouldn't be an EOA because the API requires making multiple calls per transaction. |
|
| 198 | 0 | function updateDriverAddress(uint32 driverId, address newDriverAddr) public whenNotPaused { |
| 199 | 0 | _assertCallerIsDriver(driverId); |
| 200 | 0 | _dripsStorage().driverAddresses[driverId] = newDriverAddr; |
| 201 | 0 | emit DriverAddressUpdated(driverId, msg.sender, newDriverAddr); |
| 202 | } |
|
| 203 | ||
| 204 | /// @notice Returns the driver ID which will be assigned for the next registered driver. |
|
| 205 | /// @return driverId The next driver ID. |
|
| 206 | 0 | function nextDriverId() public view returns (uint32 driverId) { |
| 207 | 0 | return _dripsStorage().nextDriverId; |
| 208 | } |
|
| 209 | ||
| 210 | /// @notice Returns the amount currently stored in the protocol of the given token. |
|
| 211 | /// The sum of streaming and splitting balances can never exceed `MAX_TOTAL_BALANCE`. |
|
| 212 | /// The amount of tokens held by the Drips contract exceeding the sum of |
|
| 213 | /// streaming and splitting balances can be `withdraw`n. |
|
| 214 | /// @param erc20 The used ERC-20 token. |
|
| 215 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 216 | /// an address, then later the same amount must be transferable from that address. |
|
| 217 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 218 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 219 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 220 | /// @return streamsBalance The balance currently stored in the protocol in streaming. |
|
| 221 | /// It's the sum of all the funds of all the users |
|
| 222 | /// that are in the streams balances, are squeezable or are receivable. |
|
| 223 | /// @return splitsBalance The balance currently stored in the protocol in splitting. |
|
| 224 | /// It's the sum of all the funds of all the users that are splittable or are collectable. |
|
| 225 | 10× | function balances(IERC20 erc20) |
| 226 | public |
|
| 227 | view |
|
| 228 | 4× | returns (uint128 streamsBalance, uint128 splitsBalance) |
| 229 | { |
|
| 230 | 52× | Balance storage balance = _dripsStorage().balances[erc20]; |
| 231 | 62× | return (balance.streams, balance.splits); |
| 232 | } |
|
| 233 | ||
| 234 | /// @notice Increases the balance of the given token currently stored in streams. |
|
| 235 | /// No funds are transferred, all the tokens are expected to be already held by Drips. |
|
| 236 | /// The new total balance is verified to have coverage in the held tokens |
|
| 237 | /// and to be within the limit of `MAX_TOTAL_BALANCE`. |
|
| 238 | /// @param erc20 The used ERC-20 token. |
|
| 239 | /// @param amt The amount to increase the streams balance by. |
|
| 240 | 8× | function _increaseStreamsBalance(IERC20 erc20, uint128 amt) internal { |
| 241 | 12× | _verifyBalanceIncrease(erc20, amt); |
| 242 | 130× | _dripsStorage().balances[erc20].streams += amt; |
| 243 | } |
|
| 244 | ||
| 245 | /// @notice Decreases the balance of the given token currently stored in streams. |
|
| 246 | /// No funds are transferred, but the tokens held by Drips |
|
| 247 | /// above the total balance become withdrawable. |
|
| 248 | /// @param erc20 The used ERC-20 token. |
|
| 249 | /// @param amt The amount to decrease the streams balance by. |
|
| 250 | 8× | function _decreaseStreamsBalance(IERC20 erc20, uint128 amt) internal { |
| 251 | 130× | _dripsStorage().balances[erc20].streams -= amt; |
| 252 | } |
|
| 253 | ||
| 254 | /// @notice Increases the balance of the given token currently stored in splits. |
|
| 255 | /// No funds are transferred, all the tokens are expected to be already held by Drips. |
|
| 256 | /// The new total balance is verified to have coverage in the held tokens |
|
| 257 | /// and to be within the limit of `MAX_TOTAL_BALANCE`. |
|
| 258 | /// @param erc20 The used ERC-20 token. |
|
| 259 | /// @param amt The amount to increase the splits balance by. |
|
| 260 | 4× | function _increaseSplitsBalance(IERC20 erc20, uint128 amt) internal { |
| 261 | 6× | _verifyBalanceIncrease(erc20, amt); |
| 262 | 65× | _dripsStorage().balances[erc20].splits += amt; |
| 263 | } |
|
| 264 | ||
| 265 | /// @notice Decreases the balance of the given token currently stored in splits. |
|
| 266 | /// No funds are transferred, but the tokens held by Drips |
|
| 267 | /// above the total balance become withdrawable. |
|
| 268 | /// @param erc20 The used ERC-20 token. |
|
| 269 | /// @param amt The amount to decrease the splits balance by. |
|
| 270 | 4× | function _decreaseSplitsBalance(IERC20 erc20, uint128 amt) internal { |
| 271 | 65× | _dripsStorage().balances[erc20].splits -= amt; |
| 272 | } |
|
| 273 | ||
| 274 | /// @notice Moves the balance of the given token currently stored in streams to splits. |
|
| 275 | /// No funds are transferred, all the tokens are already held by Drips. |
|
| 276 | /// @param erc20 The used ERC-20 token. |
|
| 277 | /// @param amt The amount to decrease the splits balance by. |
|
| 278 | 15× | function _moveBalanceFromStreamsToSplits(IERC20 erc20, uint128 amt) internal { |
| 279 | 78× | Balance storage balance = _dripsStorage().balances[erc20]; |
| 280 | 129× | balance.streams -= amt; |
| 281 | 129× | balance.splits += amt; |
| 282 | } |
|
| 283 | ||
| 284 | /// @notice Verifies that the balance of streams or splits can be increased by the given amount. |
|
| 285 | /// The sum of streaming and splitting balances is checked to not exceed |
|
| 286 | /// `MAX_TOTAL_BALANCE` or the amount of tokens held by the Drips. |
|
| 287 | /// @param erc20 The used ERC-20 token. |
|
| 288 | /// @param amt The amount to increase the streams or splits balance by. |
|
| 289 | 14× | function _verifyBalanceIncrease(IERC20 erc20, uint128 amt) internal view { |
| 290 | 26× | (uint256 streamsBalance, uint128 splitsBalance) = balances(erc20); |
| 291 | 44× | uint256 newTotalBalance = streamsBalance + splitsBalance + amt; |
| 292 | 16× | require(newTotalBalance <= MAX_TOTAL_BALANCE, "Total balance too high"); |
| 293 | 22× | require(newTotalBalance <= _tokenBalance(erc20), "Token balance too low"); |
| 294 | } |
|
| 295 | ||
| 296 | /// @notice Transfers withdrawable funds to an address. |
|
| 297 | /// The withdrawable funds are held by the Drips contract, |
|
| 298 | /// but not used in the protocol, so they are free to be transferred out. |
|
| 299 | /// Anybody can call `withdraw`, so all withdrawable funds should be withdrawn |
|
| 300 | /// or used in the protocol before any 3rd parties have a chance to do that. |
|
| 301 | /// @param erc20 The used ERC-20 token. |
|
| 302 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 303 | /// an address, then later the same amount must be transferable from that address. |
|
| 304 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 305 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 306 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 307 | /// @param receiver The address to send withdrawn funds to. |
|
| 308 | /// @param amt The withdrawn amount. |
|
| 309 | /// It must be at most the difference between the balance of the token held by the Drips |
|
| 310 | /// contract address and the sum of balances managed by the protocol as indicated by `balances`. |
|
| 311 | 68× | function withdraw(IERC20 erc20, address receiver, uint256 amt) public { |
| 312 | 22× | (uint128 streamsBalance, uint128 splitsBalance) = balances(erc20); |
| 313 | 52× | uint256 withdrawable = _tokenBalance(erc20) - streamsBalance - splitsBalance; |
| 314 | 33× | require(amt <= withdrawable, "Withdrawal amount too high"); |
| 315 | 46× | emit Withdrawn(erc20, receiver, amt); |
| 316 | 30× | erc20.safeTransfer(receiver, amt); |
| 317 | } |
|
| 318 | ||
| 319 | 12× | function _tokenBalance(IERC20 erc20) internal view returns (uint256) { |
| 320 | 136× | return erc20.balanceOf(address(this)); |
| 321 | } |
|
| 322 | ||
| 323 | /// @notice Counts cycles from which streams can be collected. |
|
| 324 | /// This function can be used to detect that there are |
|
| 325 | /// too many cycles to analyze in a single transaction. |
|
| 326 | /// @param accountId The account ID. |
|
| 327 | /// @param erc20 The used ERC-20 token. |
|
| 328 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 329 | /// an address, then later the same amount must be transferable from that address. |
|
| 330 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 331 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 332 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 333 | /// @return cycles The number of cycles which can be flushed |
|
| 334 | 46× | function receivableStreamsCycles(uint256 accountId, IERC20 erc20) |
| 335 | public |
|
| 336 | view |
|
| 337 | 1× | returns (uint32 cycles) |
| 338 | { |
|
| 339 | 8× | return Streams._receivableStreamsCycles(accountId, erc20); |
| 340 | } |
|
| 341 | ||
| 342 | /// @notice Calculate effects of calling `receiveStreams` with the given parameters. |
|
| 343 | /// @param accountId The account ID. |
|
| 344 | /// @param erc20 The used ERC-20 token. |
|
| 345 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 346 | /// an address, then later the same amount must be transferable from that address. |
|
| 347 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 348 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 349 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 350 | /// @param maxCycles The maximum number of received streams cycles. |
|
| 351 | /// If too low, receiving will be cheap, but may not cover many cycles. |
|
| 352 | /// If too high, receiving may become too expensive to fit in a single transaction. |
|
| 353 | /// @return receivableAmt The amount which would be received |
|
| 354 | 94× | function receiveStreamsResult(uint256 accountId, IERC20 erc20, uint32 maxCycles) |
| 355 | public |
|
| 356 | view |
|
| 357 | 2× | returns (uint128 receivableAmt) |
| 358 | { |
|
| 359 | 42× | (receivableAmt,,,,) = Streams._receiveStreamsResult(accountId, erc20, maxCycles); |
| 360 | } |
|
| 361 | ||
| 362 | /// @notice Receive streams for the account. |
|
| 363 | /// Received streams cycles won't need to be analyzed ever again. |
|
| 364 | /// Calling this function does not collect but makes the funds ready to be split and collected. |
|
| 365 | /// @param accountId The account ID. |
|
| 366 | /// @param erc20 The used ERC-20 token. |
|
| 367 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 368 | /// an address, then later the same amount must be transferable from that address. |
|
| 369 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 370 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 371 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 372 | /// @param maxCycles The maximum number of received streams cycles. |
|
| 373 | /// If too low, receiving will be cheap, but may not cover many cycles. |
|
| 374 | /// If too high, receiving may become too expensive to fit in a single transaction. |
|
| 375 | /// @return receivedAmt The received amount |
|
| 376 | 94× | function receiveStreams(uint256 accountId, IERC20 erc20, uint32 maxCycles) |
| 377 | public |
|
| 378 | whenNotPaused |
|
| 379 | 2× | returns (uint128 receivedAmt) |
| 380 | { |
|
| 381 | 18× | receivedAmt = Streams._receiveStreams(accountId, erc20, maxCycles); |
| 382 | 16× | if (receivedAmt != 0) { |
| 383 | 12× | _moveBalanceFromStreamsToSplits(erc20, receivedAmt); |
| 384 | 14× | Splits._addSplittable(accountId, erc20, receivedAmt); |
| 385 | } |
|
| 386 | } |
|
| 387 | ||
| 388 | /// @notice Receive streams from the currently running cycle from a single sender. |
|
| 389 | /// It doesn't receive streams from the finished cycles, to do that use `receiveStreams`. |
|
| 390 | /// Squeezed funds won't be received in the next calls to `squeezeStreams` or `receiveStreams`. |
|
| 391 | /// Only funds streamed before `block.timestamp` can be squeezed. |
|
| 392 | /// @param accountId The ID of the account receiving streams to squeeze funds for. |
|
| 393 | /// @param erc20 The used ERC-20 token. |
|
| 394 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 395 | /// an address, then later the same amount must be transferable from that address. |
|
| 396 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 397 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 398 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 399 | /// @param senderId The ID of the streaming account to squeeze funds from. |
|
| 400 | /// @param historyHash The sender's history hash that was valid right before |
|
| 401 | /// they set up the sequence of configurations described by `streamsHistory`. |
|
| 402 | /// @param streamsHistory The sequence of the sender's streams configurations. |
|
| 403 | /// It can start at an arbitrary past configuration, but must describe all the configurations |
|
| 404 | /// which have been used since then including the current one, in the chronological order. |
|
| 405 | /// Only streams described by `streamsHistory` will be squeezed. |
|
| 406 | /// If `streamsHistory` entries have no receivers, they won't be squeezed. |
|
| 407 | /// @return amt The squeezed amount. |
|
| 408 | 147× | function squeezeStreams( |
| 409 | uint256 accountId, |
|
| 410 | IERC20 erc20, |
|
| 411 | uint256 senderId, |
|
| 412 | bytes32 historyHash, |
|
| 413 | StreamsHistory[] memory streamsHistory |
|
| 414 | 3× | ) public whenNotPaused returns (uint128 amt) { |
| 415 | 33× | amt = Streams._squeezeStreams(accountId, erc20, senderId, historyHash, streamsHistory); |
| 416 | 24× | if (amt != 0) { |
| 417 | 18× | _moveBalanceFromStreamsToSplits(erc20, amt); |
| 418 | 21× | Splits._addSplittable(accountId, erc20, amt); |
| 419 | } |
|
| 420 | } |
|
| 421 | ||
| 422 | /// @notice Calculate effects of calling `squeezeStreams` with the given parameters. |
|
| 423 | /// See its documentation for more details. |
|
| 424 | /// @param accountId The ID of the account receiving streams to squeeze funds for. |
|
| 425 | /// @param erc20 The used ERC-20 token. |
|
| 426 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 427 | /// an address, then later the same amount must be transferable from that address. |
|
| 428 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 429 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 430 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 431 | /// @param senderId The ID of the streaming account to squeeze funds from. |
|
| 432 | /// @param historyHash The sender's history hash that was valid right before `streamsHistory`. |
|
| 433 | /// @param streamsHistory The sequence of the sender's streams configurations. |
|
| 434 | /// @return amt The squeezed amount. |
|
| 435 | 98× | function squeezeStreamsResult( |
| 436 | uint256 accountId, |
|
| 437 | IERC20 erc20, |
|
| 438 | uint256 senderId, |
|
| 439 | bytes32 historyHash, |
|
| 440 | StreamsHistory[] memory streamsHistory |
|
| 441 | 2× | ) public view returns (uint128 amt) { |
| 442 | 28× | (amt,,,,) = |
| 443 | 18× | Streams._squeezeStreamsResult(accountId, erc20, senderId, historyHash, streamsHistory); |
| 444 | } |
|
| 445 | ||
| 446 | /// @notice Returns account's received but not split yet funds. |
|
| 447 | /// @param accountId The account ID. |
|
| 448 | /// @param erc20 The used ERC-20 token. |
|
| 449 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 450 | /// an address, then later the same amount must be transferable from that address. |
|
| 451 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 452 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 453 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 454 | /// @return amt The amount received but not split yet. |
|
| 455 | 47× | function splittable(uint256 accountId, IERC20 erc20) public view returns (uint128 amt) { |
| 456 | 8× | return Splits._splittable(accountId, erc20); |
| 457 | } |
|
| 458 | ||
| 459 | /// @notice Calculate the result of splitting an amount using the current splits configuration. |
|
| 460 | /// Fractions of tokens are always rounder either up or down depending on the amount |
|
| 461 | /// being split, the receiver's position on the list and the other receivers' weights. |
|
| 462 | /// @param accountId The account ID. |
|
| 463 | /// @param currReceivers The list of the account's current splits receivers. |
|
| 464 | /// It must be exactly the same as the last list set for the account with `setSplits`. |
|
| 465 | /// If the splits have never been set, pass an empty array. |
|
| 466 | /// @param amount The amount being split. |
|
| 467 | /// @return collectableAmt The amount made collectable for the account |
|
| 468 | /// on top of what was collectable before. |
|
| 469 | /// @return splitAmt The amount split to the account's splits receivers |
|
| 470 | 49× | function splitResult(uint256 accountId, SplitsReceiver[] memory currReceivers, uint128 amount) |
| 471 | public |
|
| 472 | view |
|
| 473 | 2× | returns (uint128 collectableAmt, uint128 splitAmt) |
| 474 | { |
|
| 475 | 11× | return Splits._splitResult(accountId, currReceivers, amount); |
| 476 | } |
|
| 477 | ||
| 478 | /// @notice Splits the account's splittable funds among receivers. |
|
| 479 | /// The entire splittable balance of the given ERC-20 token is split. |
|
| 480 | /// Fractions of tokens are always rounder either up or down depending on the amount |
|
| 481 | /// being split, the receiver's position on the list and the other receivers' weights. |
|
| 482 | /// All split funds are split using the current splits configuration. |
|
| 483 | /// Because the account can update their splits configuration at any time, |
|
| 484 | /// it is possible that calling this function will be frontrun, |
|
| 485 | /// and all the splittable funds will become splittable only using the new configuration. |
|
| 486 | /// The account must be trusted with how funds sent to them will be splits, |
|
| 487 | /// in the end they can do with their funds whatever they want by changing the configuration. |
|
| 488 | /// @param accountId The account ID. |
|
| 489 | /// @param erc20 The used ERC-20 token. |
|
| 490 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 491 | /// an address, then later the same amount must be transferable from that address. |
|
| 492 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 493 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 494 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 495 | /// @param currReceivers The list of the account's current splits receivers. |
|
| 496 | /// It must be exactly the same as the last list set for the account with `setSplits`. |
|
| 497 | /// If the splits have never been set, pass an empty array. |
|
| 498 | /// @return collectableAmt The amount made collectable for the account |
|
| 499 | /// on top of what was collectable before. |
|
| 500 | /// @return splitAmt The amount split to the account's splits receivers |
|
| 501 | 147× | function split(uint256 accountId, IERC20 erc20, SplitsReceiver[] memory currReceivers) |
| 502 | public |
|
| 503 | whenNotPaused |
|
| 504 | 6× | returns (uint128 collectableAmt, uint128 splitAmt) |
| 505 | { |
|
| 506 | 33× | return Splits._split(accountId, erc20, currReceivers); |
| 507 | } |
|
| 508 | ||
| 509 | /// @notice Returns account's received funds already split and ready to be collected. |
|
| 510 | /// @param accountId The account ID. |
|
| 511 | /// @param erc20 The used ERC-20 token. |
|
| 512 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 513 | /// an address, then later the same amount must be transferable from that address. |
|
| 514 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 515 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 516 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 517 | /// @return amt The collectable amount. |
|
| 518 | 94× | function collectable(uint256 accountId, IERC20 erc20) public view returns (uint128 amt) { |
| 519 | 16× | return Splits._collectable(accountId, erc20); |
| 520 | } |
|
| 521 | ||
| 522 | /// @notice Collects account's received already split funds and makes them withdrawable. |
|
| 523 | /// Anybody can call `withdraw`, so all withdrawable funds should be withdrawn |
|
| 524 | /// or used in the protocol before any 3rd parties have a chance to do that. |
|
| 525 | /// @param accountId The account ID. |
|
| 526 | /// @param erc20 The used ERC-20 token. |
|
| 527 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 528 | /// an address, then later the same amount must be transferable from that address. |
|
| 529 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 530 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 531 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 532 | /// @return amt The collected amount |
|
| 533 | 46× | function collect(uint256 accountId, IERC20 erc20) |
| 534 | public |
|
| 535 | whenNotPaused |
|
| 536 | 1× | onlyDriver(accountId) |
| 537 | 1× | returns (uint128 amt) |
| 538 | { |
|
| 539 | 8× | amt = Splits._collect(accountId, erc20); |
| 540 | 14× | if (amt != 0) _decreaseSplitsBalance(erc20, amt); |
| 541 | } |
|
| 542 | ||
| 543 | /// @notice Gives funds from the account to the receiver. |
|
| 544 | /// The receiver can split and collect them immediately. |
|
| 545 | /// Requires that the tokens used to give are already sent to Drips and are withdrawable. |
|
| 546 | /// Anybody can call `withdraw`, so all withdrawable funds should be withdrawn |
|
| 547 | /// or used in the protocol before any 3rd parties have a chance to do that. |
|
| 548 | /// @param accountId The account ID. |
|
| 549 | /// @param receiver The receiver account ID. |
|
| 550 | /// @param erc20 The used ERC-20 token. |
|
| 551 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 552 | /// an address, then later the same amount must be transferable from that address. |
|
| 553 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 554 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 555 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 556 | /// @param amt The given amount |
|
| 557 | 32× | function give(uint256 accountId, uint256 receiver, IERC20 erc20, uint128 amt) |
| 558 | public |
|
| 559 | whenNotPaused |
|
| 560 | 1× | onlyDriver(accountId) |
| 561 | { |
|
| 562 | 14× | if (amt != 0) _increaseSplitsBalance(erc20, amt); |
| 563 | 8× | Splits._give(accountId, receiver, erc20, amt); |
| 564 | } |
|
| 565 | ||
| 566 | /// @notice Current account streams state. |
|
| 567 | /// @param accountId The account ID. |
|
| 568 | /// @param erc20 The used ERC-20 token. |
|
| 569 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 570 | /// an address, then later the same amount must be transferable from that address. |
|
| 571 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 572 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 573 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 574 | /// @return streamsHash The current streams receivers list hash, see `hashStreams` |
|
| 575 | /// @return streamsHistoryHash The current streams history hash, see `hashStreamsHistory`. |
|
| 576 | /// @return updateTime The time when streams have been configured for the last time. |
|
| 577 | /// @return balance The balance when streams have been configured for the last time. |
|
| 578 | /// @return maxEnd The current maximum end time of streaming. |
|
| 579 | 108× | function streamsState(uint256 accountId, IERC20 erc20) |
| 580 | public |
|
| 581 | view |
|
| 582 | returns ( |
|
| 583 | 2× | bytes32 streamsHash, |
| 584 | 2× | bytes32 streamsHistoryHash, |
| 585 | 2× | uint32 updateTime, |
| 586 | 2× | uint128 balance, |
| 587 | 2× | uint32 maxEnd |
| 588 | ) |
|
| 589 | { |
|
| 590 | 32× | return Streams._streamsState(accountId, erc20); |
| 591 | } |
|
| 592 | ||
| 593 | /// @notice The account's streams balance at the given timestamp. |
|
| 594 | /// @param accountId The account ID. |
|
| 595 | /// @param erc20 The used ERC-20 token. |
|
| 596 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 597 | /// an address, then later the same amount must be transferable from that address. |
|
| 598 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 599 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 600 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 601 | /// @param currReceivers The current streams receivers list. |
|
| 602 | /// It must be exactly the same as the last list set for the account with `setStreams`. |
|
| 603 | /// @param timestamp The timestamp for which balance should be calculated. |
|
| 604 | /// It can't be lower than the timestamp of the last call to `setStreams`. |
|
| 605 | /// If it's bigger than `block.timestamp`, then it's a prediction assuming |
|
| 606 | /// that `setStreams` won't be called before `timestamp`. |
|
| 607 | /// @return balance The account balance on `timestamp` |
|
| 608 | 96× | function balanceAt( |
| 609 | uint256 accountId, |
|
| 610 | IERC20 erc20, |
|
| 611 | StreamReceiver[] memory currReceivers, |
|
| 612 | uint32 timestamp |
|
| 613 | 2× | ) public view returns (uint128 balance) { |
| 614 | 20× | return Streams._balanceAt(accountId, erc20, currReceivers, timestamp); |
| 615 | } |
|
| 616 | ||
| 617 | /// @notice Sets the account's streams configuration. |
|
| 618 | /// Requires that the tokens used to increase the streams balance |
|
| 619 | /// are already sent to Drips and are withdrawable. |
|
| 620 | /// If the streams balance is decreased, the released tokens become withdrawable. |
|
| 621 | /// Anybody can call `withdraw`, so all withdrawable funds should be withdrawn |
|
| 622 | /// or used in the protocol before any 3rd parties have a chance to do that. |
|
| 623 | /// @param accountId The account ID. |
|
| 624 | /// @param erc20 The used ERC-20 token. |
|
| 625 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 626 | /// an address, then later the same amount must be transferable from that address. |
|
| 627 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 628 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 629 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 630 | /// @param currReceivers The current streams receivers list. |
|
| 631 | /// It must be exactly the same as the last list set for the account with `setStreams`. |
|
| 632 | /// If this is the first update, pass an empty array. |
|
| 633 | /// @param balanceDelta The streams balance change to be applied. |
|
| 634 | /// If it's positive, the balance is increased by `balanceDelta`. |
|
| 635 | /// If it's zero, the balance doesn't change. |
|
| 636 | /// If it's negative, the balance is decreased by `balanceDelta`, |
|
| 637 | /// but the change is capped at the current balance amount, so it doesn't go below 0. |
|
| 638 | /// Passing `type(int128).min` always decreases the current balance to 0. |
|
| 639 | /// @param newReceivers The list of the streams receivers of the account to be set. |
|
| 640 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 641 | /// without identical elements and without 0 amtPerSecs. |
|
| 642 | /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 643 | /// The first hint for finding the maximum end time when all streams stop due to funds |
|
| 644 | /// running out after the balance is updated and the new receivers list is applied. |
|
| 645 | /// Hints have no effect on the results of calling this function, except potentially saving gas. |
|
| 646 | /// Hints are Unix timestamps used as the starting points for binary search for the time |
|
| 647 | /// when funds run out in the range of timestamps from the current block's to `2^32`. |
|
| 648 | /// Hints lower than the current timestamp are ignored. |
|
| 649 | /// You can provide zero, one or two hints. The order of hints doesn't matter. |
|
| 650 | /// Hints are the most effective when one of them is lower than or equal to |
|
| 651 | /// the last timestamp when funds are still streamed, and the other one is strictly larger |
|
| 652 | /// than that timestamp,the smaller the difference between such hints, the higher gas savings. |
|
| 653 | /// The savings are the highest possible when one of the hints is equal to |
|
| 654 | /// the last timestamp when funds are still streamed, and the other one is larger by 1. |
|
| 655 | /// It's worth noting that the exact timestamp of the block in which this function is executed |
|
| 656 | /// may affect correctness of the hints, especially if they're precise. |
|
| 657 | /// Hints don't provide any benefits when balance is not enough to cover |
|
| 658 | /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. |
|
| 659 | /// Even inaccurate hints can be useful, and providing a single hint |
|
| 660 | /// or two hints that don't enclose the time when funds run out can still save some gas. |
|
| 661 | /// Providing poor hints that don't reduce the number of binary search steps |
|
| 662 | /// may cause slightly higher gas usage than not providing any hints. |
|
| 663 | /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 664 | /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. |
|
| 665 | /// @return realBalanceDelta The actually applied streams balance change. |
|
| 666 | /// It's equal to the passed `balanceDelta`, unless it's negative |
|
| 667 | /// and it gets capped at the current balance amount. |
|
| 668 | /// If it's lower than zero, it's the negative of the amount that became withdrawable. |
|
| 669 | 102× | function setStreams( |
| 670 | uint256 accountId, |
|
| 671 | IERC20 erc20, |
|
| 672 | StreamReceiver[] memory currReceivers, |
|
| 673 | int128 balanceDelta, |
|
| 674 | StreamReceiver[] memory newReceivers, |
|
| 675 | // slither-disable-next-line similar-names |
|
| 676 | uint32 maxEndHint1, |
|
| 677 | uint32 maxEndHint2 |
|
| 678 | 4× | ) public whenNotPaused onlyDriver(accountId) returns (int128 realBalanceDelta) { |
| 679 | 30× | if (balanceDelta > 0) _increaseStreamsBalance(erc20, uint128(balanceDelta)); |
| 680 | 12× | realBalanceDelta = Streams._setStreams( |
| 681 | 14× | accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 |
| 682 | ); |
|
| 683 | 40× | if (realBalanceDelta < 0) _decreaseStreamsBalance(erc20, uint128(-realBalanceDelta)); |
| 684 | } |
|
| 685 | ||
| 686 | /// @notice Calculates the hash of the streams configuration. |
|
| 687 | /// It's used to verify if streams configuration is the previously set one. |
|
| 688 | /// @param receivers The list of the streams receivers. |
|
| 689 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 690 | /// without identical elements and without 0 amtPerSecs. |
|
| 691 | /// If the streams have never been set, pass an empty array. |
|
| 692 | /// @return streamsHash The hash of the streams configuration |
|
| 693 | 135× | function hashStreams(StreamReceiver[] memory receivers) |
| 694 | public |
|
| 695 | pure |
|
| 696 | 3× | returns (bytes32 streamsHash) |
| 697 | { |
|
| 698 | 21× | return Streams._hashStreams(receivers); |
| 699 | } |
|
| 700 | ||
| 701 | /// @notice Calculates the hash of the streams history |
|
| 702 | /// after the streams configuration is updated. |
|
| 703 | /// @param oldStreamsHistoryHash The history hash |
|
| 704 | /// that was valid before the streams were updated. |
|
| 705 | /// The `streamsHistoryHash` of the account before they set streams for the first time is `0`. |
|
| 706 | /// @param streamsHash The hash of the streams receivers being set. |
|
| 707 | /// @param updateTime The timestamp when the streams were updated. |
|
| 708 | /// @param maxEnd The maximum end of the streams being set. |
|
| 709 | /// @return streamsHistoryHash The hash of the updated streams history. |
|
| 710 | 96× | function hashStreamsHistory( |
| 711 | bytes32 oldStreamsHistoryHash, |
|
| 712 | bytes32 streamsHash, |
|
| 713 | uint32 updateTime, |
|
| 714 | uint32 maxEnd |
|
| 715 | 2× | ) public pure returns (bytes32 streamsHistoryHash) { |
| 716 | 20× | return Streams._hashStreamsHistory(oldStreamsHistoryHash, streamsHash, updateTime, maxEnd); |
| 717 | } |
|
| 718 | ||
| 719 | /// @notice Sets the account splits configuration. |
|
| 720 | /// The configuration is common for all ERC-20 tokens. |
|
| 721 | /// Nothing happens to the currently splittable funds, but when they are split |
|
| 722 | /// after this function finishes, the new splits configuration will be used. |
|
| 723 | /// Because anybody can call `split`, calling this function may be frontrun |
|
| 724 | /// and all the currently splittable funds will be split using the old splits configuration. |
|
| 725 | /// @param accountId The account ID. |
|
| 726 | /// @param receivers The list of the account's splits receivers to be set. |
|
| 727 | /// Must be sorted by the account IDs, without duplicate account IDs and without 0 weights. |
|
| 728 | /// Each splits receiver will be getting `weight / TOTAL_SPLITS_WEIGHT` |
|
| 729 | /// share of the funds collected by the account. |
|
| 730 | /// If the sum of weights of all receivers is less than `_TOTAL_SPLITS_WEIGHT`, |
|
| 731 | /// some funds won't be split, but they will be left for the account to collect. |
|
| 732 | /// Fractions of tokens are always rounder either up or down depending on the amount |
|
| 733 | /// being split, the receiver's position on the list and the other receivers' weights. |
|
| 734 | /// It's valid to include the account's own `accountId` in the list of receivers, |
|
| 735 | /// but funds split to themselves return to their splittable balance and are not collectable. |
|
| 736 | /// This is usually unwanted, because if splitting is repeated, |
|
| 737 | /// funds split to themselves will be again split using the current configuration. |
|
| 738 | /// Splitting 100% to self effectively blocks splitting unless the configuration is updated. |
|
| 739 | 60× | function setSplits(uint256 accountId, SplitsReceiver[] memory receivers) |
| 740 | public |
|
| 741 | whenNotPaused |
|
| 742 | 2× | onlyDriver(accountId) |
| 743 | { |
|
| 744 | 12× | Splits._setSplits(accountId, receivers); |
| 745 | } |
|
| 746 | ||
| 747 | /// @notice Current account's splits hash, see `hashSplits`. |
|
| 748 | /// @param accountId The account ID. |
|
| 749 | /// @return currSplitsHash The current account's splits hash |
|
| 750 | 0 | function splitsHash(uint256 accountId) public view returns (bytes32 currSplitsHash) { |
| 751 | 0 | return Splits._splitsHash(accountId); |
| 752 | } |
|
| 753 | ||
| 754 | /// @notice Calculates the hash of the list of splits receivers. |
|
| 755 | /// @param receivers The list of the splits receivers. |
|
| 756 | /// Must be sorted by the account IDs, without duplicate account IDs and without 0 weights. |
|
| 757 | /// @return receiversHash The hash of the list of splits receivers. |
|
| 758 | 0 | function hashSplits(SplitsReceiver[] memory receivers) |
| 759 | public |
|
| 760 | pure |
|
| 761 | 0 | returns (bytes32 receiversHash) |
| 762 | { |
|
| 763 | 0 | return Splits._hashSplits(receivers); |
| 764 | } |
|
| 765 | ||
| 766 | /// @notice Emits account metadata. |
|
| 767 | /// The keys and the values are not standardized by the protocol, it's up to the users |
|
| 768 | /// to establish and follow conventions to ensure compatibility with the consumers. |
|
| 769 | /// @param accountId The account ID. |
|
| 770 | /// @param accountMetadata The list of account metadata. |
|
| 771 | 0 | function emitAccountMetadata(uint256 accountId, AccountMetadata[] calldata accountMetadata) |
| 772 | public |
|
| 773 | whenNotPaused |
|
| 774 | 0 | onlyDriver(accountId) |
| 775 | { |
|
| 776 | unchecked { |
|
| 777 | 0 | for (uint256 i = 0; i < accountMetadata.length; i++) { |
| 778 | 0 | AccountMetadata calldata metadata = accountMetadata[i]; |
| 779 | 0 | emit AccountMetadataEmitted(accountId, metadata.key, metadata.value); |
| 780 | } |
|
| 781 | } |
|
| 782 | } |
|
| 783 | ||
| 784 | /// @notice Returns the Drips storage. |
|
| 785 | /// @return storageRef The storage. |
|
| 786 | 12× | function _dripsStorage() internal view returns (DripsStorage storage storageRef) { |
| 787 | 12× | bytes32 slot = _dripsStorageSlot; |
| 788 | // slither-disable-next-line assembly |
|
| 789 | 3× | assembly { |
| 790 | 9× | storageRef.slot := slot |
| 791 | } |
|
| 792 | } |
|
| 793 | } |
100%
src/DriverTransferUtils.sol
Lines covered: 16 / 16 (100%)
| 1 | // SPDX-License-Identifier: GPL-3.0-only |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import {Drips, StreamReceiver, IERC20, SafeERC20} from "./Drips.sol"; |
|
| 5 | import {ERC2771Context} from "openzeppelin-contracts/metatx/ERC2771Context.sol"; |
|
| 6 | ||
| 7 | /// @notice ERC-20 token transfer utilities for drivers. |
|
| 8 | /// Encapsulates the logic for token transfers made by drivers implementing user identities. |
|
| 9 | /// All funds going into Drips are transferred ad-hoc from the caller (`msg.sender`), |
|
| 10 | /// and all funds going out of Drips are transferred in full to the provided address. |
|
| 11 | /// Compatible with `Caller` by supporting ERC-2771. |
|
| 12 | abstract contract DriverTransferUtils is ERC2771Context { |
|
| 13 | /// @param forwarder The ERC-2771 forwarder to trust. May be the zero address. |
|
| 14 | 2× | constructor(address forwarder) ERC2771Context(forwarder) {} |
| 15 | ||
| 16 | /// @notice Collects the account's received already split funds |
|
| 17 | /// and transfers them out of the Drips contract. |
|
| 18 | /// @param drips The Drips contract to use. |
|
| 19 | /// @param erc20 The used ERC-20 token. |
|
| 20 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 21 | /// an address, then later the same amount must be transferable from that address. |
|
| 22 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 23 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 24 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 25 | /// @param transferTo The address to send collected funds to |
|
| 26 | /// @return amt The collected amount |
|
| 27 | 8× | function _collectAndTransfer(Drips drips, uint256 accountId, IERC20 erc20, address transferTo) |
| 28 | internal |
|
| 29 | 1× | returns (uint128 amt) |
| 30 | { |
|
| 31 | 71× | amt = drips.collect(accountId, erc20); |
| 32 | 65× | if (amt > 0) drips.withdraw(erc20, transferTo, amt); |
| 33 | } |
|
| 34 | ||
| 35 | /// @notice Gives funds from the message sender to the receiver. |
|
| 36 | /// The receiver can split and collect them immediately. |
|
| 37 | /// Transfers the funds to be given from the message sender's wallet to the Drips contract. |
|
| 38 | /// @param drips The Drips contract to use. |
|
| 39 | /// @param receiver The receiver account ID. |
|
| 40 | /// @param erc20 The used ERC-20 token. |
|
| 41 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 42 | /// an address, then later the same amount must be transferable from that address. |
|
| 43 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 44 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 45 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 46 | /// @param amt The given amount |
|
| 47 | 7× | function _giveAndTransfer( |
| 48 | Drips drips, |
|
| 49 | uint256 accountId, |
|
| 50 | uint256 receiver, |
|
| 51 | IERC20 erc20, |
|
| 52 | uint128 amt |
|
| 53 | ) internal { |
|
| 54 | 16× | if (amt > 0) _transferFromCaller(drips, erc20, amt); |
| 55 | 58× | drips.give(accountId, receiver, erc20, amt); |
| 56 | } |
|
| 57 | ||
| 58 | /// @notice Sets the message sender's streams configuration. |
|
| 59 | /// Transfers funds between the message sender's wallet and the Drips contract |
|
| 60 | /// to fulfil the change of the streams balance. |
|
| 61 | /// @param drips The Drips contract to use. |
|
| 62 | /// @param erc20 The used ERC-20 token. |
|
| 63 | /// It must preserve amounts, so if some amount of tokens is transferred to |
|
| 64 | /// an address, then later the same amount must be transferable from that address. |
|
| 65 | /// Tokens which rebase the holders' balances, collect taxes on transfers, |
|
| 66 | /// or impose any restrictions on holding or transferring tokens are not supported. |
|
| 67 | /// If you use such tokens in the protocol, they can get stuck or lost. |
|
| 68 | /// @param currReceivers The current streams receivers list. |
|
| 69 | /// It must be exactly the same as the last list set for the sender with `setStreams`. |
|
| 70 | /// If this is the first update, pass an empty array. |
|
| 71 | /// @param balanceDelta The streams balance change to be applied. |
|
| 72 | /// If it's positive, the balance is increased by `balanceDelta`. |
|
| 73 | /// If it's zero, the balance doesn't change. |
|
| 74 | /// If it's negative, the balance is decreased by `balanceDelta`, |
|
| 75 | /// but the change is capped at the current balance amount, so it doesn't go below 0. |
|
| 76 | /// Passing `type(int128).min` always decreases the current balance to 0. |
|
| 77 | /// @param newReceivers The list of the streams receivers of the sender to be set. |
|
| 78 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 79 | /// without identical elements and without 0 amtPerSecs. |
|
| 80 | /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 81 | /// The first hint for finding the maximum end time when all streams stop due to funds |
|
| 82 | /// running out after the balance is updated and the new receivers list is applied. |
|
| 83 | /// Hints have no effect on the results of calling this function, except potentially saving gas. |
|
| 84 | /// Hints are Unix timestamps used as the starting points for binary search for the time |
|
| 85 | /// when funds run out in the range of timestamps from the current block's to `2^32`. |
|
| 86 | /// Hints lower than the current timestamp are ignored. |
|
| 87 | /// You can provide zero, one or two hints. The order of hints doesn't matter. |
|
| 88 | /// Hints are the most effective when one of them is lower than or equal to |
|
| 89 | /// the last timestamp when funds are still streamed, and the other one is strictly larger |
|
| 90 | /// than that timestamp,the smaller the difference between such hints, the higher gas savings. |
|
| 91 | /// The savings are the highest possible when one of the hints is equal to |
|
| 92 | /// the last timestamp when funds are still streamed, and the other one is larger by 1. |
|
| 93 | /// It's worth noting that the exact timestamp of the block in which this function is executed |
|
| 94 | /// may affect correctness of the hints, especially if they're precise. |
|
| 95 | /// Hints don't provide any benefits when balance is not enough to cover |
|
| 96 | /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. |
|
| 97 | /// Even inaccurate hints can be useful, and providing a single hint |
|
| 98 | /// or two hints that don't enclose the time when funds run out can still save some gas. |
|
| 99 | /// Providing poor hints that don't reduce the number of binary search steps |
|
| 100 | /// may cause slightly higher gas usage than not providing any hints. |
|
| 101 | /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 102 | /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. |
|
| 103 | /// @param transferTo The address to send funds to in case of decreasing balance |
|
| 104 | /// @return realBalanceDelta The actually applied streams balance change. |
|
| 105 | /// It's equal to the passed `balanceDelta`, unless it's negative |
|
| 106 | /// and it gets capped at the current balance amount. |
|
| 107 | 30× | function _setStreamsAndTransfer( |
| 108 | Drips drips, |
|
| 109 | uint256 accountId, |
|
| 110 | IERC20 erc20, |
|
| 111 | StreamReceiver[] calldata currReceivers, |
|
| 112 | int128 balanceDelta, |
|
| 113 | StreamReceiver[] calldata newReceivers, |
|
| 114 | // slither-disable-next-line similar-names |
|
| 115 | uint32 maxEndHint1, |
|
| 116 | uint32 maxEndHint2, |
|
| 117 | address transferTo |
|
| 118 | 2× | ) internal returns (int128 realBalanceDelta) { |
| 119 | 32× | if (balanceDelta > 0) _transferFromCaller(drips, erc20, uint128(balanceDelta)); |
| 120 | 159× | realBalanceDelta = drips.setStreams( |
| 121 | 18× | accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 |
| 122 | ); |
|
| 123 | 140× | if (realBalanceDelta < 0) drips.withdraw(erc20, transferTo, uint128(-realBalanceDelta)); |
| 124 | } |
|
| 125 | ||
| 126 | /// @notice Transfers tokens from the sender to Drips. |
|
| 127 | /// @param drips The Drips contract to use. |
|
| 128 | /// @param erc20 The used ERC-20 token. |
|
| 129 | /// @param amt The transferred amount |
|
| 130 | 10× | function _transferFromCaller(Drips drips, IERC20 erc20, uint128 amt) internal { |
| 131 | 26× | SafeERC20.safeTransferFrom(erc20, _msgSender(), address(drips), amt); |
| 132 | } |
|
| 133 | } |
22%
src/Managed.sol
Lines covered: 12 / 53 (22%)
| 1 | // SPDX-License-Identifier: GPL-3.0-only |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import {UUPSUpgradeable} from "openzeppelin-contracts/proxy/utils/UUPSUpgradeable.sol"; |
|
| 5 | import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; |
|
| 6 | import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; |
|
| 7 | import {StorageSlot} from "openzeppelin-contracts/utils/StorageSlot.sol"; |
|
| 8 | ||
| 9 | using EnumerableSet for EnumerableSet.AddressSet; |
|
| 10 | ||
| 11 | /// @notice A mix-in for contract pausing, upgrading and admin management. |
|
| 12 | /// It can't be used directly, only via a proxy. It uses the upgrade-safe ERC-1967 storage scheme. |
|
| 13 | /// |
|
| 14 | /// Managed uses the ERC-1967 admin slot to store the admin address. |
|
| 15 | /// All instances of the contracts have admin address `0x00` and are forever paused. |
|
| 16 | /// When a proxy uses such contract via delegation, the proxy should define |
|
| 17 | /// the initial admin address and the contract is initially unpaused. |
|
| 18 | abstract contract Managed is UUPSUpgradeable { |
|
| 19 | /// @notice The pointer to the storage slot holding a single `ManagedStorage` structure. |
|
| 20 | 60× | bytes32 private immutable _managedStorageSlot = _erc1967Slot("eip1967.managed.storage"); |
| 21 | ||
| 22 | /// @notice Emitted when a new admin of the contract is proposed. |
|
| 23 | /// The proposed admin must call `acceptAdmin` to finalize the change. |
|
| 24 | /// @param currentAdmin The current admin address. |
|
| 25 | /// @param newAdmin The proposed admin address. |
|
| 26 | event NewAdminProposed(address indexed currentAdmin, address indexed newAdmin); |
|
| 27 | ||
| 28 | /// @notice Emitted when the pauses role is granted. |
|
| 29 | /// @param pauser The address that the pauser role was granted to. |
|
| 30 | /// @param admin The address of the admin that triggered the change. |
|
| 31 | event PauserGranted(address indexed pauser, address indexed admin); |
|
| 32 | ||
| 33 | /// @notice Emitted when the pauses role is revoked. |
|
| 34 | /// @param pauser The address that the pauser role was revoked from. |
|
| 35 | /// @param admin The address of the admin that triggered the change. |
|
| 36 | event PauserRevoked(address indexed pauser, address indexed admin); |
|
| 37 | ||
| 38 | /// @notice Emitted when the pause is triggered. |
|
| 39 | /// @param pauser The address that triggered the change. |
|
| 40 | event Paused(address indexed pauser); |
|
| 41 | ||
| 42 | /// @notice Emitted when the pause is lifted. |
|
| 43 | /// @param pauser The address that triggered the change. |
|
| 44 | event Unpaused(address indexed pauser); |
|
| 45 | ||
| 46 | struct ManagedStorage { |
|
| 47 | bool isPaused; |
|
| 48 | EnumerableSet.AddressSet pausers; |
|
| 49 | address proposedAdmin; |
|
| 50 | } |
|
| 51 | ||
| 52 | /// @notice Throws if called by any caller other than the admin. |
|
| 53 | modifier onlyAdmin() { |
|
| 54 | 0 | require(admin() == msg.sender, "Caller not the admin"); |
| 55 | _; |
|
| 56 | } |
|
| 57 | ||
| 58 | /// @notice Throws if called by any caller other than the admin or a pauser. |
|
| 59 | modifier onlyAdminOrPauser() { |
|
| 60 | 0 | require(admin() == msg.sender || isPauser(msg.sender), "Caller not the admin or a pauser"); |
| 61 | _; |
|
| 62 | } |
|
| 63 | ||
| 64 | /// @notice Modifier to make a function callable only when the contract is not paused. |
|
| 65 | modifier whenNotPaused() { |
|
| 66 | 160× | require(!isPaused(), "Contract paused"); |
| 67 | 6× | _; |
| 68 | } |
|
| 69 | ||
| 70 | /// @notice Modifier to make a function callable only when the contract is paused. |
|
| 71 | modifier whenPaused() { |
|
| 72 | 0 | require(isPaused(), "Contract not paused"); |
| 73 | _; |
|
| 74 | } |
|
| 75 | ||
| 76 | /// @notice Initializes the contract in paused state and with no admin. |
|
| 77 | /// The contract instance can be used only as a call delegation target for a proxy. |
|
| 78 | constructor() { |
|
| 79 | 60× | _managedStorage().isPaused = true; |
| 80 | } |
|
| 81 | ||
| 82 | /// @notice Returns the current implementation address. |
|
| 83 | 0 | function implementation() public view returns (address) { |
| 84 | 0 | return _getImplementation(); |
| 85 | } |
|
| 86 | ||
| 87 | /// @notice Returns the address of the current admin. |
|
| 88 | 0 | function admin() public view returns (address) { |
| 89 | 0 | return _getAdmin(); |
| 90 | } |
|
| 91 | ||
| 92 | /// @notice Returns the proposed address to change the admin to. |
|
| 93 | 0 | function proposedAdmin() public view returns (address) { |
| 94 | 0 | return _managedStorage().proposedAdmin; |
| 95 | } |
|
| 96 | ||
| 97 | /// @notice Proposes a change of the admin of the contract. |
|
| 98 | /// The proposed new admin must call `acceptAdmin` to finalize the change. |
|
| 99 | /// To cancel a proposal propose a different address, e.g. the zero address. |
|
| 100 | /// Can only be called by the current admin. |
|
| 101 | /// @param newAdmin The proposed admin address. |
|
| 102 | 0 | function proposeNewAdmin(address newAdmin) public onlyAdmin { |
| 103 | 0 | emit NewAdminProposed(msg.sender, newAdmin); |
| 104 | 0 | _managedStorage().proposedAdmin = newAdmin; |
| 105 | } |
|
| 106 | ||
| 107 | /// @notice Applies a proposed change of the admin of the contract. |
|
| 108 | /// Sets the proposed admin to the zero address. |
|
| 109 | /// Can only be called by the proposed admin. |
|
| 110 | 0 | function acceptAdmin() public { |
| 111 | 0 | require(proposedAdmin() == msg.sender, "Caller not the proposed admin"); |
| 112 | 0 | _updateAdmin(msg.sender); |
| 113 | } |
|
| 114 | ||
| 115 | /// @notice Changes the admin of the contract to address zero. |
|
| 116 | /// It's no longer possible to change the admin or upgrade the contract afterwards. |
|
| 117 | /// Can only be called by the current admin. |
|
| 118 | 0 | function renounceAdmin() public onlyAdmin { |
| 119 | 0 | _updateAdmin(address(0)); |
| 120 | } |
|
| 121 | ||
| 122 | /// @notice Sets the current admin of the contract and clears the proposed admin. |
|
| 123 | /// @param newAdmin The admin address being set. Can be the zero address. |
|
| 124 | 0 | function _updateAdmin(address newAdmin) internal { |
| 125 | 0 | emit AdminChanged(admin(), newAdmin); |
| 126 | 0 | _managedStorage().proposedAdmin = address(0); |
| 127 | 0 | StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; |
| 128 | } |
|
| 129 | ||
| 130 | /// @notice Grants the pauser role to an address. Callable only by the admin. |
|
| 131 | /// @param pauser The granted address. |
|
| 132 | 0 | function grantPauser(address pauser) public onlyAdmin { |
| 133 | 0 | require(_managedStorage().pausers.add(pauser), "Address already is a pauser"); |
| 134 | 0 | emit PauserGranted(pauser, msg.sender); |
| 135 | } |
|
| 136 | ||
| 137 | /// @notice Revokes the pauser role from an address. Callable only by the admin. |
|
| 138 | /// @param pauser The revoked address. |
|
| 139 | 0 | function revokePauser(address pauser) public onlyAdmin { |
| 140 | 0 | require(_managedStorage().pausers.remove(pauser), "Address is not a pauser"); |
| 141 | 0 | emit PauserRevoked(pauser, msg.sender); |
| 142 | } |
|
| 143 | ||
| 144 | /// @notice Checks if an address is a pauser. |
|
| 145 | /// @param pauser The checked address. |
|
| 146 | /// @return isAddrPauser True if the address is a pauser. |
|
| 147 | 0 | function isPauser(address pauser) public view returns (bool isAddrPauser) { |
| 148 | 0 | return _managedStorage().pausers.contains(pauser); |
| 149 | } |
|
| 150 | ||
| 151 | /// @notice Returns all the addresses with the pauser role. |
|
| 152 | /// @return pausersList The list of all the pausers, ordered arbitrarily. |
|
| 153 | /// The list's order may change after granting or revoking the pauser role. |
|
| 154 | 0 | function allPausers() public view returns (address[] memory pausersList) { |
| 155 | 0 | return _managedStorage().pausers.values(); |
| 156 | } |
|
| 157 | ||
| 158 | /// @notice Returns true if the contract is paused, and false otherwise. |
|
| 159 | 20× | function isPaused() public view returns (bool) { |
| 160 | 90× | return _managedStorage().isPaused; |
| 161 | } |
|
| 162 | ||
| 163 | /// @notice Triggers stopped state. Callable only by the admin or a pauser. |
|
| 164 | 0 | function pause() public onlyAdminOrPauser whenNotPaused { |
| 165 | 0 | _managedStorage().isPaused = true; |
| 166 | 0 | emit Paused(msg.sender); |
| 167 | } |
|
| 168 | ||
| 169 | /// @notice Returns to normal state. Callable only by the admin or a pauser. |
|
| 170 | 0 | function unpause() public onlyAdminOrPauser whenPaused { |
| 171 | 0 | _managedStorage().isPaused = false; |
| 172 | 0 | emit Unpaused(msg.sender); |
| 173 | } |
|
| 174 | ||
| 175 | /// @notice Calculates the quasi ERC-1967 slot pointer. |
|
| 176 | /// @param name The name of the slot, should be globally unique |
|
| 177 | /// @return slot The slot pointer |
|
| 178 | 18× | function _erc1967Slot(string memory name) internal pure returns (bytes32 slot) { |
| 179 | // The original ERC-1967 subtracts 1 from the hash to get 1 storage slot |
|
| 180 | // under an index without a known hash preimage which is enough to store a single address. |
|
| 181 | // This implementation subtracts 1024 to get 1024 slots without a known preimage |
|
| 182 | // allowing securely storing much larger structures. |
|
| 183 | 60× | return bytes32(uint256(keccak256(bytes(name))) - 1024); |
| 184 | } |
|
| 185 | ||
| 186 | /// @notice Returns the Managed storage. |
|
| 187 | /// @return storageRef The storage. |
|
| 188 | 28× | function _managedStorage() internal view returns (ManagedStorage storage storageRef) { |
| 189 | 30× | bytes32 slot = _managedStorageSlot; |
| 190 | // slither-disable-next-line assembly |
|
| 191 | 7× | assembly { |
| 192 | 21× | storageRef.slot := slot |
| 193 | } |
|
| 194 | } |
|
| 195 | ||
| 196 | /// @notice Authorizes the contract upgrade. See `UUPSUpgradeable` docs for more details. |
|
| 197 | 0 | function _authorizeUpgrade(address /* newImplementation */ ) internal view override onlyAdmin { |
| 198 | return; |
|
| 199 | } |
|
| 200 | } |
|
| 201 | ||
| 202 | /// @notice A generic proxy for contracts implementing `Managed`. |
|
| 203 | 0 | contract ManagedProxy is ERC1967Proxy { |
| 204 | 0 | constructor(Managed logic, address admin) ERC1967Proxy(address(logic), new bytes(0)) { |
| 205 | 0 | _changeAdmin(admin); |
| 206 | } |
|
| 207 | } |
100%
src/Splits.sol
Lines covered: 87 / 87 (100%)
| 1 | // SPDX-License-Identifier: GPL-3.0-only |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; |
|
| 5 | ||
| 6 | /// @notice A splits receiver |
|
| 7 | struct SplitsReceiver { |
|
| 8 | /// @notice The account ID. |
|
| 9 | uint256 accountId; |
|
| 10 | /// @notice The splits weight. Must never be zero. |
|
| 11 | /// The account will be getting `weight / _TOTAL_SPLITS_WEIGHT` |
|
| 12 | /// share of the funds collected by the splitting account. |
|
| 13 | uint32 weight; |
|
| 14 | } |
|
| 15 | ||
| 16 | /// @notice Splits can keep track of at most `type(uint128).max` |
|
| 17 | /// which is `2 ^ 128 - 1` units of each ERC-20 token. |
|
| 18 | /// It's up to the caller to guarantee that this limit is never exceeded, |
|
| 19 | /// failing to do so may result in a total protocol collapse. |
|
| 20 | abstract contract Splits { |
|
| 21 | /// @notice Maximum number of splits receivers of a single account. |
|
| 22 | /// Limits the cost of splitting. |
|
| 23 | 2× | uint256 internal constant _MAX_SPLITS_RECEIVERS = 200; |
| 24 | /// @notice The total splits weight of an account. |
|
| 25 | 7× | uint32 internal constant _TOTAL_SPLITS_WEIGHT = 1_000_000; |
| 26 | /// @notice The amount the contract can keep track of each ERC-20 token. |
|
| 27 | // slither-disable-next-line unused-state |
|
| 28 | uint128 internal constant _MAX_SPLITS_BALANCE = type(uint128).max; |
|
| 29 | /// @notice The storage slot holding a single `SplitsStorage` structure. |
|
| 30 | bytes32 private immutable _splitsStorageSlot; |
|
| 31 | ||
| 32 | /// @notice Emitted when an account collects funds |
|
| 33 | /// @param accountId The account ID. |
|
| 34 | /// @param erc20 The used ERC-20 token. |
|
| 35 | /// @param collected The collected amount |
|
| 36 | event Collected(uint256 indexed accountId, IERC20 indexed erc20, uint128 collected); |
|
| 37 | ||
| 38 | /// @notice Emitted when funds are split from an account to a receiver. |
|
| 39 | /// This is caused by the account collecting received funds. |
|
| 40 | /// @param accountId The account ID. |
|
| 41 | /// @param receiver The splits receiver account ID |
|
| 42 | /// @param erc20 The used ERC-20 token. |
|
| 43 | /// @param amt The amount split to the receiver |
|
| 44 | event Split( |
|
| 45 | uint256 indexed accountId, uint256 indexed receiver, IERC20 indexed erc20, uint128 amt |
|
| 46 | ); |
|
| 47 | ||
| 48 | /// @notice Emitted when funds are made collectable after splitting. |
|
| 49 | /// @param accountId The account ID. |
|
| 50 | /// @param erc20 The used ERC-20 token. |
|
| 51 | /// @param amt The amount made collectable for the account |
|
| 52 | /// on top of what was collectable before. |
|
| 53 | event Collectable(uint256 indexed accountId, IERC20 indexed erc20, uint128 amt); |
|
| 54 | ||
| 55 | /// @notice Emitted when funds are given from the account to the receiver. |
|
| 56 | /// @param accountId The account ID. |
|
| 57 | /// @param receiver The receiver account ID. |
|
| 58 | /// @param erc20 The used ERC-20 token. |
|
| 59 | /// @param amt The given amount |
|
| 60 | event Given( |
|
| 61 | uint256 indexed accountId, uint256 indexed receiver, IERC20 indexed erc20, uint128 amt |
|
| 62 | ); |
|
| 63 | ||
| 64 | /// @notice Emitted when the account's splits are updated. |
|
| 65 | /// @param accountId The account ID. |
|
| 66 | /// @param receiversHash The splits receivers list hash |
|
| 67 | event SplitsSet(uint256 indexed accountId, bytes32 indexed receiversHash); |
|
| 68 | ||
| 69 | /// @notice Emitted when an account is seen in a splits receivers list. |
|
| 70 | /// @param receiversHash The splits receivers list hash |
|
| 71 | /// @param accountId The account ID. |
|
| 72 | /// @param weight The splits weight. Must never be zero. |
|
| 73 | /// The account will be getting `weight / _TOTAL_SPLITS_WEIGHT` |
|
| 74 | /// share of the funds collected by the splitting account. |
|
| 75 | event SplitsReceiverSeen( |
|
| 76 | bytes32 indexed receiversHash, uint256 indexed accountId, uint32 weight |
|
| 77 | ); |
|
| 78 | ||
| 79 | struct SplitsStorage { |
|
| 80 | /// @notice Account splits states. |
|
| 81 | mapping(uint256 accountId => SplitsState) splitsStates; |
|
| 82 | } |
|
| 83 | ||
| 84 | struct SplitsState { |
|
| 85 | /// @notice The account's splits configuration hash, see `hashSplits`. |
|
| 86 | bytes32 splitsHash; |
|
| 87 | /// @notice The account's splits balances. |
|
| 88 | mapping(IERC20 erc20 => SplitsBalance) balances; |
|
| 89 | } |
|
| 90 | ||
| 91 | struct SplitsBalance { |
|
| 92 | /// @notice The not yet split balance, must be split before collecting by the account. |
|
| 93 | uint128 splittable; |
|
| 94 | /// @notice The already split balance, ready to be collected by the account. |
|
| 95 | uint128 collectable; |
|
| 96 | } |
|
| 97 | ||
| 98 | /// @param splitsStorageSlot The storage slot to holding a single `SplitsStorage` structure. |
|
| 99 | 1× | constructor(bytes32 splitsStorageSlot) { |
| 100 | 7× | _splitsStorageSlot = splitsStorageSlot; |
| 101 | } |
|
| 102 | ||
| 103 | 15× | function _addSplittable(uint256 accountId, IERC20 erc20, uint128 amt) internal { |
| 104 | // This will not overflow if the requirement of tracking in the contract |
|
| 105 | // no more than `_MAX_SPLITS_BALANCE` of each token is followed. |
|
| 106 | 240× | _splitsStorage().splitsStates[accountId].balances[erc20].splittable += amt; |
| 107 | } |
|
| 108 | ||
| 109 | /// @notice Returns account's received but not split yet funds. |
|
| 110 | /// @param accountId The account ID. |
|
| 111 | /// @param erc20 The used ERC-20 token. |
|
| 112 | /// @return amt The amount received but not split yet. |
|
| 113 | 7× | function _splittable(uint256 accountId, IERC20 erc20) internal view returns (uint128 amt) { |
| 114 | 52× | return _splitsStorage().splitsStates[accountId].balances[erc20].splittable; |
| 115 | } |
|
| 116 | ||
| 117 | /// @notice Calculate the result of splitting an amount using the current splits configuration. |
|
| 118 | /// @param accountId The account ID. |
|
| 119 | /// @param currReceivers The list of the account's current splits receivers. |
|
| 120 | /// It must be exactly the same as the last list set for the account with `_setSplits`. |
|
| 121 | /// If the splits have never been set, pass an empty array. |
|
| 122 | /// @param amount The amount being split. |
|
| 123 | /// @return collectableAmt The amount made collectable for the account |
|
| 124 | /// on top of what was collectable before. |
|
| 125 | /// @return splitAmt The amount split to the account's splits receivers |
|
| 126 | 9× | function _splitResult(uint256 accountId, SplitsReceiver[] memory currReceivers, uint128 amount) |
| 127 | internal |
|
| 128 | view |
|
| 129 | 2× | returns (uint128 collectableAmt, uint128 splitAmt) |
| 130 | { |
|
| 131 | 6× | _assertCurrSplits(accountId, currReceivers); |
| 132 | 8× | if (amount == 0) { |
| 133 | 8× | return (0, 0); |
| 134 | } |
|
| 135 | 1× | unchecked { |
| 136 | 1× | uint256 splitsWeight = 0; |
| 137 | 15× | for (uint256 i = currReceivers.length; i != 0;) { |
| 138 | 30× | splitsWeight += currReceivers[--i].weight; |
| 139 | } |
|
| 140 | 14× | splitAmt = uint128(amount * splitsWeight / _TOTAL_SPLITS_WEIGHT); |
| 141 | 5× | collectableAmt = amount - splitAmt; |
| 142 | } |
|
| 143 | } |
|
| 144 | ||
| 145 | /// @notice Splits the account's splittable funds among receivers. |
|
| 146 | /// The entire splittable balance of the given ERC-20 token is split. |
|
| 147 | /// All split funds are split using the current splits configuration. |
|
| 148 | /// @param accountId The account ID. |
|
| 149 | /// @param erc20 The used ERC-20 token. |
|
| 150 | /// @param currReceivers The list of the account's current splits receivers. |
|
| 151 | /// It must be exactly the same as the last list set for the account with `_setSplits`. |
|
| 152 | /// If the splits have never been set, pass an empty array. |
|
| 153 | /// @return collectableAmt The amount made collectable for the account |
|
| 154 | /// on top of what was collectable before. |
|
| 155 | /// @return splitAmt The amount split to the account's splits receivers |
|
| 156 | 27× | function _split(uint256 accountId, IERC20 erc20, SplitsReceiver[] memory currReceivers) |
| 157 | internal |
|
| 158 | 6× | returns (uint128 collectableAmt, uint128 splitAmt) |
| 159 | 3× | { |
| 160 | 18× | _assertCurrSplits(accountId, currReceivers); |
| 161 | 123× | SplitsBalance storage balance = _splitsStorage().splitsStates[accountId].balances[erc20]; |
| 162 | ||
| 163 | 45× | collectableAmt = balance.splittable; |
| 164 | 24× | if (collectableAmt == 0) { |
| 165 | 27× | return (0, 0); |
| 166 | } |
|
| 167 | 69× | balance.splittable = 0; |
| 168 | ||
| 169 | 3× | unchecked { |
| 170 | 3× | uint256 splitsWeight = 0; |
| 171 | 55× | for (uint256 i = 0; i < currReceivers.length; i++) { |
| 172 | 48× | splitsWeight += currReceivers[i].weight; |
| 173 | 8× | uint128 currSplitAmt = splitAmt; |
| 174 | 28× | splitAmt = uint128(collectableAmt * splitsWeight / _TOTAL_SPLITS_WEIGHT); |
| 175 | 10× | currSplitAmt = splitAmt - currSplitAmt; |
| 176 | 42× | uint256 receiver = currReceivers[i].accountId; |
| 177 | 14× | _addSplittable(receiver, erc20, currSplitAmt); |
| 178 | 44× | emit Split(accountId, receiver, erc20, currSplitAmt); |
| 179 | } |
|
| 180 | 15× | collectableAmt -= splitAmt; |
| 181 | // This will not overflow if the requirement of tracking in the contract |
|
| 182 | // no more than `_MAX_SPLITS_BALANCE` of each token is followed. |
|
| 183 | 114× | balance.collectable += collectableAmt; |
| 184 | } |
|
| 185 | 63× | emit Collectable(accountId, erc20, collectableAmt); |
| 186 | } |
|
| 187 | ||
| 188 | /// @notice Returns account's received funds already split and ready to be collected. |
|
| 189 | /// @param accountId The account ID. |
|
| 190 | /// @param erc20 The used ERC-20 token. |
|
| 191 | /// @return amt The collectable amount. |
|
| 192 | 14× | function _collectable(uint256 accountId, IERC20 erc20) internal view returns (uint128 amt) { |
| 193 | 104× | return _splitsStorage().splitsStates[accountId].balances[erc20].collectable; |
| 194 | } |
|
| 195 | ||
| 196 | /// @notice Collects account's received already split funds. |
|
| 197 | /// @param accountId The account ID. |
|
| 198 | /// @param erc20 The used ERC-20 token. |
|
| 199 | /// @return amt The collected amount |
|
| 200 | 8× | function _collect(uint256 accountId, IERC20 erc20) internal returns (uint128 amt) { |
| 201 | 41× | SplitsBalance storage balance = _splitsStorage().splitsStates[accountId].balances[erc20]; |
| 202 | 15× | amt = balance.collectable; |
| 203 | 23× | balance.collectable = 0; |
| 204 | 21× | emit Collected(accountId, erc20, amt); |
| 205 | } |
|
| 206 | ||
| 207 | /// @notice Gives funds from the account to the receiver. |
|
| 208 | /// The receiver can split and collect them immediately. |
|
| 209 | /// @param accountId The account ID. |
|
| 210 | /// @param receiver The receiver account ID. |
|
| 211 | /// @param erc20 The used ERC-20 token. |
|
| 212 | /// @param amt The given amount |
|
| 213 | 6× | function _give(uint256 accountId, uint256 receiver, IERC20 erc20, uint128 amt) internal { |
| 214 | 7× | _addSplittable(receiver, erc20, amt); |
| 215 | 22× | emit Given(accountId, receiver, erc20, amt); |
| 216 | } |
|
| 217 | ||
| 218 | /// @notice Sets the account splits configuration. |
|
| 219 | /// The configuration is common for all ERC-20 tokens. |
|
| 220 | /// Nothing happens to the currently splittable funds, but when they are split |
|
| 221 | /// after this function finishes, the new splits configuration will be used. |
|
| 222 | /// @param accountId The account ID. |
|
| 223 | /// @param receivers The list of the account's splits receivers to be set. |
|
| 224 | /// Must be sorted by the account IDs, without duplicate account IDs and without 0 weights. |
|
| 225 | /// Each splits receiver will be getting `weight / _TOTAL_SPLITS_WEIGHT` |
|
| 226 | /// share of the funds collected by the account. |
|
| 227 | /// If the sum of weights of all receivers is less than `_TOTAL_SPLITS_WEIGHT`, |
|
| 228 | /// some funds won't be split, but they will be left for the account to collect. |
|
| 229 | /// Fractions of tokens are always rounder either up or down depending on the amount |
|
| 230 | /// being split, the receiver's position on the list and the other receivers' weights. |
|
| 231 | /// It's valid to include the account's own `accountId` in the list of receivers, |
|
| 232 | /// but funds split to themselves return to their splittable balance and are not collectable. |
|
| 233 | /// This is usually unwanted, because if splitting is repeated, |
|
| 234 | /// funds split to themselves will be again split using the current configuration. |
|
| 235 | /// Splitting 100% to self effectively blocks splitting unless the configuration is updated. |
|
| 236 | 14× | function _setSplits(uint256 accountId, SplitsReceiver[] memory receivers) internal { |
| 237 | 44× | SplitsState storage state = _splitsStorage().splitsStates[accountId]; |
| 238 | 16× | bytes32 newSplitsHash = _hashSplits(receivers); |
| 239 | 26× | if (newSplitsHash == state.splitsHash) return; |
| 240 | 24× | emit SplitsSet(accountId, newSplitsHash); |
| 241 | 12× | _assertSplitsValid(receivers, newSplitsHash); |
| 242 | 16× | state.splitsHash = newSplitsHash; |
| 243 | } |
|
| 244 | ||
| 245 | /// @notice Validates a list of splits receivers and emits events for them |
|
| 246 | /// @param receivers The list of splits receivers |
|
| 247 | /// @param receiversHash The hash of the list of splits receivers. |
|
| 248 | /// Must be sorted by the account IDs, without duplicate account IDs and without 0 weights. |
|
| 249 | 8× | function _assertSplitsValid(SplitsReceiver[] memory receivers, bytes32 receiversHash) private { |
| 250 | 4× | unchecked { |
| 251 | 14× | require(receivers.length <= _MAX_SPLITS_RECEIVERS, "Too many splits receivers"); |
| 252 | 2× | uint256 totalWeight = 0; |
| 253 | 2× | uint256 prevAccountId = 0; |
| 254 | 46× | for (uint256 i = 0; i < receivers.length; i++) { |
| 255 | 36× | SplitsReceiver memory receiver = receivers[i]; |
| 256 | 14× | uint32 weight = receiver.weight; |
| 257 | 35× | require(weight != 0, "Splits receiver weight is zero"); |
| 258 | 14× | totalWeight += weight; |
| 259 | 14× | uint256 accountId = receiver.accountId; |
| 260 | 12× | if (accountId <= prevAccountId) require(i == 0, "Splits receivers not sorted"); |
| 261 | 6× | prevAccountId = accountId; |
| 262 | 38× | emit SplitsReceiverSeen(receiversHash, accountId, weight); |
| 263 | } |
|
| 264 | 35× | require(totalWeight <= _TOTAL_SPLITS_WEIGHT, "Splits weights sum too high"); |
| 265 | } |
|
| 266 | } |
|
| 267 | ||
| 268 | /// @notice Asserts that the list of splits receivers is the account's currently used one. |
|
| 269 | /// @param accountId The account ID. |
|
| 270 | /// @param currReceivers The list of the account's current splits receivers. |
|
| 271 | /// If the splits have never been set, pass an empty array. |
|
| 272 | 12× | function _assertCurrSplits(uint256 accountId, SplitsReceiver[] memory currReceivers) |
| 273 | internal |
|
| 274 | view |
|
| 275 | { |
|
| 276 | 9× | require( |
| 277 | 33× | _hashSplits(currReceivers) == _splitsHash(accountId), "Invalid current splits receivers" |
| 278 | ); |
|
| 279 | } |
|
| 280 | ||
| 281 | /// @notice Current account's splits hash, see `hashSplits`. |
|
| 282 | /// @param accountId The account ID. |
|
| 283 | /// @return currSplitsHash The current account's splits hash |
|
| 284 | 18× | function _splitsHash(uint256 accountId) internal view returns (bytes32 currSplitsHash) { |
| 285 | 72× | return _splitsStorage().splitsStates[accountId].splitsHash; |
| 286 | } |
|
| 287 | ||
| 288 | /// @notice Calculates the hash of the list of splits receivers. |
|
| 289 | /// @param receivers The list of the splits receivers. |
|
| 290 | /// If the splits have never been set, pass an empty array. |
|
| 291 | /// @return receiversHash The hash of the list of splits receivers. |
|
| 292 | 18× | function _hashSplits(SplitsReceiver[] memory receivers) |
| 293 | internal |
|
| 294 | pure |
|
| 295 | 3× | returns (bytes32 receiversHash) |
| 296 | { |
|
| 297 | 20× | if (receivers.length == 0) { |
| 298 | 21× | return bytes32(0); |
| 299 | } |
|
| 300 | 62× | return keccak256(abi.encode(receivers)); |
| 301 | } |
|
| 302 | ||
| 303 | /// @notice Returns the Splits storage. |
|
| 304 | /// @return splitsStorage The storage. |
|
| 305 | 12× | function _splitsStorage() private view returns (SplitsStorage storage splitsStorage) { |
| 306 | 12× | bytes32 slot = _splitsStorageSlot; |
| 307 | // slither-disable-next-line assembly |
|
| 308 | 3× | assembly { |
| 309 | 9× | splitsStorage.slot := slot |
| 310 | } |
|
| 311 | } |
|
| 312 | } |
99%
src/Streams.sol
Lines covered: 413 / 416 (99%)
| 1 | // SPDX-License-Identifier: GPL-3.0-only |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; |
|
| 5 | ||
| 6 | /// @notice A stream receiver |
|
| 7 | struct StreamReceiver { |
|
| 8 | /// @notice The account ID. |
|
| 9 | uint256 accountId; |
|
| 10 | /// @notice The stream configuration. |
|
| 11 | StreamConfig config; |
|
| 12 | } |
|
| 13 | ||
| 14 | /// @notice The sender streams history entry, used when squeezing streams. |
|
| 15 | struct StreamsHistory { |
|
| 16 | /// @notice Streams receivers list hash, see `_hashStreams`. |
|
| 17 | /// If it's non-zero, `receivers` must be empty. |
|
| 18 | bytes32 streamsHash; |
|
| 19 | /// @notice The streams receivers. If it's non-empty, `streamsHash` must be `0`. |
|
| 20 | /// If it's empty, this history entry will be skipped when squeezing streams |
|
| 21 | /// and `streamsHash` will be used when verifying the streams history validity. |
|
| 22 | /// Skipping a history entry allows cutting gas usage on analysis |
|
| 23 | /// of parts of the streams history which are not worth squeezing. |
|
| 24 | /// The hash of an empty receivers list is `0`, so when the sender updates |
|
| 25 | /// their receivers list to be empty, the new `StreamsHistory` entry will have |
|
| 26 | /// both the `streamsHash` equal to `0` and the `receivers` empty making it always skipped. |
|
| 27 | /// This is fine, because there can't be any funds to squeeze from that entry anyway. |
|
| 28 | StreamReceiver[] receivers; |
|
| 29 | /// @notice The time when streams have been configured |
|
| 30 | uint32 updateTime; |
|
| 31 | /// @notice The maximum end time of streaming. |
|
| 32 | uint32 maxEnd; |
|
| 33 | } |
|
| 34 | ||
| 35 | /// @notice Describes a streams configuration. |
|
| 36 | /// It's a 256-bit integer constructed by concatenating the configuration parameters: |
|
| 37 | /// `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | duration (32 bits)`. |
|
| 38 | /// `streamId` is an arbitrary number used to identify a stream. |
|
| 39 | /// It's a part of the configuration but the protocol doesn't use it. |
|
| 40 | /// `amtPerSec` is the amount per second being streamed. Must never be zero. |
|
| 41 | /// It must have additional `Streams._AMT_PER_SEC_EXTRA_DECIMALS` decimals and can have fractions. |
|
| 42 | /// To achieve that its value must be multiplied by `Streams._AMT_PER_SEC_MULTIPLIER`. |
|
| 43 | /// `start` is the timestamp when streaming should start. |
|
| 44 | /// If zero, use the timestamp when the stream is configured. |
|
| 45 | /// `duration` is the duration of streaming. |
|
| 46 | /// If zero, stream until balance runs out. |
|
| 47 | type StreamConfig is uint256; |
|
| 48 | ||
| 49 | using StreamConfigImpl for StreamConfig global; |
|
| 50 | ||
| 51 | 0 | library StreamConfigImpl { |
| 52 | /// @notice Create a new StreamConfig. |
|
| 53 | /// @param streamId_ An arbitrary number used to identify a stream. |
|
| 54 | /// It's a part of the configuration but the protocol doesn't use it. |
|
| 55 | /// @param amtPerSec_ The amount per second being streamed. Must never be zero. |
|
| 56 | /// It must have additional `Streams._AMT_PER_SEC_EXTRA_DECIMALS` |
|
| 57 | /// decimals and can have fractions. |
|
| 58 | /// To achieve that the passed value must be multiplied by `Streams._AMT_PER_SEC_MULTIPLIER`. |
|
| 59 | /// @param start_ The timestamp when streaming should start. |
|
| 60 | /// If zero, use the timestamp when the stream is configured. |
|
| 61 | /// @param duration_ The duration of streaming. If zero, stream until the balance runs out. |
|
| 62 | 24× | function create(uint32 streamId_, uint160 amtPerSec_, uint32 start_, uint32 duration_) |
| 63 | internal |
|
| 64 | pure |
|
| 65 | 3× | returns (StreamConfig) |
| 66 | { |
|
| 67 | // By assignment we get `config` value: |
|
| 68 | // `zeros (224 bits) | streamId (32 bits)` |
|
| 69 | 18× | uint256 config = streamId_; |
| 70 | // By bit shifting we get `config` value: |
|
| 71 | // `zeros (64 bits) | streamId (32 bits) | zeros (160 bits)` |
|
| 72 | // By bit masking we get `config` value: |
|
| 73 | // `zeros (64 bits) | streamId (32 bits) | amtPerSec (160 bits)` |
|
| 74 | 30× | config = (config << 160) | amtPerSec_; |
| 75 | // By bit shifting we get `config` value: |
|
| 76 | // `zeros (32 bits) | streamId (32 bits) | amtPerSec (160 bits) | zeros (32 bits)` |
|
| 77 | // By bit masking we get `config` value: |
|
| 78 | // `zeros (32 bits) | streamId (32 bits) | amtPerSec (160 bits) | start (32 bits)` |
|
| 79 | 30× | config = (config << 32) | start_; |
| 80 | // By bit shifting we get `config` value: |
|
| 81 | // `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | zeros (32 bits)` |
|
| 82 | // By bit masking we get `config` value: |
|
| 83 | // `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | duration (32 bits)` |
|
| 84 | 30× | config = (config << 32) | duration_; |
| 85 | 12× | return StreamConfig.wrap(config); |
| 86 | } |
|
| 87 | ||
| 88 | /// @notice Extracts streamId from a `StreamConfig` |
|
| 89 | function streamId(StreamConfig config) internal pure returns (uint32) { |
|
| 90 | // `config` has value: |
|
| 91 | // `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | duration (32 bits)` |
|
| 92 | // By bit shifting we get value: |
|
| 93 | // `zeros (224 bits) | streamId (32 bits)` |
|
| 94 | // By casting down we get value: |
|
| 95 | // `streamId (32 bits)` |
|
| 96 | return uint32(StreamConfig.unwrap(config) >> 224); |
|
| 97 | } |
|
| 98 | ||
| 99 | /// @notice Extracts amtPerSec from a `StreamConfig` |
|
| 100 | 24× | function amtPerSec(StreamConfig config) internal pure returns (uint160) { |
| 101 | // `config` has value: |
|
| 102 | // `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | duration (32 bits)` |
|
| 103 | // By bit shifting we get value: |
|
| 104 | // `zeros (64 bits) | streamId (32 bits) | amtPerSec (160 bits)` |
|
| 105 | // By casting down we get value: |
|
| 106 | // `amtPerSec (160 bits)` |
|
| 107 | 24× | return uint160(StreamConfig.unwrap(config) >> 64); |
| 108 | } |
|
| 109 | ||
| 110 | /// @notice Extracts start from a `StreamConfig` |
|
| 111 | 18× | function start(StreamConfig config) internal pure returns (uint32) { |
| 112 | // `config` has value: |
|
| 113 | // `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | duration (32 bits)` |
|
| 114 | // By bit shifting we get value: |
|
| 115 | // `zeros (32 bits) | streamId (32 bits) | amtPerSec (160 bits) | start (32 bits)` |
|
| 116 | // By casting down we get value: |
|
| 117 | // `start (32 bits)` |
|
| 118 | 18× | return uint32(StreamConfig.unwrap(config) >> 32); |
| 119 | } |
|
| 120 | ||
| 121 | /// @notice Extracts duration from a `StreamConfig` |
|
| 122 | 18× | function duration(StreamConfig config) internal pure returns (uint32) { |
| 123 | // `config` has value: |
|
| 124 | // `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | duration (32 bits)` |
|
| 125 | // By casting down we get value: |
|
| 126 | // `duration (32 bits)` |
|
| 127 | 9× | return uint32(StreamConfig.unwrap(config)); |
| 128 | } |
|
| 129 | ||
| 130 | /// @notice Compares two `StreamConfig`s. |
|
| 131 | /// First compares `streamId`s, then `amtPerSec`s, then `start`s and finally `duration`s. |
|
| 132 | /// @return isLower True if `config` is strictly lower than `otherConfig`. |
|
| 133 | 12× | function lt(StreamConfig config, StreamConfig otherConfig) |
| 134 | internal |
|
| 135 | pure |
|
| 136 | 2× | returns (bool isLower) |
| 137 | { |
|
| 138 | // Both configs have value: |
|
| 139 | // `streamId (32 bits) | amtPerSec (160 bits) | start (32 bits) | duration (32 bits)` |
|
| 140 | // Comparing them as integers is equivalent to comparing their fields from left to right. |
|
| 141 | 10× | return StreamConfig.unwrap(config) < StreamConfig.unwrap(otherConfig); |
| 142 | } |
|
| 143 | } |
|
| 144 | ||
| 145 | /// @notice Streams can keep track of at most `type(int128).max` |
|
| 146 | /// which is `2 ^ 127 - 1` units of each ERC-20 token. |
|
| 147 | /// It's up to the caller to guarantee that this limit is never exceeded, |
|
| 148 | /// failing to do so may result in a total protocol collapse. |
|
| 149 | abstract contract Streams { |
|
| 150 | /// @notice Maximum number of streams receivers of a single account. |
|
| 151 | /// Limits cost of changes in streams configuration. |
|
| 152 | 2× | uint256 internal constant _MAX_STREAMS_RECEIVERS = 100; |
| 153 | /// @notice The additional decimals for all amtPerSec values. |
|
| 154 | 0 | uint8 internal constant _AMT_PER_SEC_EXTRA_DECIMALS = 9; |
| 155 | /// @notice The multiplier for all amtPerSec values. It's `10 ** _AMT_PER_SEC_EXTRA_DECIMALS`. |
|
| 156 | 7× | uint160 internal constant _AMT_PER_SEC_MULTIPLIER = 1_000_000_000; |
| 157 | /// @notice The amount the contract can keep track of each ERC-20 token. |
|
| 158 | 2× | uint128 internal constant _MAX_STREAMS_BALANCE = uint128(type(int128).max); |
| 159 | /// @notice On every timestamp `T`, which is a multiple of `cycleSecs`, the receivers |
|
| 160 | /// gain access to streams received during `T - cycleSecs` to `T - 1`. |
|
| 161 | /// Always higher than 1. |
|
| 162 | // slither-disable-next-line naming-convention |
|
| 163 | uint32 internal immutable _cycleSecs; |
|
| 164 | /// @notice The minimum amtPerSec of a stream. It's 1 token per cycle. |
|
| 165 | // slither-disable-next-line naming-convention |
|
| 166 | uint160 internal immutable _minAmtPerSec; |
|
| 167 | /// @notice The storage slot holding a single `StreamsStorage` structure. |
|
| 168 | bytes32 private immutable _streamsStorageSlot; |
|
| 169 | ||
| 170 | /// @notice Emitted when the streams configuration of an account is updated. |
|
| 171 | /// @param accountId The account ID. |
|
| 172 | /// @param erc20 The used ERC-20 token. |
|
| 173 | /// @param receiversHash The streams receivers list hash |
|
| 174 | /// @param streamsHistoryHash The streams history hash that was valid right before the update. |
|
| 175 | /// @param balance The account's streams balance. These funds will be streamed to the receivers. |
|
| 176 | /// @param maxEnd The maximum end time of streaming, when funds run out. |
|
| 177 | /// If funds run out after the timestamp `type(uint32).max`, it's set to `type(uint32).max`. |
|
| 178 | /// If the balance is 0 or there are no receivers, it's set to the current timestamp. |
|
| 179 | event StreamsSet( |
|
| 180 | uint256 indexed accountId, |
|
| 181 | IERC20 indexed erc20, |
|
| 182 | bytes32 indexed receiversHash, |
|
| 183 | bytes32 streamsHistoryHash, |
|
| 184 | uint128 balance, |
|
| 185 | uint32 maxEnd |
|
| 186 | ); |
|
| 187 | ||
| 188 | /// @notice Emitted when an account is seen in a streams receivers list. |
|
| 189 | /// @param receiversHash The streams receivers list hash |
|
| 190 | /// @param accountId The account ID. |
|
| 191 | /// @param config The streams configuration. |
|
| 192 | event StreamReceiverSeen( |
|
| 193 | bytes32 indexed receiversHash, uint256 indexed accountId, StreamConfig config |
|
| 194 | ); |
|
| 195 | ||
| 196 | /// @notice Emitted when streams are received. |
|
| 197 | /// @param accountId The account ID. |
|
| 198 | /// @param erc20 The used ERC-20 token. |
|
| 199 | /// @param amt The received amount. |
|
| 200 | /// @param receivableCycles The number of cycles which still can be received. |
|
| 201 | event ReceivedStreams( |
|
| 202 | uint256 indexed accountId, IERC20 indexed erc20, uint128 amt, uint32 receivableCycles |
|
| 203 | ); |
|
| 204 | ||
| 205 | /// @notice Emitted when streams are squeezed. |
|
| 206 | /// @param accountId The squeezing account ID. |
|
| 207 | /// @param erc20 The used ERC-20 token. |
|
| 208 | /// @param senderId The ID of the streaming account from whom funds are squeezed. |
|
| 209 | /// @param amt The squeezed amount. |
|
| 210 | /// @param streamsHistoryHashes The history hashes of all squeezed streams history entries. |
|
| 211 | /// Each history hash matches `streamsHistoryHash` emitted in its `StreamsSet` |
|
| 212 | /// when the squeezed streams configuration was set. |
|
| 213 | /// Sorted in the oldest streams configuration to the newest. |
|
| 214 | event SqueezedStreams( |
|
| 215 | uint256 indexed accountId, |
|
| 216 | IERC20 indexed erc20, |
|
| 217 | uint256 indexed senderId, |
|
| 218 | uint128 amt, |
|
| 219 | bytes32[] streamsHistoryHashes |
|
| 220 | ); |
|
| 221 | ||
| 222 | struct StreamsStorage { |
|
| 223 | /// @notice Account streams states. |
|
| 224 | mapping(IERC20 erc20 => mapping(uint256 accountId => StreamsState)) states; |
|
| 225 | } |
|
| 226 | ||
| 227 | struct StreamsState { |
|
| 228 | /// @notice The streams history hash, see `_hashStreamsHistory`. |
|
| 229 | bytes32 streamsHistoryHash; |
|
| 230 | /// @notice The next squeezable timestamps. |
|
| 231 | /// Each `N`th element of the array is the next squeezable timestamp |
|
| 232 | /// of the `N`th sender's streams configuration in effect in the current cycle. |
|
| 233 | mapping(uint256 accountId => uint32[2 ** 32]) nextSqueezed; |
|
| 234 | /// @notice The streams receivers list hash, see `_hashStreams`. |
|
| 235 | bytes32 streamsHash; |
|
| 236 | /// @notice The next cycle to be received |
|
| 237 | uint32 nextReceivableCycle; |
|
| 238 | /// @notice The time when streams have been configured for the last time. |
|
| 239 | uint32 updateTime; |
|
| 240 | /// @notice The maximum end time of streaming. |
|
| 241 | uint32 maxEnd; |
|
| 242 | /// @notice The balance when streams have been configured for the last time. |
|
| 243 | uint128 balance; |
|
| 244 | /// @notice The number of streams configurations seen in the cycle |
|
| 245 | /// during which streams have been configured for the last time. |
|
| 246 | uint32 lastUpdatedCycleConfigs; |
|
| 247 | /// @notice The changes of the received amounts in each cycle. |
|
| 248 | /// The keys are cycles, each cycle `C` becomes receivable on timestamp `C * cycleSecs`. |
|
| 249 | /// Each cycle is described by 2 deltas, the cycle's `thisCycle` and the previous cycle's |
|
| 250 | /// `nextCycle`, so to get the change of the amount sent during the cycle `C`, |
|
| 251 | /// you need to calculate `amtDeltas[C].thisCycle + amtDeltas[C-1].nextCycle`. |
|
| 252 | /// To calculate the absolute amount streamed in the cycle `C`, you need to take the |
|
| 253 | /// amount streamed during the cycle `C - 1` and add the change applied in the cycle `C`. |
|
| 254 | /// Values for cycles before `nextReceivableCycle` are guaranteed to be zeroed. |
|
| 255 | /// This means that the value of `amtDeltas[nextReceivableCycle].thisCycle` is always |
|
| 256 | /// relative to 0 or in other words it's an absolute value independent from other cycles. |
|
| 257 | mapping(uint32 cycle => AmtDelta) amtDeltas; |
|
| 258 | } |
|
| 259 | ||
| 260 | /// @notice The change of the received amounts in the given cycle and the cycle after it. |
|
| 261 | struct AmtDelta { |
|
| 262 | /// @notice Amount delta applied in this cycle. |
|
| 263 | int128 thisCycle; |
|
| 264 | /// @notice Amount delta applied on the next cycle. |
|
| 265 | int128 nextCycle; |
|
| 266 | } |
|
| 267 | ||
| 268 | /// @param cycleSecs The length of cycleSecs to be used in the contract instance. |
|
| 269 | /// Low value makes funds more available by shortening the average time |
|
| 270 | /// of funds being frozen between being taken from the accounts' |
|
| 271 | /// streams balance and being receivable by their receivers. |
|
| 272 | /// High value makes receiving cheaper by making it process less cycles for a given time range. |
|
| 273 | /// Must be higher than 1. |
|
| 274 | /// @param streamsStorageSlot The storage slot to holding a single `StreamsStorage` structure. |
|
| 275 | 2× | constructor(uint32 cycleSecs, bytes32 streamsStorageSlot) { |
| 276 | 8× | require(cycleSecs > 1, "Cycle length too low"); |
| 277 | 11× | _cycleSecs = cycleSecs; |
| 278 | 35× | _minAmtPerSec = (_AMT_PER_SEC_MULTIPLIER + cycleSecs - 1) / cycleSecs; |
| 279 | 7× | _streamsStorageSlot = streamsStorageSlot; |
| 280 | } |
|
| 281 | ||
| 282 | /// @notice Receive streams from unreceived cycles of the account. |
|
| 283 | /// Received streams cycles won't need to be analyzed ever again. |
|
| 284 | /// @param accountId The account ID. |
|
| 285 | /// @param erc20 The used ERC-20 token. |
|
| 286 | /// @param maxCycles The maximum number of received streams cycles. |
|
| 287 | /// If too low, receiving will be cheap, but may not cover many cycles. |
|
| 288 | /// If too high, receiving may become too expensive to fit in a single transaction. |
|
| 289 | /// @return receivedAmt The received amount |
|
| 290 | 14× | function _receiveStreams(uint256 accountId, IERC20 erc20, uint32 maxCycles) |
| 291 | internal |
|
| 292 | 2× | returns (uint128 receivedAmt) |
| 293 | 8× | { |
| 294 | 2× | uint32 receivableCycles; |
| 295 | 2× | uint32 fromCycle; |
| 296 | 2× | uint32 toCycle; |
| 297 | 2× | int128 finalAmtPerCycle; |
| 298 | 40× | (receivedAmt, receivableCycles, fromCycle, toCycle, finalAmtPerCycle) = |
| 299 | 14× | _receiveStreamsResult(accountId, erc20, maxCycles); |
| 300 | 24× | if (fromCycle != toCycle) { |
| 301 | 78× | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 302 | 46× | state.nextReceivableCycle = toCycle; |
| 303 | 12× | mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
| 304 | unchecked { |
|
| 305 | 52× | for (uint32 cycle = fromCycle; cycle < toCycle; cycle++) { |
| 306 | 102× | delete amtDeltas[cycle]; |
| 307 | } |
|
| 308 | // The next cycle delta must be relative to the last received cycle, which deltas |
|
| 309 | // got zeroed. In other words the next cycle delta must be an absolute value. |
|
| 310 | 16× | if (finalAmtPerCycle != 0) { |
| 311 | 114× | amtDeltas[toCycle].thisCycle += finalAmtPerCycle; |
| 312 | } |
|
| 313 | } |
|
| 314 | } |
|
| 315 | 46× | emit ReceivedStreams(accountId, erc20, receivedAmt, receivableCycles); |
| 316 | } |
|
| 317 | ||
| 318 | /// @notice Calculate effects of calling `_receiveStreams` with the given parameters. |
|
| 319 | /// @param accountId The account ID. |
|
| 320 | /// @param erc20 The used ERC-20 token. |
|
| 321 | /// @param maxCycles The maximum number of received streams cycles. |
|
| 322 | /// If too low, receiving will be cheap, but may not cover many cycles. |
|
| 323 | /// If too high, receiving may become too expensive to fit in a single transaction. |
|
| 324 | /// @return receivedAmt The amount which would be received |
|
| 325 | /// @return receivableCycles The number of cycles which would still be receivable after the call |
|
| 326 | /// @return fromCycle The cycle from which funds would be received |
|
| 327 | /// @return toCycle The cycle to which funds would be received |
|
| 328 | /// @return amtPerCycle The amount per cycle when `toCycle` starts. |
|
| 329 | 22× | function _receiveStreamsResult(uint256 accountId, IERC20 erc20, uint32 maxCycles) |
| 330 | internal |
|
| 331 | view |
|
| 332 | returns ( |
|
| 333 | 2× | uint128 receivedAmt, |
| 334 | 2× | uint32 receivableCycles, |
| 335 | 2× | uint32 fromCycle, |
| 336 | 2× | uint32 toCycle, |
| 337 | 2× | int128 amtPerCycle |
| 338 | ) |
|
| 339 | { |
|
| 340 | 2× | unchecked { |
| 341 | 28× | (fromCycle, toCycle) = _receivableStreamsCyclesRange(accountId, erc20); |
| 342 | 26× | if (toCycle - fromCycle > maxCycles) { |
| 343 | 7× | receivableCycles = toCycle - fromCycle - maxCycles; |
| 344 | 5× | toCycle -= receivableCycles; |
| 345 | } |
|
| 346 | 6× | mapping(uint32 cycle => AmtDelta) storage amtDeltas = |
| 347 | 76× | _streamsStorage().states[erc20][accountId].amtDeltas; |
| 348 | 54× | for (uint32 cycle = fromCycle; cycle < toCycle; cycle++) { |
| 349 | 144× | AmtDelta memory amtDelta = amtDeltas[cycle]; |
| 350 | 16× | amtPerCycle += amtDelta.thisCycle; |
| 351 | // This will not overflow if the requirement of tracking in the contract |
|
| 352 | // no more than `_MAX_STREAMS_BALANCE` of each token is followed. |
|
| 353 | 10× | receivedAmt += uint128(amtPerCycle); |
| 354 | 16× | amtPerCycle += amtDelta.nextCycle; |
| 355 | } |
|
| 356 | } |
|
| 357 | } |
|
| 358 | ||
| 359 | /// @notice Counts cycles from which streams can be received. |
|
| 360 | /// This function can be used to detect that there are |
|
| 361 | /// too many cycles to analyze in a single transaction. |
|
| 362 | /// @param accountId The account ID. |
|
| 363 | /// @param erc20 The used ERC-20 token. |
|
| 364 | /// @return cycles The number of cycles which can be flushed |
|
| 365 | 6× | function _receivableStreamsCycles(uint256 accountId, IERC20 erc20) |
| 366 | internal |
|
| 367 | view |
|
| 368 | 1× | returns (uint32 cycles) |
| 369 | { |
|
| 370 | unchecked { |
|
| 371 | 12× | (uint32 fromCycle, uint32 toCycle) = _receivableStreamsCyclesRange(accountId, erc20); |
| 372 | 7× | return toCycle - fromCycle; |
| 373 | } |
|
| 374 | } |
|
| 375 | ||
| 376 | /// @notice Calculates the cycles range from which streams can be received. |
|
| 377 | /// @param accountId The account ID. |
|
| 378 | /// @param erc20 The used ERC-20 token. |
|
| 379 | /// @return fromCycle The cycle from which funds can be received |
|
| 380 | /// @return toCycle The cycle to which funds can be received |
|
| 381 | 14× | function _receivableStreamsCyclesRange(uint256 accountId, IERC20 erc20) |
| 382 | private |
|
| 383 | view |
|
| 384 | 4× | returns (uint32 fromCycle, uint32 toCycle) |
| 385 | { |
|
| 386 | 100× | fromCycle = _streamsStorage().states[erc20][accountId].nextReceivableCycle; |
| 387 | 20× | toCycle = _cycleOf(_currTimestamp()); |
| 388 | // slither-disable-next-line timestamp |
|
| 389 | 42× | if (fromCycle == 0 || toCycle < fromCycle) { |
| 390 | 6× | toCycle = fromCycle; |
| 391 | } |
|
| 392 | } |
|
| 393 | ||
| 394 | /// @notice Receive streams from the currently running cycle from a single sender. |
|
| 395 | /// It doesn't receive streams from the finished cycles, to do that use `_receiveStreams`. |
|
| 396 | /// Squeezed funds won't be received in the next calls |
|
| 397 | /// to `_squeezeStreams` or `_receiveStreams`. |
|
| 398 | /// Only funds streamed before `block.timestamp` can be squeezed. |
|
| 399 | /// @param accountId The ID of the account receiving streams to squeeze funds for. |
|
| 400 | /// @param erc20 The used ERC-20 token. |
|
| 401 | /// @param senderId The ID of the streaming account to squeeze funds from. |
|
| 402 | /// @param historyHash The sender's history hash that was valid right before |
|
| 403 | /// they set up the sequence of configurations described by `streamsHistory`. |
|
| 404 | /// @param streamsHistory The sequence of the sender's streams configurations. |
|
| 405 | /// It can start at an arbitrary past configuration, but must describe all the configurations |
|
| 406 | /// which have been used since then including the current one, in the chronological order. |
|
| 407 | /// Only streams described by `streamsHistory` will be squeezed. |
|
| 408 | /// If `streamsHistory` entries have no receivers, they won't be squeezed. |
|
| 409 | /// @return amt The squeezed amount. |
|
| 410 | 27× | function _squeezeStreams( |
| 411 | uint256 accountId, |
|
| 412 | IERC20 erc20, |
|
| 413 | uint256 senderId, |
|
| 414 | bytes32 historyHash, |
|
| 415 | StreamsHistory[] memory streamsHistory |
|
| 416 | 3× | ) internal returns (uint128 amt) { |
| 417 | 24× | unchecked { |
| 418 | 3× | uint256 squeezedNum; |
| 419 | 3× | uint256[] memory squeezedRevIdxs; |
| 420 | 3× | bytes32[] memory historyHashes; |
| 421 | 3× | uint256 currCycleConfigs; |
| 422 | 60× | (amt, squeezedNum, squeezedRevIdxs, historyHashes, currCycleConfigs) = |
| 423 | 27× | _squeezeStreamsResult(accountId, erc20, senderId, historyHash, streamsHistory); |
| 424 | 150× | bytes32[] memory squeezedHistoryHashes = new bytes32[](squeezedNum); |
| 425 | 117× | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 426 | 57× | uint32[2 ** 32] storage nextSqueezed = state.nextSqueezed[senderId]; |
| 427 | 60× | for (uint256 i = 0; i < squeezedNum; i++) { |
| 428 | // `squeezedRevIdxs` are sorted from the newest configuration to the oldest, |
|
| 429 | // but we need to consume them from the oldest to the newest. |
|
| 430 | 66× | uint256 revIdx = squeezedRevIdxs[squeezedNum - i - 1]; |
| 431 | 111× | squeezedHistoryHashes[i] = historyHashes[historyHashes.length - revIdx]; |
| 432 | 129× | nextSqueezed[currCycleConfigs - revIdx] = _currTimestamp(); |
| 433 | } |
|
| 434 | 21× | uint32 cycleStart = _currCycleStart(); |
| 435 | 18× | _addDeltaRange( |
| 436 | 33× | state, cycleStart, cycleStart + 1, -int160(amt * _AMT_PER_SEC_MULTIPLIER) |
| 437 | ); |
|
| 438 | 72× | emit SqueezedStreams(accountId, erc20, senderId, amt, squeezedHistoryHashes); |
| 439 | } |
|
| 440 | } |
|
| 441 | ||
| 442 | /// @notice Calculate effects of calling `_squeezeStreams` with the given parameters. |
|
| 443 | /// See its documentation for more details. |
|
| 444 | /// @param accountId The ID of the account receiving streams to squeeze funds for. |
|
| 445 | /// @param erc20 The used ERC-20 token. |
|
| 446 | /// @param senderId The ID of the streaming account to squeeze funds from. |
|
| 447 | /// @param historyHash The sender's history hash that was valid right before `streamsHistory`. |
|
| 448 | /// @param streamsHistory The sequence of the sender's streams configurations. |
|
| 449 | /// @return amt The squeezed amount. |
|
| 450 | /// @return squeezedNum The number of squeezed history entries. |
|
| 451 | /// @return squeezedRevIdxs The indexes of the squeezed history entries. |
|
| 452 | /// The indexes are reversed, meaning that to get the actual index in an array, |
|
| 453 | /// they must counted from the end of arrays, as in `arrayLength - squeezedRevIdxs[i]`. |
|
| 454 | /// These indexes can be safely used to access `streamsHistory`, `historyHashes` |
|
| 455 | /// and `nextSqueezed` regardless of their lengths. |
|
| 456 | /// `squeezeRevIdxs` is sorted ascending, from pointing at the most recent entry to the oldest. |
|
| 457 | /// @return historyHashes The history hashes valid |
|
| 458 | /// for squeezing each of `streamsHistory` entries. |
|
| 459 | /// In other words history hashes which had been valid right before each streams |
|
| 460 | /// configuration was set, matching `streamsHistoryHash` emitted in its `StreamsSet`. |
|
| 461 | /// The first item is always equal to `historyHash`. |
|
| 462 | /// @return currCycleConfigs The number of the sender's |
|
| 463 | /// streams configurations which have been seen in the current cycle. |
|
| 464 | /// This is also the number of used entries in each of the sender's `nextSqueezed` arrays. |
|
| 465 | 39× | function _squeezeStreamsResult( |
| 466 | uint256 accountId, |
|
| 467 | IERC20 erc20, |
|
| 468 | uint256 senderId, |
|
| 469 | bytes32 historyHash, |
|
| 470 | StreamsHistory[] memory streamsHistory |
|
| 471 | ) |
|
| 472 | internal |
|
| 473 | view |
|
| 474 | returns ( |
|
| 475 | 3× | uint128 amt, |
| 476 | 3× | uint256 squeezedNum, |
| 477 | 3× | uint256[] memory squeezedRevIdxs, |
| 478 | 3× | bytes32[] memory historyHashes, |
| 479 | 3× | uint256 currCycleConfigs |
| 480 | ) |
|
| 481 | 6× | { |
| 482 | 3× | { |
| 483 | 117× | StreamsState storage sender = _streamsStorage().states[erc20][senderId]; |
| 484 | 6× | historyHashes = |
| 485 | 30× | _verifyStreamsHistory(historyHash, streamsHistory, sender.streamsHistoryHash); |
| 486 | // If the last update was not in the current cycle, |
|
| 487 | // there's only the single latest history entry to squeeze in the current cycle. |
|
| 488 | 9× | currCycleConfigs = 1; |
| 489 | // slither-disable-next-line timestamp |
|
| 490 | 75× | if (sender.updateTime >= _currCycleStart()) { |
| 491 | 34× | currCycleConfigs = sender.lastUpdatedCycleConfigs; |
| 492 | } |
|
| 493 | } |
|
| 494 | 150× | squeezedRevIdxs = new uint256[](streamsHistory.length); |
| 495 | 9× | uint32[2 ** 32] storage nextSqueezed = |
| 496 | 153× | _streamsStorage().states[erc20][accountId].nextSqueezed[senderId]; |
| 497 | 21× | uint32 squeezeEndCap = _currTimestamp(); |
| 498 | unchecked { |
|
| 499 | 105× | for (uint256 i = 1; i <= streamsHistory.length && i <= currCycleConfigs; i++) { |
| 500 | 63× | StreamsHistory memory historyEntry = streamsHistory[streamsHistory.length - i]; |
| 501 | 33× | if (historyEntry.receivers.length != 0) { |
| 502 | 99× | uint32 squeezeStartCap = nextSqueezed[currCycleConfigs - i]; |
| 503 | 60× | if (squeezeStartCap < _currCycleStart()) squeezeStartCap = _currCycleStart(); |
| 504 | 42× | if (squeezeStartCap < historyEntry.updateTime) { |
| 505 | 12× | squeezeStartCap = historyEntry.updateTime; |
| 506 | } |
|
| 507 | 33× | if (squeezeStartCap < squeezeEndCap) { |
| 508 | 75× | squeezedRevIdxs[squeezedNum++] = i; |
| 509 | // This will not overflow if the requirement of tracking in the contract |
|
| 510 | // no more than `_MAX_STREAMS_BALANCE` of each token is followed. |
|
| 511 | 36× | amt += _squeezedAmt(accountId, historyEntry, squeezeStartCap, squeezeEndCap); |
| 512 | } |
|
| 513 | } |
|
| 514 | 18× | squeezeEndCap = historyEntry.updateTime; |
| 515 | } |
|
| 516 | } |
|
| 517 | } |
|
| 518 | ||
| 519 | /// @notice Verify a streams history and revert if it's invalid. |
|
| 520 | /// @param historyHash The account's history hash that was valid right before `streamsHistory`. |
|
| 521 | /// @param streamsHistory The sequence of the account's streams configurations. |
|
| 522 | /// @param finalHistoryHash The history hash at the end of `streamsHistory`. |
|
| 523 | /// @return historyHashes The history hashes valid |
|
| 524 | /// for squeezing each of `streamsHistory` entries. |
|
| 525 | /// In other words history hashes which had been valid right before each streams |
|
| 526 | /// configuration was set, matching `streamsHistoryHash`es emitted in `StreamsSet`. |
|
| 527 | /// The first item is always equal to `historyHash` and `finalHistoryHash` is never included. |
|
| 528 | 21× | function _verifyStreamsHistory( |
| 529 | bytes32 historyHash, |
|
| 530 | StreamsHistory[] memory streamsHistory, |
|
| 531 | bytes32 finalHistoryHash |
|
| 532 | 3× | ) private pure returns (bytes32[] memory historyHashes) { |
| 533 | 150× | historyHashes = new bytes32[](streamsHistory.length); |
| 534 | 75× | for (uint256 i = 0; i < streamsHistory.length; i++) { |
| 535 | 54× | StreamsHistory memory historyEntry = streamsHistory[i]; |
| 536 | 21× | bytes32 streamsHash = historyEntry.streamsHash; |
| 537 | 30× | if (historyEntry.receivers.length != 0) { |
| 538 | 24× | require(streamsHash == 0, "Entry with hash and receivers"); |
| 539 | 30× | streamsHash = _hashStreams(historyEntry.receivers); |
| 540 | } |
|
| 541 | 60× | historyHashes[i] = historyHash; |
| 542 | 18× | historyHash = _hashStreamsHistory( |
| 543 | 30× | historyHash, streamsHash, historyEntry.updateTime, historyEntry.maxEnd |
| 544 | ); |
|
| 545 | } |
|
| 546 | // slither-disable-next-line incorrect-equality,timestamp |
|
| 547 | 18× | require(historyHash == finalHistoryHash, "Invalid streams history"); |
| 548 | } |
|
| 549 | ||
| 550 | /// @notice Calculate the amount squeezable by an account from a single streams history entry. |
|
| 551 | /// @param accountId The ID of the account to squeeze streams for. |
|
| 552 | /// @param historyEntry The squeezed history entry. |
|
| 553 | /// @param squeezeStartCap The squeezed time range start. |
|
| 554 | /// @param squeezeEndCap The squeezed time range end. |
|
| 555 | /// @return squeezedAmt The squeezed amount. |
|
| 556 | 24× | function _squeezedAmt( |
| 557 | uint256 accountId, |
|
| 558 | StreamsHistory memory historyEntry, |
|
| 559 | uint32 squeezeStartCap, |
|
| 560 | uint32 squeezeEndCap |
|
| 561 | 3× | ) private view returns (uint128 squeezedAmt) { |
| 562 | unchecked { |
|
| 563 | 21× | StreamReceiver[] memory receivers = historyEntry.receivers; |
| 564 | // Binary search for the `idx` of the first occurrence of `accountId` |
|
| 565 | 3× | uint256 idx = 0; |
| 566 | 51× | for (uint256 idxCap = receivers.length; idx < idxCap;) { |
| 567 | 36× | uint256 idxMid = (idx + idxCap) / 2; |
| 568 | 81× | if (receivers[idxMid].accountId < accountId) { |
| 569 | 15× | idx = idxMid + 1; |
| 570 | } else { |
|
| 571 | 9× | idxCap = idxMid; |
| 572 | } |
|
| 573 | } |
|
| 574 | 21× | uint32 updateTime = historyEntry.updateTime; |
| 575 | 21× | uint32 maxEnd = historyEntry.maxEnd; |
| 576 | 3× | uint256 amt = 0; |
| 577 | 63× | for (; idx < receivers.length; idx++) { |
| 578 | 54× | StreamReceiver memory receiver = receivers[idx]; |
| 579 | 36× | if (receiver.accountId != accountId) break; |
| 580 | 18× | (uint32 start, uint32 end) = |
| 581 | 27× | _streamRange(receiver, updateTime, maxEnd, squeezeStartCap, squeezeEndCap); |
| 582 | // This will not overflow if the requirement of tracking in the contract |
|
| 583 | // no more than `_MAX_STREAMS_BALANCE` of each token is followed. |
|
| 584 | 72× | amt += _streamedAmt(receiver.config.amtPerSec(), start, end); |
| 585 | } |
|
| 586 | 24× | return uint128(amt); |
| 587 | } |
|
| 588 | } |
|
| 589 | ||
| 590 | /// @notice Current account streams state. |
|
| 591 | /// @param accountId The account ID. |
|
| 592 | /// @param erc20 The used ERC-20 token. |
|
| 593 | /// @return streamsHash The current streams receivers list hash, see `_hashStreams` |
|
| 594 | /// @return streamsHistoryHash The current streams history hash, see `_hashStreamsHistory`. |
|
| 595 | /// @return updateTime The time when streams have been configured for the last time. |
|
| 596 | /// @return balance The balance when streams have been configured for the last time. |
|
| 597 | /// @return maxEnd The current maximum end time of streaming. |
|
| 598 | 20× | function _streamsState(uint256 accountId, IERC20 erc20) |
| 599 | internal |
|
| 600 | view |
|
| 601 | returns ( |
|
| 602 | 2× | bytes32 streamsHash, |
| 603 | 2× | bytes32 streamsHistoryHash, |
| 604 | 2× | uint32 updateTime, |
| 605 | 2× | uint128 balance, |
| 606 | 2× | uint32 maxEnd |
| 607 | ) |
|
| 608 | { |
|
| 609 | 78× | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 610 | 22× | return ( |
| 611 | 8× | state.streamsHash, |
| 612 | 8× | state.streamsHistoryHash, |
| 613 | 26× | state.updateTime, |
| 614 | 26× | state.balance, |
| 615 | 26× | state.maxEnd |
| 616 | ); |
|
| 617 | } |
|
| 618 | ||
| 619 | /// @notice The account's streams balance at the given timestamp. |
|
| 620 | /// @param accountId The account ID. |
|
| 621 | /// @param erc20 The used ERC-20 token. |
|
| 622 | /// @param currReceivers The current streams receivers list. |
|
| 623 | /// It must be exactly the same as the last list set for the account with `_setStreams`. |
|
| 624 | /// @param timestamp The timestamp for which balance should be calculated. |
|
| 625 | /// It can't be lower than the timestamp of the last call to `_setStreams`. |
|
| 626 | /// If it's bigger than `block.timestamp`, then it's a prediction assuming |
|
| 627 | /// that `_setStreams` won't be called before `timestamp`. |
|
| 628 | /// @return balance The account balance on `timestamp` |
|
| 629 | 16× | function _balanceAt( |
| 630 | uint256 accountId, |
|
| 631 | IERC20 erc20, |
|
| 632 | StreamReceiver[] memory currReceivers, |
|
| 633 | uint32 timestamp |
|
| 634 | 2× | ) internal view returns (uint128 balance) { |
| 635 | 78× | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 636 | 46× | require(timestamp >= state.updateTime, "Timestamp before the last update"); |
| 637 | 12× | _verifyStreamsReceivers(currReceivers, state); |
| 638 | 96× | return _calcBalance(state.balance, state.updateTime, state.maxEnd, currReceivers, timestamp); |
| 639 | } |
|
| 640 | ||
| 641 | /// @notice Calculates the streams balance at a given timestamp. |
|
| 642 | /// @param lastBalance The balance when streaming started. |
|
| 643 | /// @param lastUpdate The timestamp when streaming started. |
|
| 644 | /// @param maxEnd The maximum end time of streaming. |
|
| 645 | /// @param receivers The list of streams receivers. |
|
| 646 | /// @param timestamp The timestamp for which balance should be calculated. |
|
| 647 | /// It can't be lower than `lastUpdate`. |
|
| 648 | /// If it's bigger than `block.timestamp`, then it's a prediction assuming |
|
| 649 | /// that `_setStreams` won't be called before `timestamp`. |
|
| 650 | /// @return balance The account balance on `timestamp` |
|
| 651 | 27× | function _calcBalance( |
| 652 | uint128 lastBalance, |
|
| 653 | uint32 lastUpdate, |
|
| 654 | uint32 maxEnd, |
|
| 655 | StreamReceiver[] memory receivers, |
|
| 656 | uint32 timestamp |
|
| 657 | 3× | ) private view returns (uint128 balance) { |
| 658 | unchecked { |
|
| 659 | 9× | balance = lastBalance; |
| 660 | 69× | for (uint256 i = 0; i < receivers.length; i++) { |
| 661 | 54× | StreamReceiver memory receiver = receivers[i]; |
| 662 | 30× | (uint32 start, uint32 end) = _streamRange({ |
| 663 | 3× | receiver: receiver, |
| 664 | 3× | updateTime: lastUpdate, |
| 665 | 3× | maxEnd: maxEnd, |
| 666 | 3× | startCap: lastUpdate, |
| 667 | 3× | endCap: timestamp |
| 668 | }); |
|
| 669 | 72× | balance -= uint128(_streamedAmt(receiver.config.amtPerSec(), start, end)); |
| 670 | } |
|
| 671 | } |
|
| 672 | } |
|
| 673 | ||
| 674 | /// @notice Sets the account's streams configuration. |
|
| 675 | /// @param accountId The account ID. |
|
| 676 | /// @param erc20 The used ERC-20 token. |
|
| 677 | /// @param currReceivers The current streams receivers list. |
|
| 678 | /// It must be exactly the same as the last list set for the account with `_setStreams`. |
|
| 679 | /// If this is the first update, pass an empty array. |
|
| 680 | /// @param balanceDelta The streams balance change to be applied. |
|
| 681 | /// If it's positive, the balance is increased by `balanceDelta`. |
|
| 682 | /// If it's zero, the balance doesn't change. |
|
| 683 | /// If it's negative, the balance is decreased by `balanceDelta`, |
|
| 684 | /// but the change is capped at the current balance amount, so it doesn't go below 0. |
|
| 685 | /// Passing `type(int128).min` always decreases the current balance to 0. |
|
| 686 | /// @param newReceivers The list of the streams receivers of the account to be set. |
|
| 687 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 688 | /// without identical elements and without 0 amtPerSecs. |
|
| 689 | /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 690 | /// The first hint for finding the maximum end time when all streams stop due to funds |
|
| 691 | /// running out after the balance is updated and the new receivers list is applied. |
|
| 692 | /// Hints have no effect on the results of calling this function, except potentially saving gas. |
|
| 693 | /// Hints are Unix timestamps used as the starting points for binary search for the time |
|
| 694 | /// when funds run out in the range of timestamps from the current block's to `2^32`. |
|
| 695 | /// Hints lower than the current timestamp are ignored. |
|
| 696 | /// You can provide zero, one or two hints. The order of hints doesn't matter. |
|
| 697 | /// Hints are the most effective when one of them is lower than or equal to |
|
| 698 | /// the last timestamp when funds are still streamed, and the other one is strictly larger |
|
| 699 | /// than that timestamp,the smaller the difference between such hints, the higher gas savings. |
|
| 700 | /// The savings are the highest possible when one of the hints is equal to |
|
| 701 | /// the last timestamp when funds are still streamed, and the other one is larger by 1. |
|
| 702 | /// It's worth noting that the exact timestamp of the block in which this function is executed |
|
| 703 | /// may affect correctness of the hints, especially if they're precise. |
|
| 704 | /// Hints don't provide any benefits when balance is not enough to cover |
|
| 705 | /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. |
|
| 706 | /// Even inaccurate hints can be useful, and providing a single hint |
|
| 707 | /// or two hints that don't enclose the time when funds run out can still save some gas. |
|
| 708 | /// Providing poor hints that don't reduce the number of binary search steps |
|
| 709 | /// may cause slightly higher gas usage than not providing any hints. |
|
| 710 | /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. |
|
| 711 | /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. |
|
| 712 | /// @return realBalanceDelta The actually applied streams balance change. |
|
| 713 | /// It's equal to the passed `balanceDelta`, unless it's negative |
|
| 714 | /// and it gets capped at the current balance amount. |
|
| 715 | 22× | function _setStreams( |
| 716 | uint256 accountId, |
|
| 717 | IERC20 erc20, |
|
| 718 | StreamReceiver[] memory currReceivers, |
|
| 719 | int128 balanceDelta, |
|
| 720 | StreamReceiver[] memory newReceivers, |
|
| 721 | // slither-disable-next-line similar-names |
|
| 722 | uint32 maxEndHint1, |
|
| 723 | uint32 maxEndHint2 |
|
| 724 | 2× | ) internal returns (int128 realBalanceDelta) { |
| 725 | 12× | unchecked { |
| 726 | 78× | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 727 | 12× | _verifyStreamsReceivers(currReceivers, state); |
| 728 | 32× | uint32 lastUpdate = state.updateTime; |
| 729 | 2× | uint128 newBalance; |
| 730 | 2× | uint32 newMaxEnd; |
| 731 | 4× | { |
| 732 | 32× | uint32 currMaxEnd = state.maxEnd; |
| 733 | 6× | int128 currBalance = int128( |
| 734 | 8× | _calcBalance( |
| 735 | 40× | state.balance, lastUpdate, currMaxEnd, currReceivers, _currTimestamp() |
| 736 | ) |
|
| 737 | ); |
|
| 738 | 6× | realBalanceDelta = balanceDelta; |
| 739 | // Cap `realBalanceDelta` at withdrawal of the entire `currBalance` |
|
| 740 | 26× | if (realBalanceDelta < -currBalance) { |
| 741 | 10× | realBalanceDelta = -currBalance; |
| 742 | } |
|
| 743 | // This will not overflow if the requirement of tracking in the contract |
|
| 744 | // no more than `_MAX_STREAMS_BALANCE` of each token is followed. |
|
| 745 | 10× | newBalance = uint128(currBalance + realBalanceDelta); |
| 746 | 20× | newMaxEnd = _calcMaxEnd(newBalance, newReceivers, maxEndHint1, maxEndHint2); |
| 747 | 8× | _updateReceiverStates( |
| 748 | 46× | _streamsStorage().states[erc20], |
| 749 | 2× | currReceivers, |
| 750 | 2× | lastUpdate, |
| 751 | 2× | currMaxEnd, |
| 752 | 2× | newReceivers, |
| 753 | 2× | newMaxEnd |
| 754 | ); |
|
| 755 | } |
|
| 756 | 52× | state.updateTime = _currTimestamp(); |
| 757 | 46× | state.maxEnd = newMaxEnd; |
| 758 | 46× | state.balance = newBalance; |
| 759 | 14× | bytes32 streamsHistory = state.streamsHistoryHash; |
| 760 | // slither-disable-next-line timestamp |
|
| 761 | 76× | if (streamsHistory != 0 && _cycleOf(lastUpdate) != _cycleOf(_currTimestamp())) { |
| 762 | 46× | state.lastUpdatedCycleConfigs = 2; |
| 763 | } else { |
|
| 764 | 84× | state.lastUpdatedCycleConfigs++; |
| 765 | } |
|
| 766 | 16× | bytes32 newStreamsHash = _hashStreams(newReceivers); |
| 767 | 14× | state.streamsHistoryHash = |
| 768 | 22× | _hashStreamsHistory(streamsHistory, newStreamsHash, _currTimestamp(), newMaxEnd); |
| 769 | 52× | emit StreamsSet(accountId, erc20, newStreamsHash, streamsHistory, newBalance, newMaxEnd); |
| 770 | // slither-disable-next-line timestamp |
|
| 771 | 18× | if (newStreamsHash != state.streamsHash) { |
| 772 | 16× | state.streamsHash = newStreamsHash; |
| 773 | 42× | for (uint256 i = 0; i < newReceivers.length; i++) { |
| 774 | 36× | StreamReceiver memory receiver = newReceivers[i]; |
| 775 | 50× | emit StreamReceiverSeen(newStreamsHash, receiver.accountId, receiver.config); |
| 776 | } |
|
| 777 | } |
|
| 778 | } |
|
| 779 | } |
|
| 780 | ||
| 781 | /// @notice Verifies that the provided list of receivers is currently active for the account. |
|
| 782 | /// @param currReceivers The verified list of receivers. |
|
| 783 | /// @param state The account's state. |
|
| 784 | 12× | function _verifyStreamsReceivers( |
| 785 | StreamReceiver[] memory currReceivers, |
|
| 786 | StreamsState storage state |
|
| 787 | ) private view { |
|
| 788 | 39× | require(_hashStreams(currReceivers) == state.streamsHash, "Invalid streams receivers list"); |
| 789 | } |
|
| 790 | ||
| 791 | /// @notice Calculates the maximum end time of all streams. |
|
| 792 | /// @param balance The balance when streaming starts. |
|
| 793 | /// @param receivers The list of streams receivers. |
|
| 794 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 795 | /// without identical elements and without 0 amtPerSecs. |
|
| 796 | /// @param hint1 The first hint for finding the maximum end time. |
|
| 797 | /// See `_setStreams` docs for `maxEndHint1` for more details. |
|
| 798 | /// @param hint2 The second hint for finding the maximum end time. |
|
| 799 | /// See `_setStreams` docs for `maxEndHint2` for more details. |
|
| 800 | /// @return maxEnd The maximum end time of streaming. |
|
| 801 | 18× | function _calcMaxEnd( |
| 802 | uint128 balance, |
|
| 803 | StreamReceiver[] memory receivers, |
|
| 804 | uint32 hint1, |
|
| 805 | uint32 hint2 |
|
| 806 | 2× | ) private view returns (uint32 maxEnd) { |
| 807 | 22× | (uint256[] memory configs, uint256 configsLen) = _buildConfigs(receivers); |
| 808 | ||
| 809 | 18× | uint256 enoughEnd = _currTimestamp(); |
| 810 | // slither-disable-start incorrect-equality,timestamp |
|
| 811 | 34× | if (configsLen == 0 || balance == 0) { |
| 812 | 16× | return uint32(enoughEnd); |
| 813 | } |
|
| 814 | ||
| 815 | 12× | uint256 notEnoughEnd = type(uint32).max; |
| 816 | 28× | if (_isBalanceEnough(balance, configs, configsLen, notEnoughEnd)) { |
| 817 | 18× | return uint32(notEnoughEnd); |
| 818 | } |
|
| 819 | ||
| 820 | 40× | if (hint1 > enoughEnd && hint1 < notEnoughEnd) { |
| 821 | 38× | if (_isBalanceEnough(balance, configs, configsLen, hint1)) { |
| 822 | 10× | enoughEnd = hint1; |
| 823 | } else { |
|
| 824 | 10× | notEnoughEnd = hint1; |
| 825 | } |
|
| 826 | } |
|
| 827 | ||
| 828 | 40× | if (hint2 > enoughEnd && hint2 < notEnoughEnd) { |
| 829 | 38× | if (_isBalanceEnough(balance, configs, configsLen, hint2)) { |
| 830 | 10× | enoughEnd = hint2; |
| 831 | } else { |
|
| 832 | 10× | notEnoughEnd = hint2; |
| 833 | } |
|
| 834 | } |
|
| 835 | ||
| 836 | 16× | while (true) { |
| 837 | 2× | uint256 end; |
| 838 | unchecked { |
|
| 839 | 22× | end = (enoughEnd + notEnoughEnd) / 2; |
| 840 | } |
|
| 841 | 12× | if (end == enoughEnd) { |
| 842 | 20× | return uint32(end); |
| 843 | } |
|
| 844 | 34× | if (_isBalanceEnough(balance, configs, configsLen, end)) { |
| 845 | 6× | enoughEnd = end; |
| 846 | } else { |
|
| 847 | 6× | notEnoughEnd = end; |
| 848 | } |
|
| 849 | } |
|
| 850 | // slither-disable-end incorrect-equality,timestamp |
|
| 851 | } |
|
| 852 | ||
| 853 | /// @notice Check if a given balance is enough to cover all streams with the given `maxEnd`. |
|
| 854 | /// @param balance The balance when streaming starts. |
|
| 855 | /// @param configs The list of streams configurations. |
|
| 856 | /// @param configsLen The length of `configs`. |
|
| 857 | /// @param maxEnd The maximum end time of streaming. |
|
| 858 | /// @return isEnough `true` if the balance is enough, `false` otherwise. |
|
| 859 | 18× | function _isBalanceEnough( |
| 860 | uint256 balance, |
|
| 861 | uint256[] memory configs, |
|
| 862 | uint256 configsLen, |
|
| 863 | uint256 maxEnd |
|
| 864 | 2× | ) private view returns (bool isEnough) { |
| 865 | unchecked { |
|
| 866 | 8× | uint256 spent = 0; |
| 867 | 46× | for (uint256 i = 0; i < configsLen; i++) { |
| 868 | 30× | (uint256 amtPerSec, uint256 start, uint256 end) = _getConfig(configs, i); |
| 869 | // slither-disable-next-line timestamp |
|
| 870 | 12× | if (maxEnd <= start) { |
| 871 | 10× | continue; |
| 872 | } |
|
| 873 | // slither-disable-next-line timestamp |
|
| 874 | 14× | if (end > maxEnd) { |
| 875 | 6× | end = maxEnd; |
| 876 | } |
|
| 877 | 22× | spent += _streamedAmt(amtPerSec, start, end); |
| 878 | 14× | if (spent > balance) { |
| 879 | 20× | return false; |
| 880 | } |
|
| 881 | } |
|
| 882 | 8× | return true; |
| 883 | } |
|
| 884 | } |
|
| 885 | ||
| 886 | /// @notice Build a preprocessed list of streams configurations from receivers. |
|
| 887 | /// @param receivers The list of streams receivers. |
|
| 888 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 889 | /// without identical elements and without 0 amtPerSecs. |
|
| 890 | /// @return configs The list of streams configurations |
|
| 891 | /// @return configsLen The length of `configs` |
|
| 892 | 10× | function _buildConfigs(StreamReceiver[] memory receivers) |
| 893 | private |
|
| 894 | view |
|
| 895 | 4× | returns (uint256[] memory configs, uint256 configsLen) |
| 896 | { |
|
| 897 | unchecked { |
|
| 898 | 14× | require(receivers.length <= _MAX_STREAMS_RECEIVERS, "Too many streams receivers"); |
| 899 | 100× | configs = new uint256[](receivers.length); |
| 900 | 42× | for (uint256 i = 0; i < receivers.length; i++) { |
| 901 | 36× | StreamReceiver memory receiver = receivers[i]; |
| 902 | 14× | if (i > 0) { |
| 903 | 50× | require(_isOrdered(receivers[i - 1], receiver), "Streams receivers not sorted"); |
| 904 | } |
|
| 905 | 18× | configsLen = _addConfig(configs, configsLen, receiver); |
| 906 | } |
|
| 907 | } |
|
| 908 | } |
|
| 909 | ||
| 910 | /// @notice Preprocess and add a stream receiver to the list of configurations. |
|
| 911 | /// @param configs The list of streams configurations |
|
| 912 | /// @param configsLen The length of `configs` |
|
| 913 | /// @param receiver The added stream receiver. |
|
| 914 | /// @return newConfigsLen The new length of `configs` |
|
| 915 | 16× | function _addConfig( |
| 916 | uint256[] memory configs, |
|
| 917 | uint256 configsLen, |
|
| 918 | StreamReceiver memory receiver |
|
| 919 | 2× | ) private view returns (uint256 newConfigsLen) { |
| 920 | 22× | uint160 amtPerSec = receiver.config.amtPerSec(); |
| 921 | 41× | require(amtPerSec >= _minAmtPerSec, "Stream receiver amtPerSec too low"); |
| 922 | 12× | (uint32 start, uint32 end) = |
| 923 | 20× | _streamRangeInFuture(receiver, _currTimestamp(), type(uint32).max); |
| 924 | // slither-disable-next-line incorrect-equality,timestamp |
|
| 925 | 20× | if (start == end) { |
| 926 | 16× | return configsLen; |
| 927 | } |
|
| 928 | // By assignment we get `config` value: |
|
| 929 | // `zeros (96 bits) | amtPerSec (160 bits)` |
|
| 930 | 12× | uint256 config = amtPerSec; |
| 931 | // By bit shifting we get `config` value: |
|
| 932 | // `zeros (64 bits) | amtPerSec (160 bits) | zeros (32 bits)` |
|
| 933 | // By bit masking we get `config` value: |
|
| 934 | // `zeros (64 bits) | amtPerSec (160 bits) | start (32 bits)` |
|
| 935 | 20× | config = (config << 32) | start; |
| 936 | // By bit shifting we get `config` value: |
|
| 937 | // `zeros (32 bits) | amtPerSec (160 bits) | start (32 bits) | zeros (32 bits)` |
|
| 938 | // By bit masking we get `config` value: |
|
| 939 | // `zeros (32 bits) | amtPerSec (160 bits) | start (32 bits) | end (32 bits)` |
|
| 940 | 20× | config = (config << 32) | end; |
| 941 | 40× | configs[configsLen] = config; |
| 942 | unchecked { |
|
| 943 | 18× | return configsLen + 1; |
| 944 | } |
|
| 945 | } |
|
| 946 | ||
| 947 | /// @notice Load a streams configuration from the list. |
|
| 948 | /// @param configs The list of streams configurations |
|
| 949 | /// @param idx The loaded configuration index. It must be smaller than the `configs` length. |
|
| 950 | /// @return amtPerSec The amount per second being streamed. |
|
| 951 | /// @return start The timestamp when streaming starts. |
|
| 952 | /// @return end The maximum timestamp when streaming ends. |
|
| 953 | 14× | function _getConfig(uint256[] memory configs, uint256 idx) |
| 954 | private |
|
| 955 | pure |
|
| 956 | 6× | returns (uint256 amtPerSec, uint256 start, uint256 end) |
| 957 | 2× | { |
| 958 | 2× | uint256 config; |
| 959 | // `config` has value: |
|
| 960 | // `zeros (32 bits) | amtPerSec (160 bits) | start (32 bits) | end (32 bits)` |
|
| 961 | // slither-disable-next-line assembly |
|
| 962 | assembly ("memory-safe") { |
|
| 963 | 20× | config := mload(add(32, add(configs, shl(5, idx)))) |
| 964 | } |
|
| 965 | // By bit shifting we get value: |
|
| 966 | // `zeros (96 bits) | amtPerSec (160 bits)` |
|
| 967 | 12× | amtPerSec = config >> 64; |
| 968 | // By bit shifting we get value: |
|
| 969 | // `zeros (64 bits) | amtPerSec (160 bits) | start (32 bits)` |
|
| 970 | // By casting down we get value: |
|
| 971 | // `start (32 bits)` |
|
| 972 | 16× | start = uint32(config >> 32); |
| 973 | // By casting down we get value: |
|
| 974 | // `end (32 bits)` |
|
| 975 | 10× | end = uint32(config); |
| 976 | } |
|
| 977 | ||
| 978 | /// @notice Calculates the hash of the streams configuration. |
|
| 979 | /// It's used to verify if streams configuration is the previously set one. |
|
| 980 | /// @param receivers The list of the streams receivers. |
|
| 981 | /// If the streams have never been set, pass an empty array. |
|
| 982 | /// @return streamsHash The hash of the streams configuration |
|
| 983 | 18× | function _hashStreams(StreamReceiver[] memory receivers) |
| 984 | internal |
|
| 985 | pure |
|
| 986 | 3× | returns (bytes32 streamsHash) |
| 987 | { |
|
| 988 | 21× | if (receivers.length == 0) { |
| 989 | 21× | return bytes32(0); |
| 990 | } |
|
| 991 | 93× | return keccak256(abi.encode(receivers)); |
| 992 | } |
|
| 993 | ||
| 994 | /// @notice Calculates the hash of the streams history |
|
| 995 | /// after the streams configuration is updated. |
|
| 996 | /// @param oldStreamsHistoryHash The history hash |
|
| 997 | /// that was valid before the streams were updated. |
|
| 998 | /// The `streamsHistoryHash` of an account before they set streams for the first time is `0`. |
|
| 999 | /// @param streamsHash The hash of the streams receivers being set. |
|
| 1000 | /// @param updateTime The timestamp when the streams were updated. |
|
| 1001 | /// @param maxEnd The maximum end of the streams being set. |
|
| 1002 | /// @return streamsHistoryHash The hash of the updated streams history. |
|
| 1003 | 24× | function _hashStreamsHistory( |
| 1004 | bytes32 oldStreamsHistoryHash, |
|
| 1005 | bytes32 streamsHash, |
|
| 1006 | uint32 updateTime, |
|
| 1007 | uint32 maxEnd |
|
| 1008 | 3× | ) internal pure returns (bytes32 streamsHistoryHash) { |
| 1009 | 111× | return keccak256(abi.encode(oldStreamsHistoryHash, streamsHash, updateTime, maxEnd)); |
| 1010 | } |
|
| 1011 | ||
| 1012 | /// @notice Applies the effects of the change of the streams on the receivers' streams state. |
|
| 1013 | /// Only affects `amtDeltas` and `nextReceivableCycle` of the accounts |
|
| 1014 | /// present either in `currReceivers` or in `newReceivers`. |
|
| 1015 | /// @param states The streams states for the used ERC-20 token. |
|
| 1016 | /// @param currReceivers The list of the streams receivers |
|
| 1017 | /// set in the last streams update of the account. |
|
| 1018 | /// If this is the first update, pass an empty array. |
|
| 1019 | /// @param lastUpdate the last time the sender updated the streams. |
|
| 1020 | /// If this is the first update, pass zero. |
|
| 1021 | /// @param currMaxEnd The maximum end time of streaming according to the last streams update. |
|
| 1022 | /// @param newReceivers The list of the streams receivers of the account to be set. |
|
| 1023 | /// Must be sorted by the account IDs and then by the stream configurations, |
|
| 1024 | /// without identical elements and without 0 amtPerSecs. |
|
| 1025 | /// @param newMaxEnd The maximum end time of streaming according to the new configuration. |
|
| 1026 | // slither-disable-next-line cyclomatic-complexity |
|
| 1027 | 16× | function _updateReceiverStates( |
| 1028 | mapping(uint256 accountId => StreamsState) storage states, |
|
| 1029 | StreamReceiver[] memory currReceivers, |
|
| 1030 | uint32 lastUpdate, |
|
| 1031 | uint32 currMaxEnd, |
|
| 1032 | StreamReceiver[] memory newReceivers, |
|
| 1033 | uint32 newMaxEnd |
|
| 1034 | 4× | ) private { |
| 1035 | 2× | uint256 currIdx = 0; |
| 1036 | 2× | uint256 newIdx = 0; |
| 1037 | 24× | while (true) { |
| 1038 | 14× | bool pickCurr = currIdx < currReceivers.length; |
| 1039 | // slither-disable-next-line uninitialized-local |
|
| 1040 | 8× | StreamReceiver memory currRecv; |
| 1041 | 10× | if (pickCurr) { |
| 1042 | 34× | currRecv = currReceivers[currIdx]; |
| 1043 | } |
|
| 1044 | ||
| 1045 | 14× | bool pickNew = newIdx < newReceivers.length; |
| 1046 | // slither-disable-next-line uninitialized-local |
|
| 1047 | 8× | StreamReceiver memory newRecv; |
| 1048 | 10× | if (pickNew) { |
| 1049 | 34× | newRecv = newReceivers[newIdx]; |
| 1050 | } |
|
| 1051 | ||
| 1052 | // Limit picking both curr and new to situations when they |
|
| 1053 | // differ only by the stream ID, the start or the duration. |
|
| 1054 | 24× | if (pickCurr && pickNew) { |
| 1055 | 8× | if ( |
| 1056 | 30× | currRecv.accountId != newRecv.accountId |
| 1057 | 44× | || currRecv.config.amtPerSec() != newRecv.config.amtPerSec() |
| 1058 | ) { |
|
| 1059 | 16× | pickCurr = _isOrdered(currRecv, newRecv); |
| 1060 | 8× | pickNew = !pickCurr; |
| 1061 | } |
|
| 1062 | } |
|
| 1063 | ||
| 1064 | 46× | if (pickCurr && pickNew) { |
| 1065 | // Shift the existing stream to fulfil the new configuration |
|
| 1066 | 40× | StreamsState storage state = states[currRecv.accountId]; |
| 1067 | 12× | (uint32 currStart, uint32 currEnd) = |
| 1068 | 14× | _streamRangeInFuture(currRecv, lastUpdate, currMaxEnd); |
| 1069 | 12× | (uint32 newStart, uint32 newEnd) = |
| 1070 | 20× | _streamRangeInFuture(newRecv, _currTimestamp(), newMaxEnd); |
| 1071 | 26× | int256 amtPerSec = int256(uint256(currRecv.config.amtPerSec())); |
| 1072 | // Move the start and end times if updated. This has the same effects as calling |
|
| 1073 | // _addDeltaRange(state, currStart, currEnd, -amtPerSec); |
|
| 1074 | // _addDeltaRange(state, newStart, newEnd, amtPerSec); |
|
| 1075 | // but it allows skipping storage access if there's no change to the starts or ends. |
|
| 1076 | 26× | _addDeltaRange(state, currStart, newStart, -amtPerSec); |
| 1077 | 16× | _addDeltaRange(state, currEnd, newEnd, amtPerSec); |
| 1078 | // Ensure that the account receives the updated cycles |
|
| 1079 | 16× | uint32 currStartCycle = _cycleOf(currStart); |
| 1080 | 16× | uint32 newStartCycle = _cycleOf(newStart); |
| 1081 | // The `currStartCycle > newStartCycle` check is just an optimization. |
|
| 1082 | // If it's false, then `state.nextReceivableCycle > newStartCycle` must be |
|
| 1083 | // false too, there's no need to pay for the storage access to check it. |
|
| 1084 | // slither-disable-next-line timestamp |
|
| 1085 | 32× | if (currStartCycle > newStartCycle && state.nextReceivableCycle > newStartCycle) { |
| 1086 | 0 | state.nextReceivableCycle = newStartCycle; |
| 1087 | } |
|
| 1088 | 24× | } else if (pickCurr) { |
| 1089 | // Remove an existing stream |
|
| 1090 | // slither-disable-next-line similar-names |
|
| 1091 | 40× | StreamsState storage state = states[currRecv.accountId]; |
| 1092 | 26× | (uint32 start, uint32 end) = _streamRangeInFuture(currRecv, lastUpdate, currMaxEnd); |
| 1093 | // slither-disable-next-line similar-names |
|
| 1094 | 26× | int256 amtPerSec = int256(uint256(currRecv.config.amtPerSec())); |
| 1095 | 26× | _addDeltaRange(state, start, end, -amtPerSec); |
| 1096 | 28× | } else if (pickNew) { |
| 1097 | // Create a new stream |
|
| 1098 | 40× | StreamsState storage state = states[newRecv.accountId]; |
| 1099 | // slither-disable-next-line uninitialized-local |
|
| 1100 | 12× | (uint32 start, uint32 end) = |
| 1101 | 20× | _streamRangeInFuture(newRecv, _currTimestamp(), newMaxEnd); |
| 1102 | 26× | int256 amtPerSec = int256(uint256(newRecv.config.amtPerSec())); |
| 1103 | 16× | _addDeltaRange(state, start, end, amtPerSec); |
| 1104 | // Ensure that the account receives the updated cycles |
|
| 1105 | 16× | uint32 startCycle = _cycleOf(start); |
| 1106 | // slither-disable-next-line timestamp |
|
| 1107 | 32× | uint32 nextReceivableCycle = state.nextReceivableCycle; |
| 1108 | 42× | if (nextReceivableCycle == 0 || nextReceivableCycle > startCycle) { |
| 1109 | 46× | state.nextReceivableCycle = startCycle; |
| 1110 | } |
|
| 1111 | } else { |
|
| 1112 | 12× | break; |
| 1113 | } |
|
| 1114 | ||
| 1115 | unchecked { |
|
| 1116 | 10× | if (pickCurr) { |
| 1117 | 14× | currIdx++; |
| 1118 | } |
|
| 1119 | 10× | if (pickNew) { |
| 1120 | 14× | newIdx++; |
| 1121 | } |
|
| 1122 | } |
|
| 1123 | } |
|
| 1124 | } |
|
| 1125 | ||
| 1126 | /// @notice Calculates the time range in the future in which a receiver will be streamed to. |
|
| 1127 | /// @param receiver The stream receiver. |
|
| 1128 | /// @param maxEnd The maximum end time of streaming. |
|
| 1129 | 16× | function _streamRangeInFuture(StreamReceiver memory receiver, uint32 updateTime, uint32 maxEnd) |
| 1130 | private |
|
| 1131 | view |
|
| 1132 | 4× | returns (uint32 start, uint32 end) |
| 1133 | { |
|
| 1134 | 32× | return _streamRange(receiver, updateTime, maxEnd, _currTimestamp(), type(uint32).max); |
| 1135 | } |
|
| 1136 | ||
| 1137 | /// @notice Calculates the time range in which a receiver is to be streamed to. |
|
| 1138 | /// This range is capped to provide a view on the stream through a specific time window. |
|
| 1139 | /// @param receiver The stream receiver. |
|
| 1140 | /// @param updateTime The time when the stream is configured. |
|
| 1141 | /// @param maxEnd The maximum end time of streaming. |
|
| 1142 | /// @param startCap The timestamp the streaming range start should be capped to. |
|
| 1143 | /// @param endCap The timestamp the streaming range end should be capped to. |
|
| 1144 | 30× | function _streamRange( |
| 1145 | StreamReceiver memory receiver, |
|
| 1146 | uint32 updateTime, |
|
| 1147 | uint32 maxEnd, |
|
| 1148 | uint32 startCap, |
|
| 1149 | uint32 endCap |
|
| 1150 | 6× | ) private pure returns (uint32 start, uint32 end_) { |
| 1151 | 30× | start = receiver.config.start(); |
| 1152 | // slither-disable-start timestamp |
|
| 1153 | 24× | if (start == 0) { |
| 1154 | 9× | start = updateTime; |
| 1155 | } |
|
| 1156 | 3× | uint40 end; |
| 1157 | unchecked { |
|
| 1158 | 48× | end = uint40(start) + receiver.config.duration(); |
| 1159 | } |
|
| 1160 | // slither-disable-next-line incorrect-equality |
|
| 1161 | 69× | if (end == start || end > maxEnd) { |
| 1162 | 15× | end = maxEnd; |
| 1163 | } |
|
| 1164 | 33× | if (start < startCap) { |
| 1165 | 9× | start = startCap; |
| 1166 | } |
|
| 1167 | 33× | if (end > endCap) { |
| 1168 | 15× | end = endCap; |
| 1169 | } |
|
| 1170 | 33× | if (end < start) { |
| 1171 | 15× | end = start; |
| 1172 | } |
|
| 1173 | // slither-disable-end timestamp |
|
| 1174 | 21× | return (start, uint32(end)); |
| 1175 | } |
|
| 1176 | ||
| 1177 | /// @notice Adds funds received by an account in a given time range |
|
| 1178 | /// @param state The account state |
|
| 1179 | /// @param start The timestamp from which the delta takes effect |
|
| 1180 | /// @param end The timestamp until which the delta takes effect |
|
| 1181 | /// @param amtPerSec The streaming rate |
|
| 1182 | 21× | function _addDeltaRange(StreamsState storage state, uint32 start, uint32 end, int256 amtPerSec) |
| 1183 | private |
|
| 1184 | 3× | { |
| 1185 | // slither-disable-next-line incorrect-equality,timestamp |
|
| 1186 | 27× | if (start == end) { |
| 1187 | 3× | return; |
| 1188 | } |
|
| 1189 | 18× | mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
| 1190 | 27× | _addDelta(amtDeltas, start, amtPerSec); |
| 1191 | 42× | _addDelta(amtDeltas, end, -amtPerSec); |
| 1192 | } |
|
| 1193 | ||
| 1194 | /// @notice Adds delta of funds received by an account at a given time |
|
| 1195 | /// @param amtDeltas The account amount deltas |
|
| 1196 | /// @param timestamp The timestamp when the deltas need to be added |
|
| 1197 | /// @param amtPerSec The streaming rate |
|
| 1198 | 15× | function _addDelta( |
| 1199 | mapping(uint32 cycle => AmtDelta) storage amtDeltas, |
|
| 1200 | uint256 timestamp, |
|
| 1201 | int256 amtPerSec |
|
| 1202 | ) private { |
|
| 1203 | 12× | unchecked { |
| 1204 | // In order to set a delta on a specific timestamp it must be introduced in two cycles. |
|
| 1205 | // These formulas follow the logic from `_streamedAmt`, see it for more details. |
|
| 1206 | 15× | int256 amtPerSecMultiplier = int160(_AMT_PER_SEC_MULTIPLIER); |
| 1207 | 42× | int256 fullCycle = (int256(uint256(_cycleSecs)) * amtPerSec) / amtPerSecMultiplier; |
| 1208 | // slither-disable-next-line weak-prng |
|
| 1209 | 60× | int256 nextCycle = (int256(timestamp % _cycleSecs) * amtPerSec) / amtPerSecMultiplier; |
| 1210 | 75× | AmtDelta storage amtDelta = amtDeltas[_cycleOf(uint32(timestamp))]; |
| 1211 | // Any over- or under-flows are fine, they're guaranteed to be fixed by a matching |
|
| 1212 | // under- or over-flow from the other call to `_addDelta` made by `_addDeltaRange`. |
|
| 1213 | // This is because the total balance of `Streams` can never exceed `type(int128).max`, |
|
| 1214 | // so in the end no amtDelta can have delta higher than `type(int128).max`. |
|
| 1215 | 126× | amtDelta.thisCycle += int128(fullCycle - nextCycle); |
| 1216 | 120× | amtDelta.nextCycle += int128(nextCycle); |
| 1217 | } |
|
| 1218 | } |
|
| 1219 | ||
| 1220 | /// @notice Checks if two receivers fulfil the sortedness requirement of the receivers list. |
|
| 1221 | /// @param prev The previous receiver |
|
| 1222 | /// @param next The next receiver |
|
| 1223 | 14× | function _isOrdered(StreamReceiver memory prev, StreamReceiver memory next) |
| 1224 | private |
|
| 1225 | pure |
|
| 1226 | 2× | returns (bool) |
| 1227 | { |
|
| 1228 | 24× | if (prev.accountId != next.accountId) { |
| 1229 | 26× | return prev.accountId < next.accountId; |
| 1230 | } |
|
| 1231 | 38× | return prev.config.lt(next.config); |
| 1232 | } |
|
| 1233 | ||
| 1234 | /// @notice Calculates the amount streamed over a time range. |
|
| 1235 | /// The amount streamed in the `N`th second of each cycle is: |
|
| 1236 | /// `(N + 1) * amtPerSec / AMT_PER_SEC_MULTIPLIER - N * amtPerSec / AMT_PER_SEC_MULTIPLIER`. |
|
| 1237 | /// For a range of `N`s from `0` to `M` the sum of the streamed amounts is calculated as: |
|
| 1238 | /// `M * amtPerSec / AMT_PER_SEC_MULTIPLIER` assuming that `M <= cycleSecs`. |
|
| 1239 | /// For an arbitrary time range across multiple cycles the amount |
|
| 1240 | /// is calculated as the sum of the amount streamed in the start cycle, |
|
| 1241 | /// each of the full cycles in between and the end cycle. |
|
| 1242 | /// This algorithm has the following properties: |
|
| 1243 | /// - During every second full units are streamed, there are no partially streamed units. |
|
| 1244 | /// - Unstreamed fractions are streamed when they add up into full units. |
|
| 1245 | /// - Unstreamed fractions don't add up across cycle end boundaries. |
|
| 1246 | /// - Some seconds stream 1 unit more to emulate streaming fractions of the unit. |
|
| 1247 | /// - Every `N`th second of each cycle streams the same amount. |
|
| 1248 | /// - Every full cycle streams the same amount. |
|
| 1249 | /// - The amount streamed in a given second is independent from the streaming start and end. |
|
| 1250 | /// - Streaming over time ranges `A:B` and then `B:C` is equivalent to streaming over `A:C`. |
|
| 1251 | /// - Different streams existing in the system don't interfere with each other. |
|
| 1252 | /// @param amtPerSec The streaming rate |
|
| 1253 | /// @param start The streaming start time |
|
| 1254 | /// @param end The streaming end time |
|
| 1255 | /// @return amt The streamed amount |
|
| 1256 | 21× | function _streamedAmt(uint256 amtPerSec, uint256 start, uint256 end) |
| 1257 | private |
|
| 1258 | view |
|
| 1259 | 3× | returns (uint256 amt) |
| 1260 | { |
|
| 1261 | // This function is written in Yul because it can be called thousands of times |
|
| 1262 | // per transaction and it needs to be optimized as much as possible. |
|
| 1263 | // As of Solidity 0.8.13, rewriting it in unchecked Solidity triples its gas cost. |
|
| 1264 | 18× | uint256 cycleSecs = _cycleSecs; |
| 1265 | // slither-disable-next-line assembly |
|
| 1266 | 15× | assembly { |
| 1267 | 21× | let endedCycles := sub(div(end, cycleSecs), div(start, cycleSecs)) |
| 1268 | // slither-disable-next-line divide-before-multiply |
|
| 1269 | 15× | let amtPerCycle := div(mul(cycleSecs, amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
| 1270 | 15× | amt := mul(endedCycles, amtPerCycle) |
| 1271 | // slither-disable-next-line weak-prng |
|
| 1272 | 21× | let amtEnd := div(mul(mod(end, cycleSecs), amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
| 1273 | 15× | amt := add(amt, amtEnd) |
| 1274 | // slither-disable-next-line weak-prng |
|
| 1275 | 21× | let amtStart := div(mul(mod(start, cycleSecs), amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
| 1276 | 15× | amt := sub(amt, amtStart) |
| 1277 | } |
|
| 1278 | } |
|
| 1279 | ||
| 1280 | /// @notice Calculates the cycle containing the given timestamp. |
|
| 1281 | /// @param timestamp The timestamp. |
|
| 1282 | /// @return cycle The cycle containing the timestamp. |
|
| 1283 | 18× | function _cycleOf(uint32 timestamp) private view returns (uint32 cycle) { |
| 1284 | unchecked { |
|
| 1285 | 45× | return timestamp / _cycleSecs + 1; |
| 1286 | } |
|
| 1287 | } |
|
| 1288 | ||
| 1289 | /// @notice The current timestamp, casted to the contract's internal representation. |
|
| 1290 | /// @return timestamp The current timestamp |
|
| 1291 | 12× | function _currTimestamp() private view returns (uint32 timestamp) { |
| 1292 | 9× | return uint32(block.timestamp); |
| 1293 | } |
|
| 1294 | ||
| 1295 | /// @notice The current cycle start timestamp, casted to the contract's internal representation. |
|
| 1296 | /// @return timestamp The current cycle start timestamp |
|
| 1297 | 12× | function _currCycleStart() private view returns (uint32 timestamp) { |
| 1298 | unchecked { |
|
| 1299 | 21× | uint32 currTimestamp = _currTimestamp(); |
| 1300 | // slither-disable-next-line weak-prng |
|
| 1301 | 48× | return currTimestamp - (currTimestamp % _cycleSecs); |
| 1302 | } |
|
| 1303 | } |
|
| 1304 | ||
| 1305 | /// @notice Returns the Streams storage. |
|
| 1306 | /// @return streamsStorage The storage. |
|
| 1307 | 12× | function _streamsStorage() private view returns (StreamsStorage storage streamsStorage) { |
| 1308 | 12× | bytes32 slot = _streamsStorageSlot; |
| 1309 | // slither-disable-next-line assembly |
|
| 1310 | 3× | assembly { |
| 1311 | 9× | streamsStorage.slot := slot |
| 1312 | } |
|
| 1313 | } |
|
| 1314 | } |
0%
src/echidna/Echidna.sol
Lines covered: 0 / 1 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./EchidnaInvariantTests.sol"; |
|
| 4 | import "./EchidnaBasicTests.sol"; |
|
| 5 | import "./EchidnaSplitsTests.sol"; |
|
| 6 | import "./EchidnaStreamsTests.sol"; |
|
| 7 | import "./EchidnaSqueezeTests.sol"; |
|
| 8 | ||
| 9 | /** |
|
| 10 | * @title Echidna contract for testing the Drips contract |
|
| 11 | * @author Rappie <rappie@perimetersec.io> |
|
| 12 | * @dev Running the tests: |
|
| 13 | * For Echidna use `echidna . --contract Echidna --config echidna-config.yaml`. |
|
| 14 | * For Medusa use `medusa fuzz`. |
|
| 15 | * The tests are split into multiple files to be able to toggle the fuzzing |
|
| 16 | * of specific features on and off. This is done by commenting out the |
|
| 17 | * corresponding inherited contract in the Echidna contract below. |
|
| 18 | * Basic tests contain basic features like giving, receiving, splitting and |
|
| 19 | * collecting. |
|
| 20 | * Splits tests contain tests for splitting. |
|
| 21 | * Streams tests contain tests for receiving streams. |
|
| 22 | * Squeeze tests contain tests for squeezing. |
|
| 23 | * Invariant tests contain tests for invariant properties of the system. These |
|
| 24 | * make most sense with all other tests enabled. |
|
| 25 | * For further performance improvements, resource heavy tests can be toggled. |
|
| 26 | * To do so, change the value of TOGGLE_HEAVY_TESTS_ENABLED in EchidnaConfig. |
|
| 27 | */ |
|
| 28 | 0 | contract Echidna is |
| 29 | EchidnaBasicTests, |
|
| 30 | EchidnaSplitsTests, |
|
| 31 | EchidnaStreamsTests, |
|
| 32 | EchidnaSqueezeTests, |
|
| 33 | EchidnaInvariantTests |
|
| 34 | { |
|
| 35 | ||
| 36 | } |
97%
src/echidna/EchidnaBasicHelpers.sol
Lines covered: 48 / 49 (97%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./base/EchidnaBase.sol"; |
|
| 4 | ||
| 5 | /** |
|
| 6 | * @title Mixin containing basic helper functions |
|
| 7 | * @author Rappie <rappie@perimetersec.io> |
|
| 8 | */ |
|
| 9 | 0 | contract EchidnaBasicHelpers is EchidnaBase { |
| 10 | /** |
|
| 11 | * @notice Give balance to an account |
|
| 12 | * @param fromAccId Account id of the giver |
|
| 13 | * @param toAccId Account id of the receiver |
|
| 14 | * @param amount Amount to give |
|
| 15 | */ |
|
| 16 | 24× | function give( |
| 17 | uint8 fromAccId, |
|
| 18 | uint8 toAccId, |
|
| 19 | uint128 amount |
|
| 20 | 3× | ) public { |
| 21 | 8× | address from = getAccount(fromAccId); |
| 22 | 8× | address to = getAccount(toAccId); |
| 23 | ||
| 24 | 8× | uint256 toDripsAccId = getDripsAccountId(to); |
| 25 | ||
| 26 | 62× | hevm.prank(from); |
| 27 | 83× | driver.give(toDripsAccId, token, amount); |
| 28 | } |
|
| 29 | ||
| 30 | /** |
|
| 31 | * @notice Give a clamped amount to an account |
|
| 32 | * @param fromAccId Account id of the giver |
|
| 33 | * @param toAccId Account id of the receiver |
|
| 34 | * @param amount Amount to give |
|
| 35 | */ |
|
| 36 | 24× | function giveClampedAmount( |
| 37 | uint8 fromAccId, |
|
| 38 | uint8 toAccId, |
|
| 39 | uint128 amount |
|
| 40 | 4× | ) public { |
| 41 | 8× | address from = getAccount(fromAccId); |
| 42 | ||
| 43 | 4× | uint128 min = 1000; |
| 44 | 79× | uint128 max = uint128(token.balanceOf(from)); |
| 45 | 32× | uint128 clampedAmount = min + (amount % (max - min + 1)); |
| 46 | ||
| 47 | 7× | give(fromAccId, toAccId, clampedAmount); |
| 48 | } |
|
| 49 | ||
| 50 | /** |
|
| 51 | * @notice Receive streams |
|
| 52 | * @param targetAccId Account id of the receiver |
|
| 53 | * @param maxCycles Maximum number of cycles to receive |
|
| 54 | * @return Amount received |
|
| 55 | * @dev Receiving means moving receivable (already streamed) balance to |
|
| 56 | * splittable (available for splitting) balance |
|
| 57 | */ |
|
| 58 | 45× | function receiveStreams(uint8 targetAccId, uint32 maxCycles) |
| 59 | public |
|
| 60 | 2× | returns (uint128) |
| 61 | { |
|
| 62 | 16× | address target = getAccount(targetAccId); |
| 63 | 16× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 64 | ||
| 65 | 176× | uint128 receivedAmt = drips.receiveStreams( |
| 66 | 2× | targetDripsAccId, |
| 67 | 22× | token, |
| 68 | 2× | maxCycles |
| 69 | ); |
|
| 70 | ||
| 71 | 12× | return receivedAmt; |
| 72 | } |
|
| 73 | ||
| 74 | /** |
|
| 75 | * @notice Receive streams for all possible cycles |
|
| 76 | * @param targetAccId Account id of the receiver |
|
| 77 | * @return Amount received |
|
| 78 | * @dev We pass maxuint32 to receive all possible cycles |
|
| 79 | */ |
|
| 80 | 43× | function receiveStreamsAllCycles(uint8 targetAccId) |
| 81 | public |
|
| 82 | 2× | returns (uint128) |
| 83 | { |
|
| 84 | 16× | return receiveStreams(targetAccId, type(uint32).max); |
| 85 | } |
|
| 86 | ||
| 87 | /** |
|
| 88 | * @notice Split received funds |
|
| 89 | * @param targetAccId Account to have their funds split |
|
| 90 | * @return Amount collectable and amount split |
|
| 91 | * @dev Splitting means moving splittable (available for splitting) balance |
|
| 92 | * to collectable (available for collecting) balance |
|
| 93 | */ |
|
| 94 | 89× | function split(uint8 targetAccId) public returns (uint128, uint128) { |
| 95 | 24× | address target = getAccount(targetAccId); |
| 96 | 24× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 97 | ||
| 98 | 252× | (uint128 collectableAmt, uint128 splitAmt) = drips.split( |
| 99 | 3× | targetDripsAccId, |
| 100 | 33× | token, |
| 101 | 15× | getSplitsReceivers(target) |
| 102 | ); |
|
| 103 | ||
| 104 | 30× | return (collectableAmt, splitAmt); |
| 105 | } |
|
| 106 | ||
| 107 | /** |
|
| 108 | * @notice Collect funds |
|
| 109 | * @param fromAccId Account id of the giver |
|
| 110 | * @param toAccId Account id of the receiver |
|
| 111 | * @return Amount collected |
|
| 112 | * @dev Collecting means moving withdrawing collectable funds to actual |
|
| 113 | * balance of the erc20 token |
|
| 114 | */ |
|
| 115 | 42× | function collect(uint8 fromAccId, uint8 toAccId) public returns (uint128) { |
| 116 | 16× | address from = getAccount(fromAccId); |
| 117 | 16× | address to = getAccount(toAccId); |
| 118 | ||
| 119 | 114× | hevm.prank(from); |
| 120 | 92× | uint128 collected = driver.collect(token, to); |
| 121 | ||
| 122 | 6× | return collected; |
| 123 | } |
|
| 124 | ||
| 125 | /** |
|
| 126 | * @notice Collect funds to self |
|
| 127 | * @param targetAccId Target account |
|
| 128 | */ |
|
| 129 | 23× | function collectToSelf(uint8 targetAccId) public { |
| 130 | 12× | collect(targetAccId, targetAccId); |
| 131 | } |
|
| 132 | ||
| 133 | /** |
|
| 134 | * @notice Split and collect funds to self |
|
| 135 | * @param targetAccId Target account |
|
| 136 | * @dev Extra helper that narrows the search space for the fuzzer |
|
| 137 | */ |
|
| 138 | 23× | function splitAndCollectToSelf(uint8 targetAccId) public { |
| 139 | 14× | split(targetAccId); |
| 140 | 9× | collectToSelf(targetAccId); |
| 141 | } |
|
| 142 | ||
| 143 | /** |
|
| 144 | * @notice Receive streams, split and collect funds to self |
|
| 145 | * @param targetAccId Target account |
|
| 146 | * @dev Extra helper that narrows the search space for the fuzzer |
|
| 147 | */ |
|
| 148 | 23× | function receiveStreamsSplitAndCollectToSelf(uint8 targetAccId) public { |
| 149 | 12× | receiveStreamsAllCycles(targetAccId); |
| 150 | 9× | splitAndCollectToSelf(targetAccId); |
| 151 | } |
|
| 152 | } |
95%
src/echidna/EchidnaBasicTests.sol
Lines covered: 66 / 69 (95%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./base/EchidnaBase.sol"; |
|
| 4 | import "./EchidnaBasicHelpers.sol"; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @title Mixin containing basic tests |
|
| 8 | * @author Rappie <rappie@perimetersec.io> |
|
| 9 | */ |
|
| 10 | 0 | contract EchidnaBasicTests is EchidnaBase, EchidnaBasicHelpers { |
| 11 | /** |
|
| 12 | * @notice Giving an amount `<=` token balance should never revert |
|
| 13 | * @param fromAccId Account id of the giver |
|
| 14 | * @param toAccId Account id of the receiver |
|
| 15 | * @param amount Amount to give |
|
| 16 | */ |
|
| 17 | 24× | function testGiveShouldNotRevert( |
| 18 | uint8 fromAccId, |
|
| 19 | uint8 toAccId, |
|
| 20 | uint128 amount |
|
| 21 | 3× | ) public { |
| 22 | 8× | address from = getAccount(fromAccId); |
| 23 | 8× | address to = getAccount(toAccId); |
| 24 | ||
| 25 | 8× | uint256 toDripsAccId = getDripsAccountId(to); |
| 26 | ||
| 27 | 87× | require(amount <= token.balanceOf(from)); |
| 28 | ||
| 29 | 62× | hevm.prank(from); |
| 30 | 81× | try driver.give(toDripsAccId, token, amount) {} catch { |
| 31 | 0 | assert(false); |
| 32 | } |
|
| 33 | } |
|
| 34 | ||
| 35 | /** |
|
| 36 | * @notice Test internal accounting after receiving streams |
|
| 37 | * @param targetAccId Account id of the receiver |
|
| 38 | * @param maxCycles Maximum number of cycles to receive |
|
| 39 | */ |
|
| 40 | 28× | function testReceiveStreams(uint8 targetAccId, uint32 maxCycles) public { |
| 41 | 8× | address target = getAccount(targetAccId); |
| 42 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 43 | ||
| 44 | 91× | uint128 splittableBefore = drips.splittable(targetDripsAccId, token); |
| 45 | 9× | uint128 receivedAmt = receiveStreams(targetAccId, maxCycles); |
| 46 | 91× | uint128 splittableAfter = drips.splittable(targetDripsAccId, token); |
| 47 | ||
| 48 | 17× | assert(splittableAfter == splittableBefore + receivedAmt); |
| 49 | ||
| 50 | 12× | if (receivedAmt > 0) { |
| 51 | 10× | assert(splittableAfter > splittableBefore); |
| 52 | } else { |
|
| 53 | 10× | assert(splittableAfter == splittableBefore); |
| 54 | } |
|
| 55 | } |
|
| 56 | ||
| 57 | /** |
|
| 58 | * @notice Receiving streams should never revert |
|
| 59 | * @param targetAccId Account id of the receiver |
|
| 60 | */ |
|
| 61 | 24× | function testReceiveStreamsShouldNotRevert(uint8 targetAccId) public { |
| 62 | 8× | address target = getAccount(targetAccId); |
| 63 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 64 | ||
| 65 | 5× | try |
| 66 | 92× | drips.receiveStreams(targetDripsAccId, token, type(uint32).max) |
| 67 | {} catch { |
|
| 68 | 6× | assert(false); |
| 69 | } |
|
| 70 | } |
|
| 71 | ||
| 72 | /** |
|
| 73 | * @notice If there is a receivable amount, there should be at least one |
|
| 74 | * receivable cycle |
|
| 75 | * @param targetAccId Account id of the receiver |
|
| 76 | * @param maxCycles Maximum number of cycles to receive |
|
| 77 | */ |
|
| 78 | 23× | function testReceiveStreamsViewConsistency( |
| 79 | uint8 targetAccId, |
|
| 80 | uint32 maxCycles |
|
| 81 | 4× | ) public { |
| 82 | 8× | address target = getAccount(targetAccId); |
| 83 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 84 | ||
| 85 | 11× | require(maxCycles > 0); |
| 86 | ||
| 87 | 87× | uint128 receivable = drips.receiveStreamsResult( |
| 88 | 1× | targetDripsAccId, |
| 89 | 11× | token, |
| 90 | 1× | maxCycles |
| 91 | ); |
|
| 92 | 79× | uint32 receivableCycles = drips.receivableStreamsCycles( |
| 93 | 1× | targetDripsAccId, |
| 94 | 11× | token |
| 95 | ); |
|
| 96 | ||
| 97 | 17× | if (receivable > 0) assert(receivableCycles > 0); |
| 98 | ||
| 99 | // this does not hold because you can have cycles with 0 amount receivable |
|
| 100 | // if (receivableCycles > 0) assert(receivable > 0); |
|
| 101 | } |
|
| 102 | ||
| 103 | /** |
|
| 104 | * @notice `drips.receiveStreamsResult` should match actual received amount |
|
| 105 | * @param targetAccId Account id of the receiver |
|
| 106 | * @param maxCycles Maximum number of cycles to receive |
|
| 107 | */ |
|
| 108 | 23× | function testReceiveStreamsViewVsActual(uint8 targetAccId, uint32 maxCycles) |
| 109 | public |
|
| 110 | 4× | { |
| 111 | 8× | address target = getAccount(targetAccId); |
| 112 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 113 | ||
| 114 | 87× | uint128 receivable = drips.receiveStreamsResult( |
| 115 | 1× | targetDripsAccId, |
| 116 | 11× | token, |
| 117 | 1× | maxCycles |
| 118 | ); |
|
| 119 | ||
| 120 | 88× | uint128 received = drips.receiveStreams( |
| 121 | 1× | targetDripsAccId, |
| 122 | 11× | token, |
| 123 | 1× | maxCycles |
| 124 | ); |
|
| 125 | ||
| 126 | 10× | assert(receivable == received); |
| 127 | } |
|
| 128 | ||
| 129 | /** |
|
| 130 | * @notice Test internal accounting after collecting |
|
| 131 | * @param fromAccId Account id of the collector |
|
| 132 | * @param toAccId Account id of the receiving account |
|
| 133 | */ |
|
| 134 | 31× | function testCollect(uint8 fromAccId, uint8 toAccId) public { |
| 135 | 8× | address from = getAccount(fromAccId); |
| 136 | 8× | address to = getAccount(toAccId); |
| 137 | ||
| 138 | 8× | uint256 fromDripsAccId = getDripsAccountId(from); |
| 139 | ||
| 140 | 91× | uint128 colBalBefore = drips.collectable(fromDripsAccId, token); |
| 141 | 79× | uint256 tokenBalBefore = token.balanceOf(to); |
| 142 | ||
| 143 | 9× | uint128 collected = collect(fromAccId, toAccId); |
| 144 | ||
| 145 | 91× | uint128 colBalAfter = drips.collectable(fromDripsAccId, token); |
| 146 | 79× | uint256 tokenBalAfter = token.balanceOf(to); |
| 147 | ||
| 148 | 17× | assert(colBalAfter == colBalBefore - collected); |
| 149 | 15× | assert(tokenBalAfter == tokenBalBefore + collected); |
| 150 | } |
|
| 151 | ||
| 152 | /** |
|
| 153 | * @notice Collecting should never revert |
|
| 154 | * @param fromAccId Account id of the collector |
|
| 155 | * @param toAccId Account id of the receiving account |
|
| 156 | */ |
|
| 157 | 25× | function testCollectShouldNotRevert(uint8 fromAccId, uint8 toAccId) public { |
| 158 | 8× | address from = getAccount(fromAccId); |
| 159 | 8× | address to = getAccount(toAccId); |
| 160 | ||
| 161 | 62× | hevm.prank(from); |
| 162 | 95× | try driver.collect(token, to) {} catch { |
| 163 | 0 | assert(false); |
| 164 | } |
|
| 165 | } |
|
| 166 | } |
97%
src/echidna/EchidnaInvariantTests.sol
Lines covered: 69 / 71 (97%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./EchidnaBasicHelpers.sol"; |
|
| 4 | import "./EchidnaSplitsHelpers.sol"; |
|
| 5 | import "./EchidnaStreamsHelpers.sol"; |
|
| 6 | import "./EchidnaSqueezeHelpers.sol"; |
|
| 7 | ||
| 8 | /** |
|
| 9 | * @title Mixin containing invariant tests |
|
| 10 | * @author Rappie <rappie@perimetersec.io> |
|
| 11 | */ |
|
| 12 | 0 | contract EchidnaInvariantTests is |
| 13 | EchidnaBasicHelpers, |
|
| 14 | EchidnaSplitsHelpers, |
|
| 15 | EchidnaStreamsHelpers, |
|
| 16 | EchidnaSqueezeHelpers |
|
| 17 | { |
|
| 18 | /** |
|
| 19 | * @notice Withdrawing any amount directly from Drips should fail |
|
| 20 | * @param amount Amount to withdraw |
|
| 21 | */ |
|
| 22 | 22× | function invariantWithdrawShouldAlwaysFail(uint256 amount) public { |
| 23 | 25× | require(amount > 0, "withdraw amount must be > 0"); |
| 24 | ||
| 25 | 79× | try drips.withdraw(token, address(this), amount) { |
| 26 | 0 | assert(false); |
| 27 | } catch {} |
|
| 28 | } |
|
| 29 | ||
| 30 | /** |
|
| 31 | * @notice `amtPerSec` should never be lower than `drips.minAmtPerSec()` |
|
| 32 | * @param targetAccId Account id of the receiver |
|
| 33 | * @param index Index of the receiver |
|
| 34 | */ |
|
| 35 | 23× | function invariantAmtPerSecVsMinAmtPerSec(uint8 targetAccId, uint256 index) |
| 36 | public |
|
| 37 | 3× | { |
| 38 | 8× | address target = getAccount(targetAccId); |
| 39 | ||
| 40 | 8× | StreamReceiver[] memory receivers = getStreamReceivers(target); |
| 41 | 26× | require(receivers.length > 0, "no receivers"); |
| 42 | ||
| 43 | 11× | index = index % receivers.length; |
| 44 | 25× | uint160 amtPerSec = receivers[index].config.amtPerSec(); |
| 45 | ||
| 46 | 79× | assert(amtPerSec >= drips.minAmtPerSec()); |
| 47 | } |
|
| 48 | ||
| 49 | /** |
|
| 50 | * @notice The total of all internal balances should match token balance |
|
| 51 | * of the Drips contract |
|
| 52 | */ |
|
| 53 | 10× | function invariantAccountingVsTokenBalance() public { |
| 54 | 89× | uint256 tokenBalance = token.balanceOf(address(drips)); |
| 55 | 7× | uint256 dripsBalancesTotal = getDripsBalancesTotalForAllUsers(); |
| 56 | ||
| 57 | 6× | assert(tokenBalance == dripsBalancesTotal); |
| 58 | } |
|
| 59 | ||
| 60 | /** |
|
| 61 | * @notice Check internal and external balances after withdrawing all funds |
|
| 62 | * from the system |
|
| 63 | */ |
|
| 64 | 20× | function invariantWithdrawAllTokens() external heavy { |
| 65 | // remove all splits to prevent tokens from getting stuck in case |
|
| 66 | // there are splits to self |
|
| 67 | 62× | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 68 | 61× | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 69 | 31× | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 70 | 31× | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 71 | ||
| 72 | 31× | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 73 | 31× | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 74 | 31× | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 75 | 31× | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 76 | ||
| 77 | 4× | receiveStreamsSplitAndCollectToSelf( |
| 78 | 27× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] |
| 79 | ); |
|
| 80 | 4× | receiveStreamsSplitAndCollectToSelf( |
| 81 | 27× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] |
| 82 | ); |
|
| 83 | 4× | receiveStreamsSplitAndCollectToSelf( |
| 84 | 27× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] |
| 85 | ); |
|
| 86 | 4× | receiveStreamsSplitAndCollectToSelf( |
| 87 | 27× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3] |
| 88 | ); |
|
| 89 | ||
| 90 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 91 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 92 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 93 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 94 | ||
| 95 | 89× | uint256 dripsBalance = token.balanceOf(address(drips)); |
| 96 | 78× | uint256 user0Balance = token.balanceOf(ADDRESS_USER0); |
| 97 | 78× | uint256 user1Balance = token.balanceOf(ADDRESS_USER1); |
| 98 | 78× | uint256 user2Balance = token.balanceOf(ADDRESS_USER2); |
| 99 | 78× | uint256 user3Balance = token.balanceOf(ADDRESS_USER3); |
| 100 | ||
| 101 | 22× | uint256 totalUserBalance = user0Balance + |
| 102 | 1× | user1Balance + |
| 103 | 1× | user2Balance + |
| 104 | 1× | user3Balance; |
| 105 | ||
| 106 | 6× | assert(dripsBalance == 0); |
| 107 | 12× | assert(totalUserBalance == STARTING_BALANCE * 4); |
| 108 | } |
|
| 109 | ||
| 110 | /** |
|
| 111 | * @notice Withdrawing all funds from the system should never revert |
|
| 112 | */ |
|
| 113 | 9× | function invariantWithdrawAllTokensShouldNotRevert() public heavy { |
| 114 | 4× | try |
| 115 | 46× | EchidnaInvariantTests(address(this)).invariantWithdrawAllTokens() |
| 116 | {} catch { |
|
| 117 | 6× | assert(false); |
| 118 | } |
|
| 119 | } |
|
| 120 | ||
| 121 | /** |
|
| 122 | * @notice The sum of all `amtDelta`s for an account should be zero |
|
| 123 | * @param targetAccId Target account to perform the test on |
|
| 124 | */ |
|
| 125 | 29× | function invariantSumAmtDeltaIsZero(uint8 targetAccId) public heavy { |
| 126 | 8× | address target = getAccount(targetAccId); |
| 127 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 128 | ||
| 129 | 7× | uint32 maxEnd = getMaxEndForAllUsers(); |
| 130 | ||
| 131 | 9× | uint32 firstCycle = getCycleFromTimestamp(STARTING_TIMESTAMP); |
| 132 | 10× | uint32 lastCycle = getCycleFromTimestamp(maxEnd); |
| 133 | ||
| 134 | 27× | require(maxEnd > 0, "no cycles"); |
| 135 | 29× | require(firstCycle != lastCycle, "only one cycle"); |
| 136 | ||
| 137 | // limit amount of cycles for gas & memory savings |
|
| 138 | 34× | require(lastCycle - firstCycle < 1000, "too many cycles"); |
| 139 | ||
| 140 | 1× | int256 sumAmtDelta = 0; |
| 141 | ||
| 142 | 30× | for (uint32 cycle = firstCycle; cycle <= lastCycle; cycle++) { |
| 143 | 83× | (int128 thisCycle, int128 nextCycle) = drips.getAmtDeltaForCycle( |
| 144 | 1× | targetDripsAccId, |
| 145 | 11× | token, |
| 146 | 1× | cycle |
| 147 | ); |
|
| 148 | ||
| 149 | 12× | sumAmtDelta += int256(thisCycle); |
| 150 | 12× | sumAmtDelta += int256(nextCycle); |
| 151 | } |
|
| 152 | ||
| 153 | 6× | assert(sumAmtDelta == 0); |
| 154 | } |
|
| 155 | } |
98%
src/echidna/EchidnaSplitsHelpers.sol
Lines covered: 54 / 55 (98%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./base/EchidnaBase.sol"; |
|
| 4 | ||
| 5 | /** |
|
| 6 | * @title Mixin containing helpers for splitting |
|
| 7 | * @author Rappie <rappie@perimetersec.io> |
|
| 8 | */ |
|
| 9 | 0 | contract EchidnaSplitsHelpers is EchidnaBase { |
| 10 | /** |
|
| 11 | * @notice Internal helper function to set splits receivers |
|
| 12 | * @param senderAccId Account id of the sender |
|
| 13 | * @param unsortedReceivers Receivers list to set |
|
| 14 | * @dev This function also sorts the receivers list |
|
| 15 | */ |
|
| 16 | 9× | function _setSplits( |
| 17 | uint8 senderAccId, |
|
| 18 | SplitsReceiver[] memory unsortedReceivers |
|
| 19 | 4× | ) internal { |
| 20 | 24× | address sender = getAccount(senderAccId); |
| 21 | ||
| 22 | 21× | SplitsReceiver[] memory newReceivers = bubbleSortSplitsReceivers( |
| 23 | 3× | unsortedReceivers |
| 24 | ); |
|
| 25 | ||
| 26 | 18× | updateSplitsReceivers(sender, newReceivers); |
| 27 | ||
| 28 | 176× | hevm.prank(sender); |
| 29 | 131× | driver.setSplits(newReceivers); |
| 30 | } |
|
| 31 | ||
| 32 | /** |
|
| 33 | * @notice Set splits, overwriting the current receivers list |
|
| 34 | * @param senderAccId Account id of the sender |
|
| 35 | * @param receiverAccId Account id of the receiver in the receivers list |
|
| 36 | * @param weight Weight of the receiver |
|
| 37 | */ |
|
| 38 | 29× | function setSplits( |
| 39 | uint8 senderAccId, |
|
| 40 | uint8 receiverAccId, |
|
| 41 | uint32 weight |
|
| 42 | 8× | ) public { |
| 43 | 16× | address sender = getAccount(senderAccId); |
| 44 | 16× | address receiver = getAccount(receiverAccId); |
| 45 | 16× | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 46 | ||
| 47 | 108× | SplitsReceiver[] memory receivers = new SplitsReceiver[](1); |
| 48 | 70× | receivers[0] = SplitsReceiver({ |
| 49 | 2× | accountId: receiverDripsAccId, |
| 50 | 2× | weight: weight |
| 51 | }); |
|
| 52 | 12× | updateSplitsReceivers(sender, receivers); |
| 53 | ||
| 54 | 12× | _setSplits(senderAccId, receivers); |
| 55 | } |
|
| 56 | ||
| 57 | /** |
|
| 58 | * @notice Set splits, overwriting the current receivers list |
|
| 59 | * @param senderAccId Account id of the sender |
|
| 60 | * @param receiverAccId Account id of the receiver in the receivers list |
|
| 61 | * @param weight Weight of the receiver |
|
| 62 | * @dev This function clamps the weight between the minimum and maximum |
|
| 63 | * allowed values |
|
| 64 | */ |
|
| 65 | 48× | function setSplitsWithClamping( |
| 66 | uint8 senderAccId, |
|
| 67 | uint8 receiverAccId, |
|
| 68 | uint32 weight |
|
| 69 | ) public { |
|
| 70 | 16× | weight = clampSplitWeight(weight, 0); // there are no existing weights |
| 71 | 14× | setSplits(senderAccId, receiverAccId, weight); |
| 72 | } |
|
| 73 | ||
| 74 | /** |
|
| 75 | * @notice Add a splits receiver to the existing list of receivers |
|
| 76 | * @param senderAccId Account id of the sender |
|
| 77 | * @param receiverAccId Account id of the receiver to add |
|
| 78 | * @param weight Weight of the receiver |
|
| 79 | */ |
|
| 80 | 29× | function addSplitsReceiver( |
| 81 | uint8 senderAccId, |
|
| 82 | uint8 receiverAccId, |
|
| 83 | uint32 weight |
|
| 84 | 14× | ) public { |
| 85 | 16× | address sender = getAccount(senderAccId); |
| 86 | 16× | address receiver = getAccount(receiverAccId); |
| 87 | 16× | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 88 | 16× | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 89 | ||
| 90 | 16× | SplitsReceiver[] memory oldReceivers = getSplitsReceivers(sender); |
| 91 | ||
| 92 | 40× | SplitsReceiver memory addedReceiver = SplitsReceiver({ |
| 93 | 2× | accountId: receiverDripsAccId, |
| 94 | 2× | weight: weight |
| 95 | }); |
|
| 96 | ||
| 97 | 106× | SplitsReceiver[] memory newReceivers = new SplitsReceiver[]( |
| 98 | 18× | oldReceivers.length + 1 |
| 99 | ); |
|
| 100 | 46× | for (uint256 i = 0; i < oldReceivers.length; i++) { |
| 101 | 66× | newReceivers[i] = oldReceivers[i]; |
| 102 | } |
|
| 103 | 54× | newReceivers[newReceivers.length - 1] = addedReceiver; |
| 104 | ||
| 105 | 12× | _setSplits(senderAccId, newReceivers); |
| 106 | } |
|
| 107 | ||
| 108 | /** |
|
| 109 | * @notice Add a splits receiver to the existing list of receivers |
|
| 110 | * @param senderAccId Account id of the sender |
|
| 111 | * @param receiverAccId Account id of the receiver to add |
|
| 112 | * @param weight Weight of the receiver |
|
| 113 | * @dev This function clamps the weight between the minimum and maximum |
|
| 114 | * allowed values |
|
| 115 | */ |
|
| 116 | 50× | function addSplitsReceiverWithClamping( |
| 117 | uint8 senderAccId, |
|
| 118 | uint8 receiverAccId, |
|
| 119 | uint32 weight |
|
| 120 | 6× | ) public { |
| 121 | 16× | address sender = getAccount(senderAccId); |
| 122 | ||
| 123 | // sum all the existing weights |
|
| 124 | 2× | uint32 existingWeights; |
| 125 | 16× | SplitsReceiver[] memory receivers = getSplitsReceivers(sender); |
| 126 | 46× | for (uint256 i = 0; i < receivers.length; i++) { |
| 127 | 54× | existingWeights += receivers[i].weight; |
| 128 | } |
|
| 129 | ||
| 130 | // we can't add a receiver if it makes the total weight go over the |
|
| 131 | // maximum allowed |
|
| 132 | 156× | if (existingWeights >= drips.TOTAL_SPLITS_WEIGHT()) return; |
| 133 | ||
| 134 | 16× | weight = clampSplitWeight(weight, existingWeights); |
| 135 | ||
| 136 | 14× | addSplitsReceiver(senderAccId, receiverAccId, weight); |
| 137 | } |
|
| 138 | ||
| 139 | /** |
|
| 140 | * @notice Remove any existing splits |
|
| 141 | * @param targetAccId Target account id |
|
| 142 | */ |
|
| 143 | 28× | function removeAllSplits(uint8 targetAccId) public { |
| 144 | 96× | SplitsReceiver[] memory receivers = new SplitsReceiver[](0); |
| 145 | 17× | _setSplits(targetAccId, receivers); |
| 146 | } |
|
| 147 | ||
| 148 | /** |
|
| 149 | * @notice Clamp the weight between the minimum and maximum allowed values |
|
| 150 | * @param weight Weight to clamp |
|
| 151 | * @param existingWeights Sum of all the existing weights |
|
| 152 | * @return Clamped weight |
|
| 153 | */ |
|
| 154 | 12× | function clampSplitWeight(uint32 weight, uint32 existingWeights) |
| 155 | public |
|
| 156 | view |
|
| 157 | 2× | returns (uint32) |
| 158 | { |
|
| 159 | 184× | return (weight % (drips.TOTAL_SPLITS_WEIGHT() - existingWeights)) + 1; |
| 160 | } |
|
| 161 | } |
96%
src/echidna/EchidnaSplitsTests.sol
Lines covered: 99 / 103 (96%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./base/EchidnaBase.sol"; |
|
| 4 | import "./EchidnaBasicHelpers.sol"; |
|
| 5 | import "./EchidnaSplitsHelpers.sol"; |
|
| 6 | ||
| 7 | /** |
|
| 8 | * @title Mixin containing tests for splitting |
|
| 9 | * @author Rappie <rappie@perimetersec.io> |
|
| 10 | */ |
|
| 11 | 0 | contract EchidnaSplitsTests is |
| 12 | EchidnaBase, |
|
| 13 | EchidnaBasicHelpers, |
|
| 14 | EchidnaSplitsHelpers |
|
| 15 | { |
|
| 16 | /** |
|
| 17 | * @notice Test internal accounting for splittable amount after splitting |
|
| 18 | * @param targetAccId Account id execute split on |
|
| 19 | */ |
|
| 20 | 31× | function testSplittableAfterSplit(uint8 targetAccId) public { |
| 21 | 8× | address target = getAccount(targetAccId); |
| 22 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 23 | ||
| 24 | 91× | uint128 splittableBefore = drips.splittable(targetDripsAccId, token); |
| 25 | ||
| 26 | // check if we are splitting to ourselves |
|
| 27 | 1× | uint32 splitToSelfWeight; |
| 28 | 8× | SplitsReceiver[] memory receivers = getSplitsReceivers(target); |
| 29 | 23× | for (uint256 i = 0; i < receivers.length; i++) { |
| 30 | 23× | if (receivers[i].accountId == targetDripsAccId) { |
| 31 | 27× | splitToSelfWeight += receivers[i].weight; |
| 32 | } |
|
| 33 | } |
|
| 34 | ||
| 35 | // calculate amount to split to self |
|
| 36 | 3× | uint128 splitToSelfAmount = uint128( |
| 37 | 87× | (splittableBefore * splitToSelfWeight) / drips.TOTAL_SPLITS_WEIGHT() |
| 38 | ); |
|
| 39 | ||
| 40 | 11× | (uint128 collectableAmt, uint128 splitAmt) = split(targetAccId); |
| 41 | ||
| 42 | 91× | uint128 splittableAfter = drips.splittable(targetDripsAccId, token); |
| 43 | ||
| 44 | // sanity check |
|
| 45 | 18× | assert((splitAmt + collectableAmt) <= splittableBefore); |
| 46 | ||
| 47 | 11× | if (splitToSelfWeight == 0) { |
| 48 | // if we're not splitting to ourselves, things are simple |
|
| 49 | 3× | assert( |
| 50 | 21× | splittableAfter == splittableBefore - splitAmt - collectableAmt |
| 51 | ); |
|
| 52 | 2× | } else { |
| 53 | // if we ARE splitting to ourselves, there are rounding errors |
|
| 54 | // to take into account. |
|
| 55 | ||
| 56 | // calculate expected amount after the split |
|
| 57 | 22× | uint128 expectedSplittableAfter = splittableBefore - |
| 58 | 1× | splitAmt - |
| 59 | 1× | collectableAmt + |
| 60 | 1× | splitToSelfAmount; |
| 61 | ||
| 62 | // calculate difference between expected and actual |
|
| 63 | 12× | int256 difference = int256(uint256(splittableAfter)) - |
| 64 | 3× | int256(uint256(expectedSplittableAfter)); |
| 65 | ||
| 66 | // check if difference is within tolerance |
|
| 67 | 3× | assert( |
| 68 | 14× | difference >= -int256(SPLIT_ROUNDING_TOLERANCE) && |
| 69 | 3× | difference <= int256(SPLIT_ROUNDING_TOLERANCE) |
| 70 | ); |
|
| 71 | } |
|
| 72 | } |
|
| 73 | ||
| 74 | /** |
|
| 75 | * @notice Test internal accounting for collectable amount after splitting |
|
| 76 | * @param targetAccId Account id execute split on |
|
| 77 | */ |
|
| 78 | 27× | function testCollectableAfterSplit(uint8 targetAccId) public { |
| 79 | 8× | address target = getAccount(targetAccId); |
| 80 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 81 | ||
| 82 | 91× | uint128 colBalBefore = drips.collectable(targetDripsAccId, token); |
| 83 | 9× | (uint128 collectableAmt, ) = split(targetAccId); |
| 84 | 91× | uint128 colBalAfter = drips.collectable(targetDripsAccId, token); |
| 85 | ||
| 86 | 17× | assert(colBalAfter == colBalBefore + collectableAmt); |
| 87 | } |
|
| 88 | ||
| 89 | /** |
|
| 90 | * @notice After splitting, all receivers should have their splittable |
|
| 91 | * amount increased by the amount they were supposed to receive |
|
| 92 | * @param targetAccId Account id execute split on |
|
| 93 | */ |
|
| 94 | 32× | function testReceiversReceivedSplit(uint8 targetAccId) public { |
| 95 | 8× | address target = getAccount(targetAccId); |
| 96 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 97 | ||
| 98 | 91× | uint128 amountToBeSplit = drips.splittable(targetDripsAccId, token); |
| 99 | 8× | SplitsReceiver[] memory receivers = getSplitsReceivers(target); |
| 100 | ||
| 101 | // storage for all receivers |
|
| 102 | 51× | uint128[] memory splittableBefore = new uint128[](receivers.length); |
| 103 | 51× | uint128[] memory splittableAfter = new uint128[](receivers.length); |
| 104 | 51× | uint32[] memory weights = new uint32[](receivers.length); |
| 105 | 51× | uint128[] memory amounts = new uint128[](receivers.length); |
| 106 | ||
| 107 | 23× | for (uint256 i = 0; i < receivers.length; i++) { |
| 108 | // store splittable before |
|
| 109 | 101× | splittableBefore[i] = drips.splittable( |
| 110 | 18× | receivers[i].accountId, |
| 111 | 11× | token |
| 112 | ); |
|
| 113 | ||
| 114 | // calculate amount the receiver should get |
|
| 115 | 43× | weights[i] = receivers[i].weight; |
| 116 | 25× | amounts[i] = uint128( |
| 117 | 101× | (amountToBeSplit * weights[i]) / drips.TOTAL_SPLITS_WEIGHT() |
| 118 | ); |
|
| 119 | } |
|
| 120 | ||
| 121 | // split |
|
| 122 | 11× | (uint128 collectableAmt, uint128 splitAmt) = split(targetAccId); |
| 123 | ||
| 124 | // store splittable after |
|
| 125 | 23× | for (uint256 i = 0; i < receivers.length; i++) { |
| 126 | 101× | splittableAfter[i] = drips.splittable( |
| 127 | 18× | receivers[i].accountId, |
| 128 | 11× | token |
| 129 | ); |
|
| 130 | } |
|
| 131 | ||
| 132 | 25× | for (uint256 i = 0; i < receivers.length; i++) { |
| 133 | // calculate expected amount after the split |
|
| 134 | 1× | uint128 expectedAfter; |
| 135 | 26× | if (receivers[i].accountId != targetDripsAccId) { |
| 136 | // splitting so someone else is trivial |
|
| 137 | 38× | expectedAfter = splittableBefore[i] + amounts[i]; |
| 138 | } else { |
|
| 139 | // splitting to self needs to take into account that the splittable |
|
| 140 | // amount before contains the actual amount that was split to all |
|
| 141 | // the receivers. we should end up with only the amount that was |
|
| 142 | // split to ourselves |
|
| 143 | 2× | expectedAfter = |
| 144 | 27× | splittableBefore[i] - |
| 145 | 1× | amountToBeSplit + |
| 146 | 15× | amounts[i]; |
| 147 | } |
|
| 148 | ||
| 149 | // calculate difference between expected and actual |
|
| 150 | 26× | int256 difference = int256(uint256(splittableAfter[i])) - |
| 151 | 3× | int256(uint256(expectedAfter)); |
| 152 | ||
| 153 | // check if difference is within tolerance |
|
| 154 | 3× | assert( |
| 155 | 14× | difference >= -int256(SPLIT_ROUNDING_TOLERANCE) && |
| 156 | 3× | difference <= int256(SPLIT_ROUNDING_TOLERANCE) |
| 157 | ); |
|
| 158 | } |
|
| 159 | } |
|
| 160 | ||
| 161 | /** |
|
| 162 | * @notice `drips.splitResult` should match actual split amount |
|
| 163 | * @param targetAccId Account id execute split on |
|
| 164 | */ |
|
| 165 | 29× | function testSplitViewVsActual(uint8 targetAccId) public { |
| 166 | 8× | address target = getAccount(targetAccId); |
| 167 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 168 | ||
| 169 | 91× | uint128 splittable = drips.splittable(targetDripsAccId, token); |
| 170 | ||
| 171 | 83× | (uint128 collectableAmtView, uint128 splitAmtView) = drips.splitResult( |
| 172 | 1× | targetDripsAccId, |
| 173 | 5× | getSplitsReceivers(target), |
| 174 | 1× | splittable |
| 175 | ); |
|
| 176 | ||
| 177 | 10× | (uint128 collectableAmtActual, uint128 splitAmtActual) = split( |
| 178 | 1× | targetAccId |
| 179 | ); |
|
| 180 | ||
| 181 | 10× | assert(collectableAmtView == collectableAmtActual); |
| 182 | 10× | assert(splitAmtView == splitAmtActual); |
| 183 | } |
|
| 184 | ||
| 185 | /** |
|
| 186 | * @notice Splitting should never revert |
|
| 187 | * @param targetAccId Account id execute split on |
|
| 188 | */ |
|
| 189 | 24× | function testSplitShouldNotRevert(uint8 targetAccId) public { |
| 190 | 8× | address target = getAccount(targetAccId); |
| 191 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 192 | ||
| 193 | 74× | try EchidnaBasicHelpers(address(this)).split(targetAccId) {} catch { |
| 194 | 0 | assert(false); |
| 195 | } |
|
| 196 | } |
|
| 197 | ||
| 198 | /** |
|
| 199 | * @notice Setting splits with sane defaults should not revert |
|
| 200 | * @param senderAccId Account id of the sender |
|
| 201 | * @param receiverAccId Account id of the receiver in the receivers list |
|
| 202 | * @param weight Weight of the receiver |
|
| 203 | */ |
|
| 204 | 24× | function testSetSplitsShouldNotRevert( |
| 205 | uint8 senderAccId, |
|
| 206 | uint8 receiverAccId, |
|
| 207 | uint32 weight |
|
| 208 | ) public { |
|
| 209 | 4× | try |
| 210 | 54× | EchidnaSplitsHelpers(address(this)).setSplitsWithClamping( |
| 211 | 1× | senderAccId, |
| 212 | 1× | receiverAccId, |
| 213 | 1× | weight |
| 214 | ) |
|
| 215 | {} catch { |
|
| 216 | 0 | assert(false); |
| 217 | } |
|
| 218 | } |
|
| 219 | ||
| 220 | /** |
|
| 221 | * @notice Adding splits with sane defaults should not revert |
|
| 222 | * @param senderAccId Account id of the sender |
|
| 223 | * @param receiverAccId Account id of the receiver in the receivers list |
|
| 224 | * @param weight Weight of the receiver |
|
| 225 | */ |
|
| 226 | 30× | function testAddSplitsShouldNotRevert( |
| 227 | uint8 senderAccId, |
|
| 228 | uint8 receiverAccId, |
|
| 229 | uint32 weight |
|
| 230 | ) public { |
|
| 231 | 43× | try |
| 232 | 63× | EchidnaSplitsHelpers(address(this)).addSplitsReceiverWithClamping( |
| 233 | 1× | senderAccId, |
| 234 | 1× | receiverAccId, |
| 235 | 1× | weight |
| 236 | ) |
|
| 237 | 2× | {} catch (bytes memory reason) { |
| 238 | 9× | bytes4 errorSelector = bytes4(reason); |
| 239 | 15× | if (errorSelector == EchidnaStorage.DuplicateError.selector) { |
| 240 | // ignore this case, it means we tried to add a duplicate stream |
|
| 241 | } else { |
|
| 242 | 0 | assert(false); |
| 243 | } |
|
| 244 | } |
|
| 245 | } |
|
| 246 | } |
98%
src/echidna/EchidnaSqueezeHelpers.sol
Lines covered: 75 / 76 (98%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./base/EchidnaBase.sol"; |
|
| 4 | import "./EchidnaBasicHelpers.sol"; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @title Mixin containing helpers for squeezing |
|
| 8 | * @author Rappie <rappie@perimetersec.io> |
|
| 9 | */ |
|
| 10 | 0 | contract EchidnaSqueezeHelpers is EchidnaBase, EchidnaBasicHelpers { |
| 11 | /** |
|
| 12 | * @notice Internal helper function to squeeze streams |
|
| 13 | * @param receiverAccId Account id of the receiver |
|
| 14 | * @param senderAccId Account id of the sender |
|
| 15 | * @param historyHash Hash of the streams history |
|
| 16 | * @param history Streams history array |
|
| 17 | * @return Amount squeezed |
|
| 18 | */ |
|
| 19 | 24× | function _squeeze( |
| 20 | uint8 receiverAccId, |
|
| 21 | uint8 senderAccId, |
|
| 22 | bytes32 historyHash, |
|
| 23 | StreamsHistory[] memory history |
|
| 24 | 3× | ) internal returns (uint128) { |
| 25 | 24× | address receiver = getAccount(receiverAccId); |
| 26 | 24× | address sender = getAccount(senderAccId); |
| 27 | 24× | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 28 | 24× | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 29 | ||
| 30 | 249× | uint128 amount = drips.squeezeStreams( |
| 31 | 3× | receiverDripsAccId, |
| 32 | 33× | token, |
| 33 | 3× | senderDripsAccId, |
| 34 | 3× | historyHash, |
| 35 | 3× | history |
| 36 | ); |
|
| 37 | ||
| 38 | 24× | return amount; |
| 39 | } |
|
| 40 | ||
| 41 | /** |
|
| 42 | * @notice Squeeze streams with default history (all StreamHistory entries) |
|
| 43 | * @param receiverAccId Account id of the receiver |
|
| 44 | * @param senderAccId Account id of the sender |
|
| 45 | * @return Amount squeezed |
|
| 46 | */ |
|
| 47 | 84× | function squeezeWithDefaultHistory(uint8 receiverAccId, uint8 senderAccId) |
| 48 | public |
|
| 49 | 3× | returns (uint128) |
| 50 | { |
|
| 51 | 6× | return |
| 52 | 12× | _squeeze( |
| 53 | 3× | receiverAccId, |
| 54 | 3× | senderAccId, |
| 55 | 9× | bytes32(0), |
| 56 | 27× | getStreamsHistory(getAccount(senderAccId)) |
| 57 | ); |
|
| 58 | } |
|
| 59 | ||
| 60 | /** |
|
| 61 | * @notice Squeeze streams with a fuzzed history |
|
| 62 | * @param receiverAccId Account id of the receiver |
|
| 63 | * @param senderAccId Account id of the sender |
|
| 64 | * @param hashIndex Index of the history hash to use |
|
| 65 | * @param receiversRandomSeed Random seed used for fuzzing the history |
|
| 66 | * @return Amount squeezed |
|
| 67 | * @dev This function will use the seed to make random changes to the history. |
|
| 68 | * These include changing the starting point of the history, and hashing |
|
| 69 | * certain history entries to leave them out of the squeeze. |
|
| 70 | */ |
|
| 71 | 82× | function squeezeWithFuzzedHistory( |
| 72 | uint8 receiverAccId, |
|
| 73 | uint8 senderAccId, |
|
| 74 | uint256 hashIndex, |
|
| 75 | bytes32 receiversRandomSeed |
|
| 76 | 2× | ) public returns (uint128) { |
| 77 | 16× | address receiver = getAccount(receiverAccId); |
| 78 | 16× | address sender = getAccount(senderAccId); |
| 79 | 16× | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 80 | 16× | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 81 | ||
| 82 | 8× | ( |
| 83 | 2× | bytes32 historyHash, |
| 84 | 2× | StreamsHistory[] memory history |
| 85 | 8× | ) = getFuzzedStreamsHistory( |
| 86 | 2× | senderAccId, |
| 87 | 2× | hashIndex, |
| 88 | 2× | receiversRandomSeed |
| 89 | ); |
|
| 90 | ||
| 91 | 32× | return _squeeze(receiverAccId, senderAccId, historyHash, history); |
| 92 | } |
|
| 93 | ||
| 94 | /** |
|
| 95 | * @notice Squeeze streams sent to self |
|
| 96 | * @param targetAccId Account id of the sender |
|
| 97 | */ |
|
| 98 | 22× | function squeezeToSelf(uint8 targetAccId) public { |
| 99 | 7× | squeezeWithDefaultHistory(targetAccId, targetAccId); |
| 100 | } |
|
| 101 | ||
| 102 | /** |
|
| 103 | * @notice Squeeze streams from all possible senders to target |
|
| 104 | * @param targetAccId Account id of the receiver |
|
| 105 | * @dev This can be used to test extracting all value from the system |
|
| 106 | */ |
|
| 107 | 25× | function squeezeAllSenders(uint8 targetAccId) public { |
| 108 | 10× | squeezeWithDefaultHistory( |
| 109 | 2× | targetAccId, |
| 110 | 54× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] |
| 111 | ); |
|
| 112 | 10× | squeezeWithDefaultHistory( |
| 113 | 2× | targetAccId, |
| 114 | 54× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] |
| 115 | ); |
|
| 116 | 10× | squeezeWithDefaultHistory( |
| 117 | 2× | targetAccId, |
| 118 | 54× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] |
| 119 | ); |
|
| 120 | 10× | squeezeWithDefaultHistory( |
| 121 | 2× | targetAccId, |
| 122 | 54× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3] |
| 123 | ); |
|
| 124 | } |
|
| 125 | ||
| 126 | /** |
|
| 127 | * @notice Squeeze all senders, receive streams, split and collect funds to self |
|
| 128 | * @param targetAccId Target account |
|
| 129 | * @dev Extra helper that narrows the search space for the fuzzer |
|
| 130 | */ |
|
| 131 | 22× | function squeezeAllAndReceiveAndSplitAndCollectToSelf(uint8 targetAccId) |
| 132 | public |
|
| 133 | { |
|
| 134 | 5× | squeezeAllSenders(targetAccId); |
| 135 | 5× | receiveStreamsSplitAndCollectToSelf(targetAccId); |
| 136 | } |
|
| 137 | ||
| 138 | /** |
|
| 139 | * @notice Helper to create a fuzzed version of a sender's streams history |
|
| 140 | * @param targetAccId Account id of the sender |
|
| 141 | * @param hashIndex Index of the history entry to be used as the starting point |
|
| 142 | * @param receiversRandomSeed Random seed used to determine which history entries |
|
| 143 | * to leave out of the squeeze (by hashing them) |
|
| 144 | * @return Hash of the history, and the fuzzed streams history array |
|
| 145 | */ |
|
| 146 | 16× | function getFuzzedStreamsHistory( |
| 147 | uint8 targetAccId, |
|
| 148 | uint256 hashIndex, |
|
| 149 | bytes32 receiversRandomSeed |
|
| 150 | 4× | ) internal returns (bytes32, StreamsHistory[] memory) { |
| 151 | 16× | address target = getAccount(targetAccId); |
| 152 | ||
| 153 | // get the history structs and hashes |
|
| 154 | 16× | StreamsHistory[] memory historyStructs = getStreamsHistory(target); |
| 155 | 16× | bytes32[] memory historyHashes = getStreamsHistoryHashes(target); |
| 156 | ||
| 157 | // having a hashed history requires at least 2 history entries |
|
| 158 | 35× | require(historyStructs.length >= 2, "need at least 2 history entries"); |
| 159 | ||
| 160 | // hashIndex must be within bounds and cant be the last entry |
|
| 161 | 36× | hashIndex = hashIndex % (historyHashes.length - 1); |
| 162 | ||
| 163 | // get the history hash at the index |
|
| 164 | 36× | bytes32 historyHash = historyHashes[hashIndex]; |
| 165 | ||
| 166 | // create a history array with all entries after the hashIndex |
|
| 167 | 106× | StreamsHistory[] memory history = new StreamsHistory[]( |
| 168 | 32× | historyStructs.length - 1 - hashIndex |
| 169 | ); |
|
| 170 | 66× | for (uint256 i = hashIndex + 1; i < historyStructs.length; i++) { |
| 171 | 94× | history[i - hashIndex - 1] = historyStructs[i]; |
| 172 | } |
|
| 173 | ||
| 174 | // hash receivers based on 'receiversRandomSeed' |
|
| 175 | 48× | for (uint256 i = 0; i < history.length; i++) { |
| 176 | 62× | receiversRandomSeed = keccak256(bytes.concat(receiversRandomSeed)); |
| 177 | 42× | bool hashBool = (uint256(receiversRandomSeed) % 2) == 0 |
| 178 | 2× | ? false |
| 179 | 2× | : true; |
| 180 | ||
| 181 | 10× | if (hashBool) { |
| 182 | 194× | history[i].streamsHash = drips.hashStreams( |
| 183 | 36× | history[i].receivers |
| 184 | ); |
|
| 185 | 100× | history[i].receivers = new StreamReceiver[](0); |
| 186 | } |
|
| 187 | } |
|
| 188 | ||
| 189 | 22× | return (historyHash, history); |
| 190 | } |
|
| 191 | } |
97%
src/echidna/EchidnaSqueezeTests.sol
Lines covered: 110 / 113 (97%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./EchidnaBasicHelpers.sol"; |
|
| 4 | import "./EchidnaSplitsHelpers.sol"; |
|
| 5 | import "./EchidnaStreamsHelpers.sol"; |
|
| 6 | import "./EchidnaSqueezeHelpers.sol"; |
|
| 7 | ||
| 8 | /** |
|
| 9 | * @title Mixin containing tests for squeezing |
|
| 10 | * @author Rappie <rappie@perimetersec.io> |
|
| 11 | */ |
|
| 12 | 0 | contract EchidnaSqueezeTests is |
| 13 | EchidnaBasicHelpers, |
|
| 14 | EchidnaSplitsHelpers, |
|
| 15 | EchidnaStreamsHelpers, |
|
| 16 | EchidnaSqueezeHelpers |
|
| 17 | { |
|
| 18 | /** |
|
| 19 | * @notice Test internal accounting after squeezing |
|
| 20 | * @param receiverAccId Account id of the receiver |
|
| 21 | * @param senderAccId Account id of the sender |
|
| 22 | */ |
|
| 23 | 31× | function testSqueeze(uint8 receiverAccId, uint8 senderAccId) public { |
| 24 | 8× | address receiver = getAccount(receiverAccId); |
| 25 | 8× | address sender = getAccount(senderAccId); |
| 26 | ||
| 27 | 8× | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 28 | ||
| 29 | 9× | uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
| 30 | 91× | uint128 splittableBefore = drips.splittable(receiverDripsAccId, token); |
| 31 | ||
| 32 | 7× | uint128 squeezedAmt = squeezeWithDefaultHistory( |
| 33 | 1× | receiverAccId, |
| 34 | 1× | senderAccId |
| 35 | ); |
|
| 36 | ||
| 37 | 9× | uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
| 38 | 91× | uint128 splittableAfter = drips.splittable(receiverDripsAccId, token); |
| 39 | ||
| 40 | 17× | assert(squeezableAfter == squeezableBefore - squeezedAmt); |
| 41 | 17× | assert(splittableAfter == splittableBefore + squeezedAmt); |
| 42 | ||
| 43 | 12× | if (squeezedAmt > 0) { |
| 44 | 10× | assert(squeezableAfter < squeezableBefore); |
| 45 | 10× | assert(splittableAfter > splittableBefore); |
| 46 | } else { |
|
| 47 | 10× | assert(squeezableAfter == squeezableBefore); |
| 48 | 10× | assert(splittableAfter == splittableBefore); |
| 49 | } |
|
| 50 | } |
|
| 51 | ||
| 52 | /** |
|
| 53 | * @notice `drips.squeezeStreamsResult` should match actual squeezed amount |
|
| 54 | * @param receiverAccId Account id of the receiver |
|
| 55 | * @param senderAccId Account id of the sender |
|
| 56 | */ |
|
| 57 | 23× | function testSqueezeViewVsActual(uint8 receiverAccId, uint8 senderAccId) |
| 58 | public |
|
| 59 | 4× | { |
| 60 | 8× | address receiver = getAccount(receiverAccId); |
| 61 | 8× | address sender = getAccount(senderAccId); |
| 62 | ||
| 63 | 9× | uint128 squeezable = getSqueezableAmount(sender, receiver); |
| 64 | 7× | uint128 squeezed = squeezeWithDefaultHistory( |
| 65 | 1× | receiverAccId, |
| 66 | 1× | senderAccId |
| 67 | ); |
|
| 68 | ||
| 69 | 10× | assert(squeezable == squeezed); |
| 70 | } |
|
| 71 | ||
| 72 | /** |
|
| 73 | * @notice Squeezable amount should be equal to receivable amount in the future |
|
| 74 | * @param targetAccId Account id of the receiver |
|
| 75 | */ |
|
| 76 | 28× | function testSqueezableVsReceived(uint8 targetAccId) public heavy { |
| 77 | 8× | address target = getAccount(targetAccId); |
| 78 | ||
| 79 | // store the current squeezable and receivable amount |
|
| 80 | 8× | uint128 squeezable = getTotalSqueezableAmountForUser(target); |
| 81 | 8× | uint128 receivableBefore = getReceivableAmountForUser(target); |
| 82 | ||
| 83 | // remove all streaming balance from the system, so that warping to |
|
| 84 | // the future will not increase the receivable/squeezable amount |
|
| 85 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 86 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 87 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 88 | 32× | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 89 | ||
| 90 | // warp to the point in time where the streams are receivable |
|
| 91 | 72× | hevm.warp(getCurrentCycleEnd() + 1); |
| 92 | ||
| 93 | 8× | uint128 receivableAfter = getReceivableAmountForUser(target); |
| 94 | ||
| 95 | // sanity check |
|
| 96 | 11× | assert(receivableAfter >= receivableBefore); |
| 97 | ||
| 98 | 11× | uint128 receiveableDelta = receivableAfter - receivableBefore; |
| 99 | ||
| 100 | // squeezable before should match receivable now |
|
| 101 | 10× | assert(squeezable == receiveableDelta); |
| 102 | } |
|
| 103 | ||
| 104 | /** |
|
| 105 | * @notice Squeezing with a fully hashed history should do nothing |
|
| 106 | * @param receiverAccId Account id of the receiver |
|
| 107 | * @param senderAccId Account id of the sender |
|
| 108 | */ |
|
| 109 | 23× | function testSqueezeWithFullyHashedHistory( |
| 110 | uint8 receiverAccId, |
|
| 111 | uint8 senderAccId |
|
| 112 | 6× | ) public { |
| 113 | 8× | address receiver = getAccount(receiverAccId); |
| 114 | 8× | address sender = getAccount(senderAccId); |
| 115 | ||
| 116 | 9× | uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
| 117 | ||
| 118 | 8× | StreamsHistory[] memory history = getStreamsHistory(sender); |
| 119 | 23× | for (uint256 i = 0; i < history.length; i++) { |
| 120 | 115× | history[i].streamsHash = drips.hashStreams(history[i].receivers); |
| 121 | 50× | history[i].receivers = new StreamReceiver[](0); |
| 122 | } |
|
| 123 | ||
| 124 | 7× | uint128 squeezedAmt = _squeeze( |
| 125 | 1× | receiverAccId, |
| 126 | 1× | senderAccId, |
| 127 | 3× | bytes32(0), |
| 128 | 1× | history |
| 129 | ); |
|
| 130 | ||
| 131 | 9× | uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
| 132 | ||
| 133 | 8× | assert(squeezedAmt == 0); |
| 134 | 10× | assert(squeezableAfter == squeezableBefore); |
| 135 | } |
|
| 136 | ||
| 137 | /** |
|
| 138 | * @notice Squeezing the same part(s) of history should only work the first time |
|
| 139 | * @param receiverAccId Account id of the receiver |
|
| 140 | * @param senderAccId Account id of the sender |
|
| 141 | * @param hashIndex Index of the history entry to squeeze |
|
| 142 | * @param receiversRandomSeed Random seed used to determine which history entries |
|
| 143 | * to leave out of the squeeze (by hashing them) |
|
| 144 | */ |
|
| 145 | 25× | function testSqueezeTwice( |
| 146 | uint8 receiverAccId, |
|
| 147 | uint8 senderAccId, |
|
| 148 | uint256 hashIndex, |
|
| 149 | bytes32 receiversRandomSeed |
|
| 150 | 4× | ) external { |
| 151 | 8× | address receiver = getAccount(receiverAccId); |
| 152 | 8× | address sender = getAccount(senderAccId); |
| 153 | ||
| 154 | 7× | uint128 amount0 = squeezeWithFuzzedHistory( |
| 155 | 1× | receiverAccId, |
| 156 | 1× | senderAccId, |
| 157 | 1× | hashIndex, |
| 158 | 1× | receiversRandomSeed |
| 159 | ); |
|
| 160 | ||
| 161 | 7× | uint128 amount1 = squeezeWithFuzzedHistory( |
| 162 | 1× | receiverAccId, |
| 163 | 1× | senderAccId, |
| 164 | 1× | hashIndex, |
| 165 | 1× | receiversRandomSeed |
| 166 | ); |
|
| 167 | ||
| 168 | 8× | assert(amount1 == 0); |
| 169 | } |
|
| 170 | ||
| 171 | /** |
|
| 172 | * @notice Already streamed (and therefore squeezable) balance should not be |
|
| 173 | * affected changing the stream receivers |
|
| 174 | * @param receiverAccId Account id of the receiver |
|
| 175 | * @param senderAccId Account id of the sender |
|
| 176 | * @param amountPerSec Amount per second to stream |
|
| 177 | * @param startTime Start time for the stream |
|
| 178 | * @param duration Duration for the stream |
|
| 179 | * @param balanceDelta Amount to update stream balance with |
|
| 180 | */ |
|
| 181 | 27× | function testSqueezableAmountCantBeUndone( |
| 182 | uint8 receiverAccId, |
|
| 183 | uint8 senderAccId, |
|
| 184 | uint160 amountPerSec, |
|
| 185 | uint32 startTime, |
|
| 186 | uint32 duration, |
|
| 187 | int128 balanceDelta |
|
| 188 | 4× | ) external { |
| 189 | 8× | address receiver = getAccount(receiverAccId); |
| 190 | 8× | address sender = getAccount(senderAccId); |
| 191 | ||
| 192 | 9× | uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
| 193 | ||
| 194 | 5× | setStreams( |
| 195 | 1× | receiverAccId, |
| 196 | 1× | senderAccId, |
| 197 | 1× | amountPerSec, |
| 198 | 1× | startTime, |
| 199 | 1× | duration, |
| 200 | 1× | balanceDelta |
| 201 | ); |
|
| 202 | ||
| 203 | 9× | uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
| 204 | ||
| 205 | 10× | assert(squeezableAfter == squeezableBefore); |
| 206 | } |
|
| 207 | ||
| 208 | /** |
|
| 209 | * @notice Already streamed (and therefore squeezable) balance should not be |
|
| 210 | * affected by withdrawing all streaming balance |
|
| 211 | * @param receiverAccId Account id of the receiver |
|
| 212 | * @param senderAccId Account id of the sender |
|
| 213 | */ |
|
| 214 | 23× | function testSqueezableAmountCantBeWithdrawn( |
| 215 | uint8 receiverAccId, |
|
| 216 | uint8 senderAccId |
|
| 217 | 4× | ) external { |
| 218 | 8× | address receiver = getAccount(receiverAccId); |
| 219 | 8× | address sender = getAccount(senderAccId); |
| 220 | ||
| 221 | 9× | uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
| 222 | ||
| 223 | 6× | setStreamBalanceWithdrawAll(senderAccId); |
| 224 | ||
| 225 | 9× | uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
| 226 | ||
| 227 | 10× | assert(squeezableAfter == squeezableBefore); |
| 228 | } |
|
| 229 | ||
| 230 | /** |
|
| 231 | * @notice Squeezing with default history (all history entries) should |
|
| 232 | * not revert |
|
| 233 | * @param receiverAccId Account id of the receiver |
|
| 234 | * @param senderAccId Account id of the sender |
|
| 235 | */ |
|
| 236 | 23× | function testSqueezeWithDefaultHistoryShouldNotRevert( |
| 237 | uint8 receiverAccId, |
|
| 238 | uint8 senderAccId |
|
| 239 | ) public { |
|
| 240 | 5× | try |
| 241 | 68× | EchidnaSqueezeHelpers(address(this)).squeezeWithDefaultHistory( |
| 242 | 1× | receiverAccId, |
| 243 | 1× | senderAccId |
| 244 | ) |
|
| 245 | {} catch { |
|
| 246 | 0 | assert(false); |
| 247 | } |
|
| 248 | } |
|
| 249 | ||
| 250 | /** |
|
| 251 | * @notice Squeezing with fuzzed history should not revert |
|
| 252 | * @param receiverAccId Account id of the receiver |
|
| 253 | * @param senderAccId Account id of the sender |
|
| 254 | * @param hashIndex Index of the history entry to squeeze |
|
| 255 | * @param receiversRandomSeed Random seed used to determine which history entries |
|
| 256 | * to leave out of the squeeze (by hashing them) |
|
| 257 | */ |
|
| 258 | 25× | function testSqueezeWithFuzzedHistoryShouldNotRevert( |
| 259 | uint8 receiverAccId, |
|
| 260 | uint8 senderAccId, |
|
| 261 | uint256 hashIndex, |
|
| 262 | bytes32 receiversRandomSeed |
|
| 263 | 1× | ) public { |
| 264 | 8× | address sender = getAccount(senderAccId); |
| 265 | 22× | require( |
| 266 | 9× | getStreamsHistory(sender).length >= 2, |
| 267 | "need at least 2 history entries" |
|
| 268 | ); |
|
| 269 | ||
| 270 | 5× | try |
| 271 | 70× | EchidnaSqueezeHelpers(address(this)).squeezeWithFuzzedHistory( |
| 272 | 1× | receiverAccId, |
| 273 | 1× | senderAccId, |
| 274 | 1× | hashIndex, |
| 275 | 1× | receiversRandomSeed |
| 276 | ) |
|
| 277 | {} catch { |
|
| 278 | 0 | assert(false); |
| 279 | } |
|
| 280 | } |
|
| 281 | ||
| 282 | } |
98%
src/echidna/EchidnaStreamsHelpers.sol
Lines covered: 163 / 165 (98%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./base/EchidnaBase.sol"; |
|
| 4 | ||
| 5 | /** |
|
| 6 | * @title Mixin containing helpers for streams |
|
| 7 | * @author Rappie <rappie@perimetersec.io> |
|
| 8 | */ |
|
| 9 | 0 | contract EchidnaStreamsHelpers is EchidnaBase { |
| 10 | // Internal variables to store maxEndHint1 and maxEndHint2. These are used |
|
| 11 | // as hints to the Drips contract to speed up the setStreams call. |
|
| 12 | // |
|
| 13 | // Instead of fuzzing these directly, we use the `setMaxEndHints` helper |
|
| 14 | // function to set these values. This makes fuzzing these toggleable and |
|
| 15 | // also makes debugging easier because the values don't have to be passed |
|
| 16 | // as arguments to all functions calling `setStreams`. |
|
| 17 | // |
|
| 18 | uint32 internal maxEndHint1; |
|
| 19 | uint32 internal maxEndHint2; |
|
| 20 | ||
| 21 | /** |
|
| 22 | * @notice Internal helper function to set streams receivers |
|
| 23 | * @param from Account to set streams for |
|
| 24 | * @param currReceivers Current stream receivers (Drips needs this) |
|
| 25 | * @param balanceDelta Balance delta to set |
|
| 26 | * @param unsortedNewReceivers New receivers list to set |
|
| 27 | * @return Real balance delta |
|
| 28 | * @dev This function also sorts the receivers list |
|
| 29 | */ |
|
| 30 | 17× | function _setStreams( |
| 31 | address from, |
|
| 32 | StreamReceiver[] memory currReceivers, |
|
| 33 | int128 balanceDelta, |
|
| 34 | StreamReceiver[] memory unsortedNewReceivers |
|
| 35 | 3× | ) internal returns (int128) { |
| 36 | 21× | StreamReceiver[] memory newReceivers = bubbleSortStreamReceivers( |
| 37 | 3× | unsortedNewReceivers |
| 38 | ); |
|
| 39 | ||
| 40 | 176× | hevm.prank(from); |
| 41 | 177× | int128 realBalanceDelta = driver.setStreams( |
| 42 | 22× | token, |
| 43 | 2× | currReceivers, |
| 44 | 2× | balanceDelta, |
| 45 | 2× | newReceivers, |
| 46 | 22× | maxEndHint1, |
| 47 | 22× | maxEndHint2, |
| 48 | 2× | from |
| 49 | ); |
|
| 50 | ||
| 51 | 12× | updateStreamReceivers(from, newReceivers); |
| 52 | ||
| 53 | 10× | return realBalanceDelta; |
| 54 | } |
|
| 55 | ||
| 56 | /** |
|
| 57 | * @notice Set streams, overwriting the current receivers list |
|
| 58 | * @param fromAccId Account id of the sender |
|
| 59 | * @param toAccId Account id of the receiver in the receivers list |
|
| 60 | * @param amountPerSec Amount per second to stream |
|
| 61 | * @param startTime Start time of the stream |
|
| 62 | * @param duration Duration of the stream |
|
| 63 | * @param balanceDelta Balance delta to set |
|
| 64 | * @return Real balance delta |
|
| 65 | */ |
|
| 66 | 53× | function setStreams( |
| 67 | uint8 fromAccId, |
|
| 68 | uint8 toAccId, |
|
| 69 | uint160 amountPerSec, |
|
| 70 | uint32 startTime, |
|
| 71 | uint32 duration, |
|
| 72 | int128 balanceDelta |
|
| 73 | 2× | ) public returns (int128) { |
| 74 | 16× | address from = getAccount(fromAccId); |
| 75 | 16× | address to = getAccount(toAccId); |
| 76 | ||
| 77 | 108× | StreamReceiver[] memory receivers = new StreamReceiver[](1); |
| 78 | 66× | receivers[0] = StreamReceiver( |
| 79 | 10× | getDripsAccountId(to), |
| 80 | 8× | StreamConfigImpl.create( |
| 81 | 2× | 0, // streamId is arbitrary and can be ignored |
| 82 | 2× | amountPerSec, |
| 83 | 2× | startTime, |
| 84 | 2× | duration |
| 85 | ) |
|
| 86 | ); |
|
| 87 | ||
| 88 | 14× | int128 realBalanceDelta = _setStreams( |
| 89 | 2× | from, |
| 90 | 10× | getStreamReceivers(from), |
| 91 | 2× | balanceDelta, |
| 92 | 2× | receivers |
| 93 | ); |
|
| 94 | ||
| 95 | 14× | return realBalanceDelta; |
| 96 | } |
|
| 97 | ||
| 98 | /** |
|
| 99 | * @notice Set streams, overwriting the current receivers list |
|
| 100 | * @param fromAccId Account id of the sender |
|
| 101 | * @param toAccId Account id of the receiver in the receivers list |
|
| 102 | * @param amountPerSec Amount per second to stream |
|
| 103 | * @param startTime Start time of the stream |
|
| 104 | * @param duration Duration of the stream |
|
| 105 | * @param balanceDelta Balance delta to set |
|
| 106 | * @return Real balance delta |
|
| 107 | * @dev This function clamps the amountPerSec, startTime, duration and |
|
| 108 | * balanceDelta between the minimum and maximum allowed values |
|
| 109 | */ |
|
| 110 | 86× | function setStreamsWithClamping( |
| 111 | uint8 fromAccId, |
|
| 112 | uint8 toAccId, |
|
| 113 | uint160 amountPerSec, |
|
| 114 | uint32 startTime, |
|
| 115 | uint32 duration, |
|
| 116 | int128 balanceDelta |
|
| 117 | 6× | ) public returns (int128) { |
| 118 | 16× | address from = getAccount(fromAccId); |
| 119 | 16× | address to = getAccount(toAccId); |
| 120 | ||
| 121 | 14× | amountPerSec = clampAmountPerSec(amountPerSec); |
| 122 | 14× | startTime = clampStartTime(startTime); |
| 123 | 14× | duration = clampDuration(duration); |
| 124 | 16× | balanceDelta = clampBalanceDelta(balanceDelta, from); |
| 125 | ||
| 126 | 10× | setStreams( |
| 127 | 2× | fromAccId, |
| 128 | 2× | toAccId, |
| 129 | 2× | amountPerSec, |
| 130 | 2× | startTime, |
| 131 | 2× | duration, |
| 132 | 2× | balanceDelta |
| 133 | ); |
|
| 134 | } |
|
| 135 | ||
| 136 | /** |
|
| 137 | * @notice Add a stream receiver to the existing list of receivers |
|
| 138 | * @param fromAccId Account id of the sender |
|
| 139 | * @param toAccId Account id of the receiver to add |
|
| 140 | * @param amountPerSec Amount per second to stream |
|
| 141 | * @param startTime Start time of the stream |
|
| 142 | * @param duration Duration of the stream |
|
| 143 | * @param balanceDelta Balance delta to set |
|
| 144 | * @return Real balance delta |
|
| 145 | */ |
|
| 146 | 54× | function addStream( |
| 147 | uint8 fromAccId, |
|
| 148 | uint8 toAccId, |
|
| 149 | uint160 amountPerSec, |
|
| 150 | uint32 startTime, |
|
| 151 | uint32 duration, |
|
| 152 | int128 balanceDelta |
|
| 153 | 3× | ) public returns (int128) { |
| 154 | 24× | address from = getAccount(fromAccId); |
| 155 | 24× | address to = getAccount(toAccId); |
| 156 | ||
| 157 | 24× | StreamReceiver[] memory oldReceivers = getStreamReceivers(from); |
| 158 | ||
| 159 | 54× | StreamReceiver memory addedReceiver = StreamReceiver( |
| 160 | 15× | getDripsAccountId(to), |
| 161 | 12× | StreamConfigImpl.create( |
| 162 | 3× | 0, // streamId is arbitrary and can be ignored |
| 163 | 3× | amountPerSec, |
| 164 | 3× | startTime, |
| 165 | 3× | duration |
| 166 | ) |
|
| 167 | ); |
|
| 168 | ||
| 169 | 159× | StreamReceiver[] memory newReceivers = new StreamReceiver[]( |
| 170 | 27× | oldReceivers.length + 1 |
| 171 | ); |
|
| 172 | 69× | for (uint256 i = 0; i < oldReceivers.length; i++) { |
| 173 | 99× | newReceivers[i] = oldReceivers[i]; |
| 174 | } |
|
| 175 | 81× | newReceivers[newReceivers.length - 1] = addedReceiver; |
| 176 | ||
| 177 | 18× | int128 realBalanceDelta = _setStreams( |
| 178 | 3× | from, |
| 179 | 3× | oldReceivers, |
| 180 | 3× | balanceDelta, |
| 181 | 3× | newReceivers |
| 182 | ); |
|
| 183 | ||
| 184 | 18× | return realBalanceDelta; |
| 185 | } |
|
| 186 | ||
| 187 | /** |
|
| 188 | * @notice Add a stream receiver to the existing list of receivers |
|
| 189 | * @param fromAccId Account id of the sender |
|
| 190 | * @param toAccId Account id of the receiver to add |
|
| 191 | * @param amountPerSec Amount per second to stream |
|
| 192 | * @param startTime Start time of the stream |
|
| 193 | * @param duration Duration of the stream |
|
| 194 | * @param balanceDelta Balance delta to set |
|
| 195 | * @dev This function clamps the amountPerSec, startTime, duration and |
|
| 196 | * balanceDelta between the minimum and maximum allowed values |
|
| 197 | */ |
|
| 198 | 86× | function addStreamWithClamping( |
| 199 | uint8 fromAccId, |
|
| 200 | uint8 toAccId, |
|
| 201 | uint160 amountPerSec, |
|
| 202 | uint32 startTime, |
|
| 203 | uint32 duration, |
|
| 204 | int128 balanceDelta |
|
| 205 | 2× | ) public returns (int128) { |
| 206 | 16× | address from = getAccount(fromAccId); |
| 207 | ||
| 208 | 14× | amountPerSec = clampAmountPerSec(amountPerSec); |
| 209 | 14× | startTime = clampStartTime(startTime); |
| 210 | 14× | duration = clampDuration(duration); |
| 211 | 16× | balanceDelta = clampBalanceDelta(balanceDelta, from); |
| 212 | ||
| 213 | 6× | return |
| 214 | 8× | addStream( |
| 215 | 2× | fromAccId, |
| 216 | 2× | toAccId, |
| 217 | 2× | amountPerSec, |
| 218 | 2× | startTime, |
| 219 | 2× | duration, |
| 220 | 2× | balanceDelta |
| 221 | ); |
|
| 222 | } |
|
| 223 | ||
| 224 | /** |
|
| 225 | * @notice Add a stream receiver to the existing list of receivers, making sure |
|
| 226 | * it is immediately squeezable in a transaction after this call |
|
| 227 | * @param fromAccId Account id of the sender |
|
| 228 | * @param toAccId Account id of the receiver to add |
|
| 229 | * @param amountPerSec Amount per second to stream |
|
| 230 | * @dev This is meant as a helper to quickly seed the corpus with situations |
|
| 231 | * where there is something to squeeze in the next transaction |
|
| 232 | */ |
|
| 233 | 24× | function addStreamImmediatelySqueezable( |
| 234 | uint8 fromAccId, |
|
| 235 | uint8 toAccId, |
|
| 236 | uint160 amountPerSec |
|
| 237 | 4× | ) public { |
| 238 | 8× | address receiver = getAccount(toAccId); |
| 239 | 8× | address sender = getAccount(fromAccId); |
| 240 | ||
| 241 | // calculate amount per second so there will be something to squeeze |
|
| 242 | // this cycle |
|
| 243 | 80× | uint160 minAmtPerSec = drips.minAmtPerSec() * SECONDS_PER_CYCLE; |
| 244 | 2× | amountPerSec = |
| 245 | 7× | minAmtPerSec + |
| 246 | 21× | (amountPerSec % (MAX_AMOUNT_PER_SEC - minAmtPerSec + 1)); |
| 247 | ||
| 248 | // deposit 100 times the amount of 'amountPerSec' so chances are high |
|
| 249 | // that there is enough balance to stream this cycle |
|
| 250 | 18× | int128 balanceDelta = (int128(uint128(amountPerSec)) * 100) / 1e9; |
| 251 | 84× | if (uint128(balanceDelta) > token.balanceOf(sender)) { |
| 252 | 78× | balanceDelta = int128(uint128(token.balanceOf(sender))); |
| 253 | } |
|
| 254 | ||
| 255 | // add the stream |
|
| 256 | 11× | addStream(fromAccId, toAccId, amountPerSec, 0, 0, balanceDelta); |
| 257 | ||
| 258 | // warp 1 second forward so there is something to squeeze |
|
| 259 | 69× | hevm.warp(block.timestamp + 1); |
| 260 | } |
|
| 261 | ||
| 262 | /** |
|
| 263 | * @notice Remove a stream receiver from the existing list of receivers |
|
| 264 | * @param targetAccId Account id of the receiver to remove |
|
| 265 | * @param indexSeed Random seed used to determine which receiver to remove |
|
| 266 | */ |
|
| 267 | 56× | function removeStream(uint8 targetAccId, uint256 indexSeed) public { |
| 268 | 16× | address target = getAccount(targetAccId); |
| 269 | ||
| 270 | 16× | StreamReceiver[] memory oldReceivers = getStreamReceivers(target); |
| 271 | ||
| 272 | 24× | uint256 index = indexSeed % oldReceivers.length; |
| 273 | ||
| 274 | 106× | StreamReceiver[] memory newReceivers = new StreamReceiver[]( |
| 275 | 18× | oldReceivers.length - 1 |
| 276 | ); |
|
| 277 | 2× | uint256 j = 0; |
| 278 | 46× | for (uint256 i = 0; i < oldReceivers.length; i++) { |
| 279 | 12× | if (i != index) { |
| 280 | 66× | newReceivers[j] = oldReceivers[i]; |
| 281 | 20× | j++; |
| 282 | } |
|
| 283 | } |
|
| 284 | ||
| 285 | 18× | _setStreams(target, oldReceivers, 0, newReceivers); |
| 286 | } |
|
| 287 | ||
| 288 | /** |
|
| 289 | * @notice Update stream balance by calling `setStreams` with the same |
|
| 290 | * receivers list |
|
| 291 | * @param targetAccId Account id of the sender |
|
| 292 | * @param balanceDelta Balance delta to set |
|
| 293 | * @return Real balance delta |
|
| 294 | */ |
|
| 295 | 45× | function setStreamBalance(uint8 targetAccId, int128 balanceDelta) |
| 296 | public |
|
| 297 | 2× | returns (int128) |
| 298 | { |
|
| 299 | 16× | address target = getAccount(targetAccId); |
| 300 | ||
| 301 | 14× | int128 realBalanceDelta = _setStreams( |
| 302 | 2× | target, |
| 303 | 10× | getStreamReceivers(target), |
| 304 | 2× | balanceDelta, |
| 305 | 10× | getStreamReceivers(target) |
| 306 | ); |
|
| 307 | ||
| 308 | 10× | return realBalanceDelta; |
| 309 | } |
|
| 310 | ||
| 311 | /** |
|
| 312 | * @notice Update stream balance by calling `setStreams` with the same |
|
| 313 | * receivers list |
|
| 314 | * @param targetAccId Account id of the sender |
|
| 315 | * @param balanceDelta Balance delta to set |
|
| 316 | * @dev This function clamps the balanceDelta between the minimum and |
|
| 317 | * maximum allowed values |
|
| 318 | */ |
|
| 319 | 46× | function setStreamBalanceWithClamping( |
| 320 | uint8 targetAccId, |
|
| 321 | int128 balanceDelta |
|
| 322 | 2× | ) public { |
| 323 | 16× | address target = getAccount(targetAccId); |
| 324 | 16× | balanceDelta = clampBalanceDelta(balanceDelta, target); |
| 325 | 14× | setStreamBalance(targetAccId, balanceDelta); |
| 326 | } |
|
| 327 | ||
| 328 | /** |
|
| 329 | * @notice Withdraw all stream balance by calling `setStreams` with the same |
|
| 330 | * receivers list and using min int128 as balance delta |
|
| 331 | * @param targetAccId Account id of the sender |
|
| 332 | * @return Real balance delta |
|
| 333 | */ |
|
| 334 | 77× | function setStreamBalanceWithdrawAll(uint8 targetAccId) |
| 335 | public |
|
| 336 | 3× | returns (int128) |
| 337 | 6× | { |
| 338 | 24× | address target = getAccount(targetAccId); |
| 339 | 24× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 340 | ||
| 341 | 18× | int128 realBalanceDelta = _setStreams( |
| 342 | 3× | target, |
| 343 | 15× | getStreamReceivers(target), |
| 344 | 3× | type(int128).min, |
| 345 | 15× | getStreamReceivers(target) |
| 346 | ); |
|
| 347 | } |
|
| 348 | ||
| 349 | /** |
|
| 350 | * @notice Helper function to update the values used as maxEnd hints |
|
| 351 | * @param _maxEndHint1 New value for maxEndHint1 |
|
| 352 | * @param _maxEndHint2 New value for maxEndHint2 |
|
| 353 | * @dev Can be toggled on/off with TOGGLE_MAXENDHINTS_ENABLED |
|
| 354 | */ |
|
| 355 | 23× | function setMaxEndHints(uint32 _maxEndHint1, uint32 _maxEndHint2) public { |
| 356 | 3× | require(TOGGLE_MAXENDHINTS_ENABLED); |
| 357 | 21× | maxEndHint1 = _maxEndHint1; |
| 358 | 21× | maxEndHint2 = _maxEndHint2; |
| 359 | } |
|
| 360 | ||
| 361 | /** |
|
| 362 | * @notice Clamp the amountPerSec between the minimum and maximum allowed values |
|
| 363 | * @param amountPerSec Amount per second to clamp |
|
| 364 | * @return Clamped amountPerSec |
|
| 365 | */ |
|
| 366 | 10× | function clampAmountPerSec(uint160 amountPerSec) |
| 367 | internal |
|
| 368 | 2× | returns (uint160) |
| 369 | { |
|
| 370 | 4× | return |
| 371 | 150× | drips.minAmtPerSec() + |
| 372 | 178× | (amountPerSec % (MAX_AMOUNT_PER_SEC - drips.minAmtPerSec() + 1)); |
| 373 | } |
|
| 374 | ||
| 375 | /** |
|
| 376 | * @notice Clamp the startTime between the minimum and maximum allowed values |
|
| 377 | * @param startTime Start time to clamp |
|
| 378 | * @return Clamped startTime |
|
| 379 | */ |
|
| 380 | 14× | function clampStartTime(uint32 startTime) internal returns (uint32) { |
| 381 | 26× | if (startTime == 0) return 0; |
| 382 | ||
| 383 | // We want to make sure that the start time does not go below 1 |
|
| 384 | 2× | uint32 minStartTime; |
| 385 | 16× | if (CYCLE_FUZZING_BUFFER_SECONDS >= block.timestamp) { |
| 386 | 0 | minStartTime = 1; |
| 387 | } else { |
|
| 388 | 4× | minStartTime = |
| 389 | 14× | uint32(block.timestamp) - |
| 390 | CYCLE_FUZZING_BUFFER_SECONDS; |
|
| 391 | } |
|
| 392 | ||
| 393 | 20× | uint32 maxStartTime = uint32(block.timestamp) + |
| 394 | CYCLE_FUZZING_BUFFER_SECONDS; |
|
| 395 | ||
| 396 | 66× | return minStartTime + (startTime % (maxStartTime - minStartTime + 1)); |
| 397 | } |
|
| 398 | ||
| 399 | /** |
|
| 400 | * @notice Clamp the duration between the minimum and maximum allowed values |
|
| 401 | * @param duration Duration to clamp |
|
| 402 | * @return Clamped duration |
|
| 403 | */ |
|
| 404 | 14× | function clampDuration(uint32 duration) internal returns (uint32) { |
| 405 | 26× | if (duration == 0) return 0; |
| 406 | ||
| 407 | 32× | return duration % (MAX_STREAM_DURATION + 1); |
| 408 | } |
|
| 409 | ||
| 410 | /** |
|
| 411 | * @notice Clamp the balanceDelta between the minimum and maximum allowed values |
|
| 412 | * @param balanceDelta Balance delta to clamp |
|
| 413 | * @param from Account performing the setStreams action |
|
| 414 | * @return Clamped balanceDelta |
|
| 415 | */ |
|
| 416 | 18× | function clampBalanceDelta(int128 balanceDelta, address from) |
| 417 | internal |
|
| 418 | 3× | returns (int128) |
| 419 | { |
|
| 420 | 35× | if (balanceDelta > 0) { |
| 421 | 6× | balanceDelta = |
| 422 | 21× | balanceDelta % |
| 423 | 249× | (int128(uint128(token.balanceOf(from))) + 1); |
| 424 | } else { |
|
| 425 | 18× | balanceDelta = balanceDelta % int128(uint128(STARTING_BALANCE)); |
| 426 | } |
|
| 427 | 9× | return balanceDelta; |
| 428 | } |
|
| 429 | } |
94%
src/echidna/EchidnaStreamsTests.sol
Lines covered: 112 / 118 (94%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./base/EchidnaBase.sol"; |
|
| 4 | import "./EchidnaStreamsHelpers.sol"; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @title Mixin containing tests for streams |
|
| 8 | * @author Rappie <rappie@perimetersec.io> |
|
| 9 | */ |
|
| 10 | 0 | contract EchidnaStreamsTests is EchidnaBase, EchidnaStreamsHelpers { |
| 11 | /** |
|
| 12 | * @notice Test internal accounting after updating stream balance |
|
| 13 | * @param targetAccId Account id of the sender |
|
| 14 | * @param balanceDelta Amount to update stream balance with |
|
| 15 | */ |
|
| 16 | 23× | function testSetStreamBalance(uint8 targetAccId, int128 balanceDelta) |
| 17 | public |
|
| 18 | 7× | { |
| 19 | 8× | address target = getAccount(targetAccId); |
| 20 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 21 | ||
| 22 | 79× | uint256 tokenBalanceBefore = token.balanceOf(target); |
| 23 | 81× | uint128 streamBalanceBefore = drips.balanceAt( |
| 24 | 1× | targetDripsAccId, |
| 25 | 11× | token, |
| 26 | 5× | getStreamReceivers(target), |
| 27 | 1× | uint32(block.timestamp) |
| 28 | ); |
|
| 29 | ||
| 30 | 9× | int128 realBalanceDelta = setStreamBalance(targetAccId, balanceDelta); |
| 31 | ||
| 32 | 79× | uint256 tokenBalanceAfter = token.balanceOf(target); |
| 33 | 81× | uint128 streamBalanceAfter = drips.balanceAt( |
| 34 | 1× | targetDripsAccId, |
| 35 | 11× | token, |
| 36 | 5× | getStreamReceivers(target), |
| 37 | 1× | uint32(block.timestamp) |
| 38 | ); |
|
| 39 | ||
| 40 | 11× | if (balanceDelta >= 0) { |
| 41 | 10× | assert(realBalanceDelta == balanceDelta); |
| 42 | } else { |
|
| 43 | 9× | assert(realBalanceDelta <= 0); |
| 44 | 11× | assert(realBalanceDelta >= balanceDelta); |
| 45 | } |
|
| 46 | ||
| 47 | 3× | assert( |
| 48 | 2× | int256(tokenBalanceAfter) == |
| 49 | 10× | int256(tokenBalanceBefore) - realBalanceDelta |
| 50 | ); |
|
| 51 | 3× | assert( |
| 52 | 6× | int128(streamBalanceAfter) == |
| 53 | 8× | int128(streamBalanceBefore) + realBalanceDelta |
| 54 | ); |
|
| 55 | } |
|
| 56 | ||
| 57 | /** |
|
| 58 | * @notice Check balances before and after adding a stream, warping to the |
|
| 59 | * future, and receiving the stream. |
|
| 60 | * @param fromAccId Account id of the sender |
|
| 61 | * @param toAccId Account id to be added as receiver |
|
| 62 | * @param amtPerSecAdded Amount per second for the stream to be added |
|
| 63 | * @dev This test is resource heavy because it contains lots of logic and |
|
| 64 | * warping to the future. |
|
| 65 | */ |
|
| 66 | 25× | function testBalanceAtInFuture( |
| 67 | uint8 fromAccId, |
|
| 68 | uint8 toAccId, |
|
| 69 | uint160 amtPerSecAdded |
|
| 70 | 18× | ) public heavy { |
| 71 | 8× | address from = getAccount(fromAccId); |
| 72 | 8× | address to = getAccount(toAccId); |
| 73 | 8× | uint256 fromDripsAccId = getDripsAccountId(from); |
| 74 | 8× | uint256 toDripsAccId = getDripsAccountId(to); |
| 75 | ||
| 76 | 7× | amtPerSecAdded = clampAmountPerSec(amtPerSecAdded); |
| 77 | ||
| 78 | // the timestamps we are comparing |
|
| 79 | 4× | uint256 currentTimestamp = block.timestamp; |
| 80 | 16× | uint256 futureTimestamp = getCurrentCycleEnd() + 1; |
| 81 | ||
| 82 | // retrieve initial balances |
|
| 83 | 7× | uint128 balanceInitial = getStreamBalanceForUser( |
| 84 | 1× | from, |
| 85 | 1× | uint32(block.timestamp) |
| 86 | ); |
|
| 87 | 7× | uint128 receivableInitial = getReceivableAmountForAllUsers(); |
| 88 | ||
| 89 | // look at balances in the future if we wouldnt do anything |
|
| 90 | 62× | hevm.warp(futureTimestamp); |
| 91 | 7× | uint128 balanceBaseline = getStreamBalanceForUser( |
| 92 | 1× | from, |
| 93 | 1× | uint32(block.timestamp) |
| 94 | ); |
|
| 95 | 7× | uint128 receivableBaseline = getReceivableAmountForAllUsers(); |
| 96 | 62× | hevm.warp(currentTimestamp); |
| 97 | ||
| 98 | // add a stream |
|
| 99 | // make sure we add enough balance to complete the cycle |
|
| 100 | 12× | uint128 balanceAdded = uint128(amtPerSecAdded) * SECONDS_PER_CYCLE; |
| 101 | 8× | balanceAdded = uint128(clampBalanceDelta(int128(balanceAdded), from)); |
| 102 | 22× | require(balanceAdded / amtPerSecAdded >= SECONDS_PER_CYCLE); |
| 103 | 5× | addStream( |
| 104 | 1× | fromAccId, |
| 105 | 1× | toAccId, |
| 106 | 1× | amtPerSecAdded, |
| 107 | 1× | 0, |
| 108 | 1× | 0, |
| 109 | 1× | int128(balanceAdded) |
| 110 | ); |
|
| 111 | ||
| 112 | // retrieve balances after adding stream |
|
| 113 | 7× | uint128 balanceBefore = getStreamBalanceForUser( |
| 114 | 1× | from, |
| 115 | 1× | uint32(block.timestamp) |
| 116 | ); |
|
| 117 | 7× | uint128 receivableBefore = getReceivableAmountForAllUsers(); |
| 118 | ||
| 119 | // jump to future |
|
| 120 | 62× | hevm.warp(futureTimestamp); |
| 121 | ||
| 122 | // retrieve balances in the future after adding the stream |
|
| 123 | 7× | uint128 balanceAfter = getStreamBalanceForUser( |
| 124 | 1× | from, |
| 125 | 1× | uint32(block.timestamp) |
| 126 | ); |
|
| 127 | 7× | uint128 receivableAfter = getReceivableAmountForAllUsers(); |
| 128 | ||
| 129 | // sanity checks |
|
| 130 | 11× | assert(balanceInitial >= balanceBaseline); |
| 131 | 11× | assert(balanceBefore >= balanceAfter); |
| 132 | 11× | assert(receivableAfter >= receivableBaseline); |
| 133 | ||
| 134 | // the amount that would have been streamed if we do nothing |
|
| 135 | 11× | uint128 baselineBalanceStreamed = balanceInitial - balanceBaseline; |
| 136 | ||
| 137 | // calculate expected balance change including the effect of the |
|
| 138 | // added stream |
|
| 139 | 16× | uint128 expectedBalanceChange = balanceBefore - |
| 140 | 1× | balanceAfter - |
| 141 | 1× | baselineBalanceStreamed; |
| 142 | 11× | uint128 expectedReceivedChange = receivableAfter - receivableBaseline; |
| 143 | ||
| 144 | 10× | assert(expectedBalanceChange == expectedReceivedChange); |
| 145 | } |
|
| 146 | ||
| 147 | /** |
|
| 148 | * @notice Setting streams with sane defaults should not revert |
|
| 149 | * @param fromAccId Account id of the sender |
|
| 150 | * @param toAccId Account id of the receiver |
|
| 151 | * @param amountPerSec Amount per second for the stream |
|
| 152 | * @param startTime Start time for the stream |
|
| 153 | * @param duration Duration for the stream |
|
| 154 | * @param balanceDelta Amount to update stream balance with |
|
| 155 | */ |
|
| 156 | 27× | function testSetStreamsShouldNotRevert( |
| 157 | uint8 fromAccId, |
|
| 158 | uint8 toAccId, |
|
| 159 | uint160 amountPerSec, |
|
| 160 | uint32 startTime, |
|
| 161 | uint32 duration, |
|
| 162 | int128 balanceDelta |
|
| 163 | ) public { |
|
| 164 | 5× | try |
| 165 | 72× | EchidnaStreamsHelpers(address(this)).setStreamsWithClamping( |
| 166 | 1× | fromAccId, |
| 167 | 1× | toAccId, |
| 168 | 1× | amountPerSec, |
| 169 | 1× | startTime, |
| 170 | 1× | duration, |
| 171 | 1× | balanceDelta |
| 172 | ) |
|
| 173 | {} catch { |
|
| 174 | 0 | assert(false); |
| 175 | } |
|
| 176 | } |
|
| 177 | ||
| 178 | /** |
|
| 179 | * @notice Adding streams with sane defaults should not revert |
|
| 180 | * @param fromAccId Account id of the sender |
|
| 181 | * @param toAccId Account id of the receiver |
|
| 182 | * @param amountPerSec Amount per second for the stream |
|
| 183 | * @param startTime Start time for the stream |
|
| 184 | * @param duration Duration for the stream |
|
| 185 | * @param balanceDelta Amount to update stream balance with |
|
| 186 | */ |
|
| 187 | 36× | function testAddStreamShouldNotRevert( |
| 188 | uint8 fromAccId, |
|
| 189 | uint8 toAccId, |
|
| 190 | uint160 amountPerSec, |
|
| 191 | uint32 startTime, |
|
| 192 | uint32 duration, |
|
| 193 | int128 balanceDelta |
|
| 194 | ) public { |
|
| 195 | 44× | try |
| 196 | 81× | EchidnaStreamsHelpers(address(this)).addStreamWithClamping( |
| 197 | 1× | fromAccId, |
| 198 | 1× | toAccId, |
| 199 | 1× | amountPerSec, |
| 200 | 1× | startTime, |
| 201 | 1× | duration, |
| 202 | 1× | balanceDelta |
| 203 | ) |
|
| 204 | 2× | {} catch (bytes memory reason) { |
| 205 | 9× | bytes4 errorSelector = bytes4(reason); |
| 206 | 15× | if (errorSelector == EchidnaStorage.DuplicateError.selector) { |
| 207 | // ignore this case, it means we tried to add a duplicate stream |
|
| 208 | } else { |
|
| 209 | 0 | assert(false); |
| 210 | } |
|
| 211 | } |
|
| 212 | } |
|
| 213 | ||
| 214 | /* |
|
| 215 | * @notice Removing streams should not revert |
|
| 216 | * @param targetAccId Account id of the sender |
|
| 217 | * @param indexSeed Random seed used to determine which receiver to remove |
|
| 218 | */ |
|
| 219 | 23× | function testRemoveStreamShouldNotRevert( |
| 220 | uint8 targetAccId, |
|
| 221 | uint256 indexSeed |
|
| 222 | 1× | ) public { |
| 223 | 8× | address target = getAccount(targetAccId); |
| 224 | 14× | require(getStreamReceivers(target).length > 0); |
| 225 | ||
| 226 | 4× | try |
| 227 | 53× | EchidnaStreamsHelpers(address(this)).removeStream( |
| 228 | 1× | targetAccId, |
| 229 | 1× | indexSeed |
| 230 | ) |
|
| 231 | {} catch { |
|
| 232 | 0 | assert(false); |
| 233 | } |
|
| 234 | } |
|
| 235 | ||
| 236 | /** |
|
| 237 | * @notice Updating stream balance with sane defaults should not revert |
|
| 238 | * @param targetAccId Account id of the sender |
|
| 239 | * @param balanceDelta Amount to update stream balance with |
|
| 240 | */ |
|
| 241 | 23× | function testSetStreamBalanceShouldNotRevert( |
| 242 | uint8 targetAccId, |
|
| 243 | int128 balanceDelta |
|
| 244 | ) public { |
|
| 245 | 4× | try |
| 246 | 53× | EchidnaStreamsHelpers(address(this)).setStreamBalanceWithClamping( |
| 247 | 1× | targetAccId, |
| 248 | 1× | balanceDelta |
| 249 | ) |
|
| 250 | {} catch { |
|
| 251 | 0 | assert(false); |
| 252 | } |
|
| 253 | } |
|
| 254 | ||
| 255 | /** |
|
| 256 | * @notice Withdrawing all stream balance should not revert |
|
| 257 | * @param targetAccId Account id of the sender |
|
| 258 | */ |
|
| 259 | 22× | function testSetStreamBalanceWithdrawAllShouldNotRevert(uint8 targetAccId) |
| 260 | public |
|
| 261 | { |
|
| 262 | 5× | try |
| 263 | 67× | EchidnaStreamsHelpers(address(this)).setStreamBalanceWithdrawAll( |
| 264 | 1× | targetAccId |
| 265 | ) |
|
| 266 | {} catch { |
|
| 267 | 0 | assert(false); |
| 268 | } |
|
| 269 | } |
|
| 270 | } |
97%
src/echidna/base/EchidnaAccounting.sol
Lines covered: 87 / 89 (97%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./EchidnaStorage.sol"; |
|
| 4 | ||
| 5 | /** |
|
| 6 | * @title Mixin for handling accounting related functions |
|
| 7 | * @author Rappie <rappie@perimetersec.io> |
|
| 8 | */ |
|
| 9 | 0 | contract EchidnaAccounting is EchidnaStorage { |
| 10 | /** |
|
| 11 | * @notice Get the total amount of drips balances for all users |
|
| 12 | * @return Total drips balances for all users |
|
| 13 | * @dev This is the total amount of drips balances for all users, including |
|
| 14 | * the current stream balance, the receivable amount, the collectable |
|
| 15 | * amount, the splittable amount, and the squeezable amount. |
|
| 16 | */ |
|
| 17 | 4× | function getDripsBalancesTotalForAllUsers() internal returns (uint256) { |
| 18 | 7× | uint256 user0Total = getDripsBalancesTotalForUser(ADDRESS_USER0); |
| 19 | 7× | uint256 user1Total = getDripsBalancesTotalForUser(ADDRESS_USER1); |
| 20 | 7× | uint256 user2Total = getDripsBalancesTotalForUser(ADDRESS_USER2); |
| 21 | 7× | uint256 user3Total = getDripsBalancesTotalForUser(ADDRESS_USER3); |
| 22 | ||
| 23 | 28× | return user0Total + user1Total + user2Total + user3Total; |
| 24 | } |
|
| 25 | ||
| 26 | /** |
|
| 27 | * @notice Get the total amount of drips balances for a user |
|
| 28 | * @param target The user to query |
|
| 29 | * @return Total drips balances for the target user |
|
| 30 | * @dev This is the total amount of drips balances for a user, including |
|
| 31 | * the current stream balance, the receivable amount, the collectable |
|
| 32 | * amount, the splittable amount, and the squeezable amount. |
|
| 33 | */ |
|
| 34 | 5× | function getDripsBalancesTotalForUser(address target) |
| 35 | internal |
|
| 36 | 1× | returns (uint256) |
| 37 | { |
|
| 38 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 39 | ||
| 40 | 8× | uint128 balance = getCurrentStreamBalanceForUser(target); |
| 41 | 8× | uint128 squeezable = getTotalSqueezableAmountForUser(target); |
| 42 | 8× | uint128 receivable = getReceivableAmountForUser(target); |
| 43 | 91× | uint128 collectable = drips.collectable(targetDripsAccId, token); |
| 44 | 91× | uint128 splittable = drips.splittable(targetDripsAccId, token); |
| 45 | ||
| 46 | 39× | return balance + squeezable + receivable + collectable + splittable; |
| 47 | } |
|
| 48 | ||
| 49 | /** |
|
| 50 | * @notice Get the streamable balance for a user |
|
| 51 | * @param target The user to query |
|
| 52 | * @param timestamp The point in time to get 'balanceAt' from |
|
| 53 | * @return Streamable balance for the target user |
|
| 54 | * @dev This is the streamable balance for a user, which is the current |
|
| 55 | * stream balance minus the receivable amount. |
|
| 56 | */ |
|
| 57 | 12× | function getStreamBalanceForUser(address target, uint32 timestamp) |
| 58 | internal |
|
| 59 | 2× | returns (uint128) |
| 60 | { |
|
| 61 | 16× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 62 | ||
| 63 | 2× | uint128 balance; |
| 64 | 8× | try |
| 65 | 158× | drips.balanceAt( |
| 66 | 2× | targetDripsAccId, |
| 67 | 22× | token, |
| 68 | 10× | getStreamReceivers(target), |
| 69 | 2× | timestamp |
| 70 | ) |
|
| 71 | 2× | returns (uint128 _balance) { |
| 72 | 6× | balance = _balance; |
| 73 | } catch { |
|
| 74 | // this should not happen, so put an assert here to be sure |
|
| 75 | 0 | assert(false); |
| 76 | } |
|
| 77 | ||
| 78 | 10× | return balance; |
| 79 | } |
|
| 80 | ||
| 81 | /** |
|
| 82 | * @notice Get the current stream balance for a user |
|
| 83 | * @param target The user to query |
|
| 84 | * @return Current stream balance for the target user |
|
| 85 | */ |
|
| 86 | 5× | function getCurrentStreamBalanceForUser(address target) |
| 87 | internal |
|
| 88 | 1× | returns (uint128) |
| 89 | { |
|
| 90 | 8× | return getStreamBalanceForUser(target, uint32(block.timestamp)); |
| 91 | } |
|
| 92 | ||
| 93 | /** |
|
| 94 | * @notice Get the receivable amount for all users |
|
| 95 | * @return Receivable amount for all users |
|
| 96 | * @dev This is the receivable amount for all users, which means the amount |
|
| 97 | * that has already been streamed to the users but not yet collected. |
|
| 98 | */ |
|
| 99 | 8× | function getReceivableAmountForAllUsers() internal returns (uint128) { |
| 100 | 2× | uint128 receivable; |
| 101 | 26× | receivable += getReceivableAmountForUser(ADDRESS_USER0); |
| 102 | 26× | receivable += getReceivableAmountForUser(ADDRESS_USER1); |
| 103 | 26× | receivable += getReceivableAmountForUser(ADDRESS_USER2); |
| 104 | 26× | receivable += getReceivableAmountForUser(ADDRESS_USER3); |
| 105 | 8× | return receivable; |
| 106 | } |
|
| 107 | ||
| 108 | /** |
|
| 109 | * @notice Get the receivable amount for a user |
|
| 110 | * @param target The user to query |
|
| 111 | * @return Receivable amount for the target user |
|
| 112 | * @dev This is the receivable amount for a user, which means the amount |
|
| 113 | * that has already been streamed to the user but not yet collected. |
|
| 114 | */ |
|
| 115 | 10× | function getReceivableAmountForUser(address target) |
| 116 | internal |
|
| 117 | 2× | returns (uint128) |
| 118 | { |
|
| 119 | 174× | uint128 receivable = drips.receiveStreamsResult( |
| 120 | 10× | getDripsAccountId(target), |
| 121 | 22× | token, |
| 122 | 2× | type(uint32).max |
| 123 | ); |
|
| 124 | 8× | return receivable; |
| 125 | } |
|
| 126 | ||
| 127 | /** |
|
| 128 | * @notice Get the squeezable amount for all users |
|
| 129 | * @param target The user to query |
|
| 130 | * @return Squeezable amount for all users |
|
| 131 | * @dev This is the squeezable amount for a user, which means the amount |
|
| 132 | * that has already been streamed in the current cycle that is not yet |
|
| 133 | * receivable. |
|
| 134 | */ |
|
| 135 | 5× | function getTotalSqueezableAmountForUser(address target) |
| 136 | internal |
|
| 137 | 1× | returns (uint128) |
| 138 | { |
|
| 139 | 4× | uint128 amount = 0; |
| 140 | 14× | amount += getSqueezableAmount(ADDRESS_USER0, target); |
| 141 | 14× | amount += getSqueezableAmount(ADDRESS_USER1, target); |
| 142 | 14× | amount += getSqueezableAmount(ADDRESS_USER2, target); |
| 143 | 14× | amount += getSqueezableAmount(ADDRESS_USER3, target); |
| 144 | ||
| 145 | 4× | return amount; |
| 146 | } |
|
| 147 | ||
| 148 | /** |
|
| 149 | * @notice Get the squeezable amount for a user |
|
| 150 | * @param sender The sender of the stream(s) |
|
| 151 | * @param receiver The receiver of the stream(s) |
|
| 152 | * @return Squeezable amount for the target user |
|
| 153 | * @dev This is the squeezable amount for a user, which means the amount |
|
| 154 | * that has already been streamed in the current cycle that is not yet |
|
| 155 | * receivable. |
|
| 156 | */ |
|
| 157 | 12× | function getSqueezableAmount(address sender, address receiver) |
| 158 | internal |
|
| 159 | 2× | returns (uint128) |
| 160 | { |
|
| 161 | 16× | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 162 | 16× | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 163 | ||
| 164 | 164× | uint128 amount = drips.squeezeStreamsResult( |
| 165 | 2× | receiverDripsAccId, |
| 166 | 22× | token, |
| 167 | 2× | senderDripsAccId, |
| 168 | 6× | bytes32(0), |
| 169 | 10× | getStreamsHistory(sender) |
| 170 | ); |
|
| 171 | ||
| 172 | 12× | return amount; |
| 173 | } |
|
| 174 | ||
| 175 | /** |
|
| 176 | * @notice Get the 'maxEnd' farthest in the future for all users |
|
| 177 | * @return 'maxEnd' fartherst in the future for all users |
|
| 178 | * @dev This is the 'maxEnd' farthest in the future for all users, which |
|
| 179 | * means the farthest in the future that any stream ends for any user. |
|
| 180 | */ |
|
| 181 | 4× | function getMaxEndForAllUsers() internal returns (uint32) { |
| 182 | 1× | uint32 maxMaxEnd; |
| 183 | ||
| 184 | 7× | uint32 maxEndUser0 = getMaxEndForUser(ADDRESS_USER0); |
| 185 | 14× | if (maxEndUser0 > maxMaxEnd) maxMaxEnd = maxEndUser0; |
| 186 | 7× | uint32 maxEndUser1 = getMaxEndForUser(ADDRESS_USER1); |
| 187 | 14× | if (maxEndUser1 > maxMaxEnd) maxMaxEnd = maxEndUser1; |
| 188 | 7× | uint32 maxEndUser2 = getMaxEndForUser(ADDRESS_USER2); |
| 189 | 14× | if (maxEndUser2 > maxMaxEnd) maxMaxEnd = maxEndUser2; |
| 190 | 7× | uint32 maxEndUser3 = getMaxEndForUser(ADDRESS_USER3); |
| 191 | 14× | if (maxEndUser3 > maxMaxEnd) maxMaxEnd = maxEndUser3; |
| 192 | ||
| 193 | 8× | return maxMaxEnd; |
| 194 | } |
|
| 195 | ||
| 196 | /** |
|
| 197 | * @notice Get the 'maxEnd' for a user |
|
| 198 | * @param target The user to query |
|
| 199 | * @return The 'maxEnd' for the target user |
|
| 200 | */ |
|
| 201 | 6× | function getMaxEndForUser(address target) internal returns (uint32) { |
| 202 | 8× | uint256 targetDripsAccId = getDripsAccountId(target); |
| 203 | 95× | (, , , , uint32 maxEnd) = drips.streamsState(targetDripsAccId, token); |
| 204 | 5× | return maxEnd; |
| 205 | } |
|
| 206 | ||
| 207 | /** |
|
| 208 | * @notice Get the timestamp on which the current cycle started |
|
| 209 | * @return Timestamp on which the current cycle started |
|
| 210 | */ |
|
| 211 | 8× | function getCurrentCycleStart() internal returns (uint32) { |
| 212 | 8× | uint32 currTimestamp = uint32(block.timestamp); |
| 213 | 34× | return currTimestamp - (currTimestamp % SECONDS_PER_CYCLE); |
| 214 | } |
|
| 215 | ||
| 216 | /** |
|
| 217 | * @notice Get the timestamp on which the current cycle ends |
|
| 218 | * @return Timestamp on which the current cycle ends |
|
| 219 | */ |
|
| 220 | 8× | function getCurrentCycleEnd() internal returns (uint32) { |
| 221 | 38× | return getCurrentCycleStart() + SECONDS_PER_CYCLE - 1; |
| 222 | } |
|
| 223 | ||
| 224 | /** |
|
| 225 | * @notice Get the cycle number for a given timestamp |
|
| 226 | * @param timestamp The timestamp to get the cycle number for |
|
| 227 | * @return timestamp Cycle number for the given timestamp |
|
| 228 | */ |
|
| 229 | 5× | function getCycleFromTimestamp(uint256 timestamp) |
| 230 | internal |
|
| 231 | 1× | returns (uint32) |
| 232 | { |
|
| 233 | 18× | return uint32(timestamp / SECONDS_PER_CYCLE + 1); |
| 234 | } |
|
| 235 | } |
0%
src/echidna/base/EchidnaBase.sol
Lines covered: 0 / 1 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "./EchidnaSetup.sol"; |
|
| 4 | import "./EchidnaStorage.sol"; |
|
| 5 | import "./EchidnaAccounting.sol"; |
|
| 6 | ||
| 7 | /** |
|
| 8 | * @title Mixin grouping together all base contracts |
|
| 9 | * @author Rappie <rappie@perimetersec.io> |
|
| 10 | */ |
|
| 11 | 0 | contract EchidnaBase is |
| 12 | EchidnaSetup, |
|
| 13 | EchidnaStorage, |
|
| 14 | EchidnaAccounting |
|
| 15 | {} |
88%
src/echidna/base/EchidnaConfig.sol
Lines covered: 23 / 26 (88%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import {IERC20, ERC20PresetFixedSupply} from "openzeppelin-contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; |
|
| 4 | import {ManagedProxy} from "src/Managed.sol"; |
|
| 5 | import {Drips, SplitsReceiver, StreamReceiver, StreamConfig, StreamConfigImpl, StreamsHistory} from "src/Drips.sol"; |
|
| 6 | import {AddressDriver} from "src/AddressDriver.sol"; |
|
| 7 | ||
| 8 | import {DripsEchidna} from "../tools/DripsEchidna.sol"; |
|
| 9 | import {AddressDriverEchidna} from "../tools/AddressDriverEchidna.sol"; |
|
| 10 | ||
| 11 | import "../tools/Debugger.sol"; |
|
| 12 | ||
| 13 | /** |
|
| 14 | * @title Mixin containing the configuration for the fuzzing campaign |
|
| 15 | * @author Rappie <rappie@perimetersec.io> |
|
| 16 | */ |
|
| 17 | 0 | contract EchidnaConfig { |
| 18 | // Addresses used for the simulated users |
|
| 19 | 20× | address internal constant ADDRESS_USER0 = address(0x10000); |
| 20 | 20× | address internal constant ADDRESS_USER1 = address(0x20000); |
| 21 | 19× | address internal constant ADDRESS_USER2 = address(0x30000); |
| 22 | 19× | address internal constant ADDRESS_USER3 = address(0x40000); |
| 23 | ||
| 24 | // Mappings from address to account id |
|
| 25 | mapping(address => uint8) internal ADDRESS_TO_ACCOUNT_ID; |
|
| 26 | mapping(address => uint256) internal ADDRESS_TO_DRIPS_ACCOUNT_ID; |
|
| 27 | ||
| 28 | // Variable to store the timestamp when fuzzing starts |
|
| 29 | uint256 internal STARTING_TIMESTAMP; |
|
| 30 | ||
| 31 | // Starting token balance of the simulated users |
|
| 32 | 11× | uint256 internal constant STARTING_BALANCE = 1_000_000_000e18; |
| 33 | ||
| 34 | // Amount of seconds in a Drips cycle |
|
| 35 | 20× | uint32 internal constant SECONDS_PER_CYCLE = 10; |
| 36 | ||
| 37 | // Buffers to be used as fuzzing boundaries |
|
| 38 | 8× | uint32 internal constant CYCLE_FUZZING_BUFFER_CYCLES = 10; |
| 39 | uint32 internal constant CYCLE_FUZZING_BUFFER_SECONDS = |
|
| 40 | 48× | CYCLE_FUZZING_BUFFER_CYCLES * SECONDS_PER_CYCLE; |
| 41 | ||
| 42 | // Maximum amount of streamable funds per second that make sense based |
|
| 43 | // on the starting balance of the simulated users |
|
| 44 | uint160 internal constant MAX_AMOUNT_PER_SEC = |
|
| 45 | 45× | (uint160(STARTING_BALANCE) / uint160(SECONDS_PER_CYCLE)) * 1e9; |
| 46 | ||
| 47 | // Sensible maximum amount of seconds for a stream |
|
| 48 | uint32 internal constant MAX_STREAM_DURATION = CYCLE_FUZZING_BUFFER_SECONDS; |
|
| 49 | ||
| 50 | // Due to the inner workings of the splitting algorithm Drips uses, |
|
| 51 | // it is unpredictable wether a split will be rounded up or down. To |
|
| 52 | // remedy this, allow a tolerance for the expected split amount. |
|
| 53 | 4× | uint256 internal constant SPLIT_ROUNDING_TOLERANCE = 1; |
| 54 | ||
| 55 | // Toggles for certain tests |
|
| 56 | bool internal constant TOGGLE_EXPERIMENTAL_TESTS_ENABLED = true; |
|
| 57 | 6× | bool internal constant TOGGLE_HEAVY_TESTS_ENABLED = true; |
| 58 | 1× | bool internal constant TOGGLE_MAXENDHINTS_ENABLED = true; |
| 59 | ||
| 60 | // Modifier to toggle experimental tests |
|
| 61 | modifier experimental() { |
|
| 62 | if (!TOGGLE_EXPERIMENTAL_TESTS_ENABLED) return; |
|
| 63 | _; |
|
| 64 | } |
|
| 65 | ||
| 66 | // Modifier to toggle performance heavy tests |
|
| 67 | modifier heavy() { |
|
| 68 | 18× | if (!TOGGLE_HEAVY_TESTS_ENABLED) return; |
| 69 | _; |
|
| 70 | } |
|
| 71 | ||
| 72 | 0 | constructor() { |
| 73 | 37× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] = 0; |
| 74 | 37× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] = 64; |
| 75 | 37× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] = 128; |
| 76 | 37× | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3] = 192; |
| 77 | } |
|
| 78 | ||
| 79 | /* |
|
| 80 | * @notice Get the address for a certain account id |
|
| 81 | * @param rawId The raw account id |
|
| 82 | * @return The address for the account id |
|
| 83 | * @dev In this case we have 4 users, spread over the range of 256 (8 bits). |
|
| 84 | */ |
|
| 85 | 21× | function getAccount(uint8 rawId) internal pure returns (address) { |
| 86 | 39× | uint256 id = uint256(rawId) / 64; |
| 87 | ||
| 88 | 33× | if (id == 0) return ADDRESS_USER0; |
| 89 | 33× | if (id == 1) return ADDRESS_USER1; |
| 90 | 33× | if (id == 2) return ADDRESS_USER2; |
| 91 | 30× | if (id == 3) return ADDRESS_USER3; |
| 92 | ||
| 93 | 0 | require(false, "Unknown account ID"); |
| 94 | } |
|
| 95 | } |
96%
src/echidna/base/EchidnaSetup.sol
Lines covered: 27 / 28 (96%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | import "../tools/IHevm.sol"; |
|
| 4 | import "./EchidnaConfig.sol"; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @title Mixin containing the deployment and setup |
|
| 8 | * @author Rappie <rappie@perimetersec.io> |
|
| 9 | */ |
|
| 10 | 0 | contract EchidnaSetup is EchidnaConfig { |
| 11 | 21× | IHevm hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); |
| 12 | ||
| 13 | ERC20PresetFixedSupply token; |
|
| 14 | DripsEchidna drips; |
|
| 15 | AddressDriverEchidna driver; |
|
| 16 | ||
| 17 | 1× | constructor() EchidnaConfig() { |
| 18 | // Deploy ERC20 token |
|
| 19 | 50× | token = new ERC20PresetFixedSupply( |
| 20 | "Test Token", |
|
| 21 | "TEST", |
|
| 22 | 7× | STARTING_BALANCE * 4, |
| 23 | 1× | address(this) |
| 24 | ); |
|
| 25 | ||
| 26 | // Deploy Drips |
|
| 27 | 49× | drips = new DripsEchidna(SECONDS_PER_CYCLE); |
| 28 | 55× | drips.unpause_noModifiers(); |
| 29 | ||
| 30 | // Deploy AddressDriver |
|
| 31 | 80× | uint32 driverId = drips.registerDriver(address(this)); |
| 32 | 51× | driver = new AddressDriverEchidna( |
| 33 | 11× | drips, |
| 34 | 1× | address(0), |
| 35 | 1× | driverId |
| 36 | ); |
|
| 37 | 55× | driver.unpause_noModifiers(); |
| 38 | 74× | drips.updateDriverAddress(driverId, address(driver)); |
| 39 | ||
| 40 | // Set up token balances |
|
| 41 | 78× | token.transfer(ADDRESS_USER0, STARTING_BALANCE); |
| 42 | 61× | hevm.prank(ADDRESS_USER0); |
| 43 | 90× | token.approve(address(driver), type(uint256).max); |
| 44 | 78× | token.transfer(ADDRESS_USER1, STARTING_BALANCE); |
| 45 | 61× | hevm.prank(ADDRESS_USER1); |
| 46 | 90× | token.approve(address(driver), type(uint256).max); |
| 47 | 78× | token.transfer(ADDRESS_USER2, STARTING_BALANCE); |
| 48 | 61× | hevm.prank(ADDRESS_USER2); |
| 49 | 90× | token.approve(address(driver), type(uint256).max); |
| 50 | 78× | token.transfer(ADDRESS_USER3, STARTING_BALANCE); |
| 51 | 61× | hevm.prank(ADDRESS_USER3); |
| 52 | 90× | token.approve(address(driver), type(uint256).max); |
| 53 | ||
| 54 | // Store starting timestamp |
|
| 55 | 6× | STARTING_TIMESTAMP = block.timestamp; |
| 56 | } |
|
| 57 | } |
98%
src/echidna/base/EchidnaStorage.sol
Lines covered: 85 / 86 (98%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | import "./EchidnaSetup.sol"; |
|
| 3 | ||
| 4 | /** |
|
| 5 | * @title Mixin for storing stream receivers and streams history |
|
| 6 | * @author Rappie <rappie@perimetersec.io> |
|
| 7 | */ |
|
| 8 | 0 | contract EchidnaStorage is EchidnaSetup { |
| 9 | error DuplicateError(); |
|
| 10 | ||
| 11 | // Mapping from address to current stream receivers |
|
| 12 | mapping(address => StreamReceiver[]) internal userToStreamReceivers; |
|
| 13 | ||
| 14 | // Mappings from address to full streams history and hashes |
|
| 15 | mapping(address => StreamsHistory[]) internal userToStreamsHistory; |
|
| 16 | mapping(address => bytes32[]) internal userToStreamsHistoryHashes; |
|
| 17 | ||
| 18 | // Mapping from address to current splits receivers |
|
| 19 | mapping(address => SplitsReceiver[]) internal userToSplitsReceivers; |
|
| 20 | ||
| 21 | /** |
|
| 22 | * @notice Update the stream receivers for a given account. |
|
| 23 | * @param sender Account to update |
|
| 24 | * @param receivers New stream receivers |
|
| 25 | * @dev This function will generate a new hash for the stream receivers, |
|
| 26 | * update the stream receivers for the given account, and add a new entry |
|
| 27 | * to the streams history and streams history hashes for the given |
|
| 28 | * account. This is stored for later use. |
|
| 29 | */ |
|
| 30 | 8× | function updateStreamReceivers( |
| 31 | address sender, |
|
| 32 | StreamReceiver[] memory receivers |
|
| 33 | 12× | ) internal { |
| 34 | // Hash the new stream receivers |
|
| 35 | 158× | bytes32 receiversHash = drips.hashStreams(receivers); |
| 36 | ||
| 37 | // Update the stream receivers for 'sender' |
|
| 38 | 50× | delete userToStreamReceivers[sender]; |
| 39 | 46× | for (uint256 i = 0; i < receivers.length; i++) { |
| 40 | 170× | userToStreamReceivers[sender].push(receivers[i]); |
| 41 | } |
|
| 42 | ||
| 43 | // Add a new entry to the streams history |
|
| 44 | 50× | uint256 nextIndex = userToStreamsHistory[sender].length; |
| 45 | 82× | userToStreamsHistory[sender].push(); |
| 46 | 90× | userToStreamsHistory[sender][nextIndex].streamsHash = bytes32(0); |
| 47 | 46× | for (uint256 i = 0; i < receivers.length; i++) { |
| 48 | 180× | userToStreamsHistory[sender][nextIndex].receivers.push( |
| 49 | 30× | receivers[i] |
| 50 | ); |
|
| 51 | } |
|
| 52 | 170× | (, , uint32 updateTime, , uint32 maxEnd) = drips.streamsState( |
| 53 | 10× | getDripsAccountId(sender), |
| 54 | 22× | token |
| 55 | ); |
|
| 56 | 116× | userToStreamsHistory[sender][nextIndex].updateTime = updateTime; |
| 57 | 116× | userToStreamsHistory[sender][nextIndex].maxEnd = maxEnd; |
| 58 | ||
| 59 | // Generate starting hash. If there is no previous history, use 0 |
|
| 60 | 2× | bytes32 startingHash; |
| 61 | 18× | if (nextIndex == 0) { |
| 62 | 10× | startingHash = bytes32(0); |
| 63 | } else { |
|
| 64 | 86× | startingHash = userToStreamsHistoryHashes[sender][nextIndex - 1]; |
| 65 | } |
|
| 66 | ||
| 67 | // Add new entry to the streams history hashes |
|
| 68 | 162× | bytes32 historyHash = drips.hashStreamsHistory( |
| 69 | 2× | startingHash, |
| 70 | 2× | receiversHash, |
| 71 | 2× | updateTime, |
| 72 | 2× | maxEnd |
| 73 | ); |
|
| 74 | 102× | userToStreamsHistoryHashes[sender].push(historyHash); |
| 75 | } |
|
| 76 | ||
| 77 | /** |
|
| 78 | * @notice Update the splits receivers for a given account. |
|
| 79 | * @param sender Account to update |
|
| 80 | * @param receivers New splits receivers |
|
| 81 | */ |
|
| 82 | 12× | function updateSplitsReceivers( |
| 83 | address sender, |
|
| 84 | SplitsReceiver[] memory receivers |
|
| 85 | ) internal { |
|
| 86 | 75× | delete userToSplitsReceivers[sender]; |
| 87 | 57× | for (uint256 i = 0; i < receivers.length; i++) { |
| 88 | 206× | userToSplitsReceivers[sender].push(receivers[i]); |
| 89 | } |
|
| 90 | } |
|
| 91 | ||
| 92 | /** |
|
| 93 | * @notice Get the stream receivers for a given account. |
|
| 94 | * @param sender Account to get stream receivers for |
|
| 95 | * @return Stream receivers for the given account |
|
| 96 | */ |
|
| 97 | 15× | function getStreamReceivers(address sender) |
| 98 | internal |
|
| 99 | 3× | returns (StreamReceiver[] memory) |
| 100 | { |
|
| 101 | 312× | return userToStreamReceivers[sender]; |
| 102 | } |
|
| 103 | ||
| 104 | /** |
|
| 105 | * @notice Get the streams history for a given account. |
|
| 106 | * @param sender Account to get streams history for |
|
| 107 | * @return Streams history for the given account |
|
| 108 | */ |
|
| 109 | 15× | function getStreamsHistory(address sender) |
| 110 | internal |
|
| 111 | 3× | returns (StreamsHistory[] memory) |
| 112 | { |
|
| 113 | 687× | return userToStreamsHistory[sender]; |
| 114 | } |
|
| 115 | ||
| 116 | /** |
|
| 117 | * @notice Get the streams history hashes for a given account. |
|
| 118 | * @param sender Account to get streams history hashes for |
|
| 119 | * @return Streams history hashes for the given account |
|
| 120 | */ |
|
| 121 | 10× | function getStreamsHistoryHashes(address sender) |
| 122 | internal |
|
| 123 | 2× | returns (bytes32[] memory) |
| 124 | { |
|
| 125 | 168× | return userToStreamsHistoryHashes[sender]; |
| 126 | } |
|
| 127 | ||
| 128 | /** |
|
| 129 | * @notice Get the splits receivers for a given account. |
|
| 130 | * @param sender Account to get splits receivers for |
|
| 131 | * @return Splits receivers for the given account |
|
| 132 | */ |
|
| 133 | 15× | function getSplitsReceivers(address sender) |
| 134 | internal |
|
| 135 | 3× | returns (SplitsReceiver[] memory) |
| 136 | { |
|
| 137 | 291× | return userToSplitsReceivers[sender]; |
| 138 | } |
|
| 139 | ||
| 140 | /** |
|
| 141 | * @notice Sort the stream receivers. |
|
| 142 | * @param unsorted Unsorted stream receivers |
|
| 143 | * @return Sorted stream receivers |
|
| 144 | * @dev This function will sort the stream receivers by account ID, |
|
| 145 | * then by config, then by stream ID. It will revert if there are |
|
| 146 | * duplicate stream receivers. |
|
| 147 | */ |
|
| 148 | 18× | function bubbleSortStreamReceivers(StreamReceiver[] memory unsorted) |
| 149 | internal |
|
| 150 | 3× | returns (StreamReceiver[] memory) |
| 151 | { |
|
| 152 | 15× | uint256 n = unsorted.length; |
| 153 | 36× | if (n <= 1) return unsorted; |
| 154 | ||
| 155 | 12× | StreamReceiver[] memory sorted = unsorted; |
| 156 | 87× | for (uint256 i = 0; i < n - 1; i++) { |
| 157 | 108× | for (uint256 j = 0; j < n - i - 1; j++) { |
| 158 | 138× | if (bubbleSortStreamReceiverGT(sorted[j], sorted[j + 1])) { |
| 159 | 54× | StreamReceiver memory temp = sorted[j]; |
| 160 | 120× | sorted[j] = sorted[j + 1]; |
| 161 | 78× | sorted[j + 1] = temp; |
| 162 | } |
|
| 163 | } |
|
| 164 | } |
|
| 165 | 15× | return sorted; |
| 166 | } |
|
| 167 | ||
| 168 | /** |
|
| 169 | * @notice Sort the splits receivers. |
|
| 170 | * @param unsorted Unsorted splits receivers |
|
| 171 | * @return Sorted splits receivers |
|
| 172 | */ |
|
| 173 | 18× | function bubbleSortSplitsReceivers(SplitsReceiver[] memory unsorted) |
| 174 | internal |
|
| 175 | 3× | returns (SplitsReceiver[] memory) |
| 176 | { |
|
| 177 | 15× | uint256 n = unsorted.length; |
| 178 | 35× | if (n <= 1) return unsorted; |
| 179 | ||
| 180 | 8× | SplitsReceiver[] memory sorted = unsorted; |
| 181 | 58× | for (uint256 i = 0; i < n - 1; i++) { |
| 182 | 72× | for (uint256 j = 0; j < n - i - 1; j++) { |
| 183 | 92× | if (bubbleSortSplitsReceiverGT(sorted[j], sorted[j + 1])) { |
| 184 | 36× | SplitsReceiver memory temp = sorted[j]; |
| 185 | 80× | sorted[j] = sorted[j + 1]; |
| 186 | 52× | sorted[j + 1] = temp; |
| 187 | } |
|
| 188 | } |
|
| 189 | } |
|
| 190 | 10× | return sorted; |
| 191 | } |
|
| 192 | ||
| 193 | /** |
|
| 194 | * @notice Compare two stream receivers. |
|
| 195 | * @param a First stream receiver |
|
| 196 | * @param b Second stream receiver |
|
| 197 | * @return True if `a` is greater than `b`, false otherwise |
|
| 198 | * @dev This function will compare two stream receivers by account ID, |
|
| 199 | * then by config. It will revert if there are duplicate stream receivers. |
|
| 200 | */ |
|
| 201 | 21× | function bubbleSortStreamReceiverGT( |
| 202 | StreamReceiver memory a, |
|
| 203 | StreamReceiver memory b |
|
| 204 | 3× | ) internal returns (bool) { |
| 205 | 36× | if (a.accountId != b.accountId) { |
| 206 | 39× | return a.accountId > b.accountId; |
| 207 | } |
|
| 208 | 35× | if (StreamConfig.unwrap(a.config) != StreamConfig.unwrap(b.config)) { |
| 209 | 12× | return |
| 210 | 27× | StreamConfig.unwrap(a.config) > StreamConfig.unwrap(b.config); |
| 211 | } |
|
| 212 | 28× | revert DuplicateError(); |
| 213 | } |
|
| 214 | ||
| 215 | /** |
|
| 216 | * @notice Compare two splits receivers. |
|
| 217 | * @param a First splits receiver |
|
| 218 | * @param b Second splits receiver |
|
| 219 | * @return True if `a` is greater than `b`, false otherwise |
|
| 220 | * @dev This function will compare two splits receivers by account ID. |
|
| 221 | * It will revert if there are duplicate splits receivers. |
|
| 222 | */ |
|
| 223 | 14× | function bubbleSortSplitsReceiverGT( |
| 224 | SplitsReceiver memory a, |
|
| 225 | SplitsReceiver memory b |
|
| 226 | 2× | ) internal returns (bool) { |
| 227 | 24× | if (a.accountId != b.accountId) { |
| 228 | 26× | return a.accountId > b.accountId; |
| 229 | } |
|
| 230 | 28× | revert DuplicateError(); |
| 231 | } |
|
| 232 | ||
| 233 | /** |
|
| 234 | * @notice Get the drips account ID for a given address. |
|
| 235 | * @param account Address to get drips account ID for |
|
| 236 | * @return Drips account ID for the given address |
|
| 237 | * @dev Caches the value to increase performance |
|
| 238 | */ |
|
| 239 | 18× | function getDripsAccountId(address account) internal returns (uint256) { |
| 240 | 72× | if (ADDRESS_TO_DRIPS_ACCOUNT_ID[account] == 0) { |
| 241 | 291× | ADDRESS_TO_DRIPS_ACCOUNT_ID[account] = driver.calcAccountId( |
| 242 | 3× | account |
| 243 | ); |
|
| 244 | } |
|
| 245 | 63× | return ADDRESS_TO_DRIPS_ACCOUNT_ID[account]; |
| 246 | } |
|
| 247 | } |
100%
src/echidna/tools/AddressDriverEchidna.sol
Lines covered: 3 / 3 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | import {AddressDriver} from "src/AddressDriver.sol"; |
|
| 3 | ||
| 4 | import {DripsEchidna} from "./DripsEchidna.sol"; |
|
| 5 | import {ManagedEchidna} from "./ManagedEchidna.sol"; |
|
| 6 | ||
| 7 | /** |
|
| 8 | * @title Wrapper around AddressDriver with minor fuzzing helpers |
|
| 9 | * @author Rappie <rappie@perimetersec.io> |
|
| 10 | */ |
|
| 11 | 311× | contract AddressDriverEchidna is AddressDriver, ManagedEchidna { |
| 12 | 33× | constructor( |
| 13 | DripsEchidna drips_, |
|
| 14 | address forwarder, |
|
| 15 | uint32 driverId_ |
|
| 16 | 3× | ) AddressDriver(drips_, forwarder, driverId_) {} |
| 17 | } |
0%
src/echidna/tools/Debugger.sol
Lines covered: 0 / 1 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | ||
| 3 | 0 | library Debugger { |
| 4 | event Debug(string debugString); |
|
| 5 | event Debug(string description, string data); |
|
| 6 | event Debug(string prefix, string description, string data); |
|
| 7 | event Debug(string description, bytes32 data); |
|
| 8 | event Debug(string prefix, string description, bytes32 data); |
|
| 9 | event Debug(string description, uint256 data); |
|
| 10 | event Debug(string prefix, string description, uint256 data); |
|
| 11 | event Debug(string description, int256 data); |
|
| 12 | event Debug(string prefix, string description, int256 data); |
|
| 13 | event Debug(string description, address data); |
|
| 14 | event Debug(string prefix, string description, address data); |
|
| 15 | event Debug(string description, bool data); |
|
| 16 | event Debug(string prefix, string description, bool data); |
|
| 17 | ||
| 18 | function log(string memory debugString) internal { |
|
| 19 | emit Debug(debugString); |
|
| 20 | } |
|
| 21 | ||
| 22 | function log(string memory description, string memory data) internal { |
|
| 23 | emit Debug(description, data); |
|
| 24 | } |
|
| 25 | ||
| 26 | function log( |
|
| 27 | string memory prefix, |
|
| 28 | string memory description, |
|
| 29 | string memory data |
|
| 30 | ) internal { |
|
| 31 | emit Debug(prefix, description, data); |
|
| 32 | } |
|
| 33 | ||
| 34 | function log(string memory description, bytes32 data) internal { |
|
| 35 | emit Debug(description, data); |
|
| 36 | } |
|
| 37 | ||
| 38 | function log( |
|
| 39 | string memory prefix, |
|
| 40 | string memory description, |
|
| 41 | bytes32 data |
|
| 42 | ) internal { |
|
| 43 | emit Debug(prefix, description, data); |
|
| 44 | } |
|
| 45 | ||
| 46 | function log(string memory description, uint256 data) internal { |
|
| 47 | emit Debug(description, data); |
|
| 48 | } |
|
| 49 | ||
| 50 | function log( |
|
| 51 | string memory prefix, |
|
| 52 | string memory description, |
|
| 53 | uint256 data |
|
| 54 | ) internal { |
|
| 55 | emit Debug(prefix, description, data); |
|
| 56 | } |
|
| 57 | ||
| 58 | function log(string memory description, int256 data) internal { |
|
| 59 | emit Debug(description, data); |
|
| 60 | } |
|
| 61 | ||
| 62 | function log( |
|
| 63 | string memory prefix, |
|
| 64 | string memory description, |
|
| 65 | int256 data |
|
| 66 | ) internal { |
|
| 67 | emit Debug(prefix, description, data); |
|
| 68 | } |
|
| 69 | ||
| 70 | function log(string memory description, address data) internal { |
|
| 71 | emit Debug(description, data); |
|
| 72 | } |
|
| 73 | ||
| 74 | function log( |
|
| 75 | string memory prefix, |
|
| 76 | string memory description, |
|
| 77 | address data |
|
| 78 | ) internal { |
|
| 79 | emit Debug(prefix, description, data); |
|
| 80 | } |
|
| 81 | ||
| 82 | function log(string memory description, bool data) internal { |
|
| 83 | emit Debug(description, data); |
|
| 84 | } |
|
| 85 | ||
| 86 | function log( |
|
| 87 | string memory prefix, |
|
| 88 | string memory description, |
|
| 89 | bool data |
|
| 90 | ) internal { |
|
| 91 | emit Debug(prefix, description, data); |
|
| 92 | } |
|
| 93 | } |
100%
src/echidna/tools/DripsEchidna.sol
Lines covered: 10 / 10 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | import {IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; |
|
| 3 | import {Drips} from "src/Drips.sol"; |
|
| 4 | ||
| 5 | import {ManagedEchidna} from "./ManagedEchidna.sol"; |
|
| 6 | ||
| 7 | /** |
|
| 8 | * @title Wrapper around Drips with minor fuzzing helpers |
|
| 9 | * @author Rappie <rappie@perimetersec.io> |
|
| 10 | */ |
|
| 11 | 718× | contract DripsEchidna is Drips, ManagedEchidna { |
| 12 | 32× | constructor(uint32 cycleSecs_) Drips(cycleSecs_) {} |
| 13 | ||
| 14 | /** |
|
| 15 | * @notice Get the amtDelta for a given user and cycle |
|
| 16 | * @param accountId The account to get the amtDelta for |
|
| 17 | * @param erc20 The ERC20 token |
|
| 18 | * @param cycle The cycle to get the amtDelta for |
|
| 19 | * @return The amtDelta for this cycle and the next cycle |
|
| 20 | */ |
|
| 21 | 49× | function getAmtDeltaForCycle( |
| 22 | uint256 accountId, |
|
| 23 | IERC20 erc20, |
|
| 24 | uint32 cycle |
|
| 25 | 2× | ) public view returns (int128, int128) { |
| 26 | // Manually calculate the storage slot because it is a private variable |
|
| 27 | // in Streams |
|
| 28 | 1× | StreamsStorage storage streamsStorage; |
| 29 | 24× | bytes32 slot = _erc1967Slot("eip1967.streams.storage"); |
| 30 | assembly { |
|
| 31 | 3× | streamsStorage.slot := slot |
| 32 | } |
|
| 33 | ||
| 34 | // Return the amtDelta for the given cycle |
|
| 35 | 36× | StreamsState storage state = streamsStorage.states[erc20][accountId]; |
| 36 | 6× | mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
| 37 | 68× | return (amtDeltas[cycle].thisCycle, amtDeltas[cycle].nextCycle); |
| 38 | } |
|
| 39 | } |
0%
src/echidna/tools/ManagedEchidna.sol
Lines covered: 0 / 5 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | import {Managed} from "src/Managed.sol"; |
|
| 3 | ||
| 4 | /** |
|
| 5 | * @title Wrapper around Managed with minor fuzzing helpers |
|
| 6 | * @author Rappie <rappie@perimetersec.io> |
|
| 7 | */ |
|
| 8 | 0 | contract ManagedEchidna is Managed { |
| 9 | 0 | constructor() Managed() {} |
| 10 | ||
| 11 | /** |
|
| 12 | * @notice Helper function to unpause the contract as any user |
|
| 13 | */ |
|
| 14 | 0 | function unpause_noModifiers() public { |
| 15 | 0 | _managedStorage().isPaused = false; |
| 16 | 0 | emit Unpaused(msg.sender); |
| 17 | } |
|
| 18 | } |
100%
test/recon/CryticTester.sol
Lines covered: 1 / 1 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import {Properties} from "./Properties.sol"; |
|
| 5 | ||
| 6 | // echidna test/recon/CryticTester.sol --contract CryticTester --config echidna.yaml |
|
| 7 | 689× | contract CryticTester is Properties {} |
88%
test/recon/Properties.sol
Lines covered: 8 / 9 (88%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.20; |
|
| 3 | ||
| 4 | import {Echidna} from "src/echidna/Echidna.sol"; |
|
| 5 | ||
| 6 | /// @notice scfuzzbench adaptation layer: canary checks on top of the |
|
| 7 | /// Perimeter Drips fuzzing suite. Assertion failures are surfaced the same |
|
| 8 | /// way for Echidna, Medusa, Foundry, and Recon (AssertionFailed event + |
|
| 9 | /// assert), and dedup to the emitting function name across fuzzers. |
|
| 10 | abstract contract Properties is Echidna { |
|
| 11 | string internal constant ASSERTION_CANARY = "!!! canary assertion"; |
|
| 12 | string internal constant INVARIANT_CANARY_GLOBAL_INVARIANT_FAILURE = "Canary invariant"; |
|
| 13 | ||
| 14 | event AssertionFailed(string reason); |
|
| 15 | ||
| 16 | 4× | function t(bool b, string memory reason) internal { |
| 17 | 4× | if (!b) { |
| 18 | 17× | emit AssertionFailed(reason); |
| 19 | 6× | assert(false); |
| 20 | } |
|
| 21 | } |
|
| 22 | ||
| 23 | 6× | function invariant_canary() public returns (bool) { |
| 24 | 21× | t(false, INVARIANT_CANARY_GLOBAL_INVARIANT_FAILURE); |
| 25 | 0 | return true; |
| 26 | } |
|
| 27 | ||
| 28 | 22× | function assert_canary_ASSERTION_CANARY(uint256 entropy) public { |
| 29 | 24× | t(entropy > 0, ASSERTION_CANARY); |
| 30 | } |
|
| 31 | } |