Recon Coverage Report 90% coverage
Files 34 files
    Files
    34
    Lines
    6829
    Coverage
    90%
    1773 / 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
        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
            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
            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
        ) 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
        ) 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
        ) 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
                _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
        function _revert(bytes memory returndata, string memory errorMessage) private pure {
    232
            // Look for revert reason and bubble it up if present
    233
            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
                    let returndata_size := mload(returndata)
    238
                    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
        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
        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
        ) public whenNotPaused returns (int128 realBalanceDelta) {
    135 12×
            return _setStreamsAndTransfer(
    136
                drips,
    137
                _callerAccountId(),
    138
                erc20,
    139
                currReceivers,
    140
                balanceDelta,
    141
                newReceivers,
    142
                maxEndHint1,
    143
                maxEndHint2,
    144
                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
        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
        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
        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
        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
            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
        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
        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
        function _increaseSplitsBalance(IERC20 erc20, uint128 amt) internal {
    261
            _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
        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
            returns (uint32 cycles)
    338
        {
    339
            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
            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
            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
        ) 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
        ) 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
            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
            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
            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
            onlyDriver(accountId)
    537
            returns (uint128 amt)
    538
        {
    539
            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
            onlyDriver(accountId)
    561
        {
    562 14×
            if (amt != 0) _increaseSplitsBalance(erc20, amt);
    563
            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
                bytes32 streamsHash,
    584
                bytes32 streamsHistoryHash,
    585
                uint32 updateTime,
    586
                uint128 balance,
    587
                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
        ) 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
        ) 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
            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
        ) 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
            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
            assembly {
    790
                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
        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
        function _collectAndTransfer(Drips drips, uint256 accountId, IERC20 erc20, address transferTo)
    28
            internal
    29
            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
        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
        ) 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
            _;
    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
            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
        uint256 internal constant _MAX_SPLITS_RECEIVERS = 200;
    24
        /// @notice The total splits weight of an account.
    25
        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
        constructor(bytes32 splitsStorageSlot) {
    100
            _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
        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
        function _splitResult(uint256 accountId, SplitsReceiver[] memory currReceivers, uint128 amount)
    127
            internal
    128
            view
    129
            returns (uint128 collectableAmt, uint128 splitAmt)
    130
        {
    131
            _assertCurrSplits(accountId, currReceivers);
    132
            if (amount == 0) {
    133
                return (0, 0);
    134
            }
    135
            unchecked {
    136
                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
                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
            returns (uint128 collectableAmt, uint128 splitAmt)
    159
        {
    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
            unchecked {
    170
                uint256 splitsWeight = 0;
    171 55×
                for (uint256 i = 0; i < currReceivers.length; i++) {
    172 48×
                    splitsWeight += currReceivers[i].weight;
    173
                    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
        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
        function _give(uint256 accountId, uint256 receiver, IERC20 erc20, uint128 amt) internal {
    214
            _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
        function _assertSplitsValid(SplitsReceiver[] memory receivers, bytes32 receiversHash) private {
    250
            unchecked {
    251 14×
                require(receivers.length <= _MAX_SPLITS_RECEIVERS, "Too many splits receivers");
    252
                uint256 totalWeight = 0;
    253
                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
                    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
            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
            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
            assembly {
    309
                splitsStorage.slot := slot
    310
            }
    311
        }
    312
    }
    99% src/Streams.sol
    Lines covered: 414 / 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
            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
            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
            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
        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
        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
        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
        constructor(uint32 cycleSecs, bytes32 streamsStorageSlot) {
    276
            require(cycleSecs > 1, "Cycle length too low");
    277 11×
            _cycleSecs = cycleSecs;
    278 35×
            _minAmtPerSec = (_AMT_PER_SEC_MULTIPLIER + cycleSecs - 1) / cycleSecs;
    279
            _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
            returns (uint128 receivedAmt)
    293
        {
    294
            uint32 receivableCycles;
    295
            uint32 fromCycle;
    296
            uint32 toCycle;
    297
            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
                uint128 receivedAmt,
    334
                uint32 receivableCycles,
    335
                uint32 fromCycle,
    336
                uint32 toCycle,
    337
                int128 amtPerCycle
    338
            )
    339
        {
    340
            unchecked {
    341 28×
                (fromCycle, toCycle) = _receivableStreamsCyclesRange(accountId, erc20);
    342 26×
                if (toCycle - fromCycle > maxCycles) {
    343
                    receivableCycles = toCycle - fromCycle - maxCycles;
    344
                    toCycle -= receivableCycles;
    345
                }
    346
                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
        function _receivableStreamsCycles(uint256 accountId, IERC20 erc20)
    366
            internal
    367
            view
    368
            returns (uint32 cycles)
    369
        {
    370
            unchecked {
    371 12×
                (uint32 fromCycle, uint32 toCycle) = _receivableStreamsCyclesRange(accountId, erc20);
    372
                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
            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
                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
        ) internal returns (uint128 amt) {
    417 24×
            unchecked {
    418
                uint256 squeezedNum;
    419
                uint256[] memory squeezedRevIdxs;
    420
                bytes32[] memory historyHashes;
    421
                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
                uint128 amt,
    476
                uint256 squeezedNum,
    477
                uint256[] memory squeezedRevIdxs,
    478
                bytes32[] memory historyHashes,
    479
                uint256 currCycleConfigs
    480
            )
    481
        {
    482
            {
    483 117×
                StreamsState storage sender = _streamsStorage().states[erc20][senderId];
    484
                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
                currCycleConfigs = 1;
    489
                // slither-disable-next-line timestamp
    490 75×
                if (sender.updateTime >= _currCycleStart()) {
    491 51×
                    currCycleConfigs = sender.lastUpdatedCycleConfigs;
    492
                }
    493
            }
    494 150×
            squeezedRevIdxs = new uint256[](streamsHistory.length);
    495
            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 18×
                            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
        ) 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
        ) 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
                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
                        idxCap = idxMid;
    572
                    }
    573
                }
    574 21×
                uint32 updateTime = historyEntry.updateTime;
    575 21×
                uint32 maxEnd = historyEntry.maxEnd;
    576
                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
                bytes32 streamsHash,
    603
                bytes32 streamsHistoryHash,
    604
                uint32 updateTime,
    605
                uint128 balance,
    606
                uint32 maxEnd
    607
            )
    608
        {
    609 78×
            StreamsState storage state = _streamsStorage().states[erc20][accountId];
    610 22×
            return (
    611
                state.streamsHash,
    612
                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
        ) 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
        ) private view returns (uint128 balance) {
    658
            unchecked {
    659
                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
                        receiver: receiver,
    664
                        updateTime: lastUpdate,
    665
                        maxEnd: maxEnd,
    666
                        startCap: lastUpdate,
    667
                        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
        ) 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
                uint128 newBalance;
    730
                uint32 newMaxEnd;
    731
                {
    732 32×
                    uint32 currMaxEnd = state.maxEnd;
    733
                    int128 currBalance = int128(
    734
                        _calcBalance(
    735 40×
                            state.balance, lastUpdate, currMaxEnd, currReceivers, _currTimestamp()
    736
                        )
    737
                    );
    738
                    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
                    _updateReceiverStates(
    748 46×
                        _streamsStorage().states[erc20],
    749
                        currReceivers,
    750
                        lastUpdate,
    751
                        currMaxEnd,
    752
                        newReceivers,
    753
                        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
        ) 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
                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
                    enoughEnd = end;
    846
                } else {
    847
                    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
        ) private view returns (bool isEnough) {
    865
            unchecked {
    866
                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
                        end = maxEnd;
    876
                    }
    877 22×
                    spent += _streamedAmt(amtPerSec, start, end);
    878 14×
                    if (spent > balance) {
    879 20×
                        return false;
    880
                    }
    881
                }
    882
                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
            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
        ) 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
            returns (uint256 amtPerSec, uint256 start, uint256 end)
    957
        {
    958
            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
            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
        ) 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
        ) private {
    1035
            uint256 currIdx = 0;
    1036
            uint256 newIdx = 0;
    1037 24×
            while (true) {
    1038 14×
                bool pickCurr = currIdx < currReceivers.length;
    1039
                // slither-disable-next-line uninitialized-local
    1040
                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
                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
                    if (
    1056 30×
                        currRecv.accountId != newRecv.accountId
    1057 44×
                            || currRecv.config.amtPerSec() != newRecv.config.amtPerSec()
    1058
                    ) {
    1059 16×
                        pickCurr = _isOrdered(currRecv, newRecv);
    1060
                        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 52×
                    if (currStartCycle > newStartCycle && state.nextReceivableCycle > newStartCycle) {
    1086 23×
                        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
            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
        ) private pure returns (uint32 start, uint32 end_) {
    1151 30×
            start = receiver.config.start();
    1152
            // slither-disable-start timestamp
    1153 24×
            if (start == 0) {
    1154
                start = updateTime;
    1155
            }
    1156
            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
                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
        {
    1185
            // slither-disable-next-line incorrect-equality,timestamp
    1186 27×
            if (start == end) {
    1187
                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
            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
            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
            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
            assembly {
    1311
                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
        ) public {
    21
            address from = getAccount(fromAccId);
    22
            address to = getAccount(toAccId);
    23
    24
            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
        ) public {
    41
            address from = getAccount(fromAccId);
    42
    43
            uint128 min = 1000;
    44 79×
            uint128 max = uint128(token.balanceOf(from));
    45 32×
            uint128 clampedAmount = min + (amount % (max - min + 1));
    46
    47
            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
            returns (uint128)
    61
        {
    62 16×
            address target = getAccount(targetAccId);
    63 16×
            uint256 targetDripsAccId = getDripsAccountId(target);
    64
    65 176×
            uint128 receivedAmt = drips.receiveStreams(
    66
                targetDripsAccId,
    67 22×
                token,
    68
                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
            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
                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
            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
            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
            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
        ) public {
    22
            address from = getAccount(fromAccId);
    23
            address to = getAccount(toAccId);
    24
    25
            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
            address target = getAccount(targetAccId);
    42
            uint256 targetDripsAccId = getDripsAccountId(target);
    43
    44 91×
            uint128 splittableBefore = drips.splittable(targetDripsAccId, token);
    45
            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
            address target = getAccount(targetAccId);
    63
            uint256 targetDripsAccId = getDripsAccountId(target);
    64
    65
            try
    66 92×
                drips.receiveStreams(targetDripsAccId, token, type(uint32).max)
    67
            {} catch {
    68
                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
        ) public {
    82
            address target = getAccount(targetAccId);
    83
            uint256 targetDripsAccId = getDripsAccountId(target);
    84
    85 11×
            require(maxCycles > 0);
    86
    87 87×
            uint128 receivable = drips.receiveStreamsResult(
    88
                targetDripsAccId,
    89 11×
                token,
    90
                maxCycles
    91
            );
    92 79×
            uint32 receivableCycles = drips.receivableStreamsCycles(
    93
                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
        {
    111
            address target = getAccount(targetAccId);
    112
            uint256 targetDripsAccId = getDripsAccountId(target);
    113
    114 87×
            uint128 receivable = drips.receiveStreamsResult(
    115
                targetDripsAccId,
    116 11×
                token,
    117
                maxCycles
    118
            );
    119
    120 88×
            uint128 received = drips.receiveStreams(
    121
                targetDripsAccId,
    122 11×
                token,
    123
                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
            address from = getAccount(fromAccId);
    136
            address to = getAccount(toAccId);
    137
    138
            uint256 fromDripsAccId = getDripsAccountId(from);
    139
    140 91×
            uint128 colBalBefore = drips.collectable(fromDripsAccId, token);
    141 79×
            uint256 tokenBalBefore = token.balanceOf(to);
    142
    143
            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
            address from = getAccount(fromAccId);
    159
            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
        {
    38
            address target = getAccount(targetAccId);
    39
    40
            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
            uint256 dripsBalancesTotal = getDripsBalancesTotalForAllUsers();
    56
    57
            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
            receiveStreamsSplitAndCollectToSelf(
    78 27×
                ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]
    79
            );
    80
            receiveStreamsSplitAndCollectToSelf(
    81 27×
                ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]
    82
            );
    83
            receiveStreamsSplitAndCollectToSelf(
    84 27×
                ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]
    85
            );
    86
            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
                user1Balance +
    103
                user2Balance +
    104
                user3Balance;
    105
    106
            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
        function invariantWithdrawAllTokensShouldNotRevert() public heavy {
    114
            try
    115 46×
                EchidnaInvariantTests(address(this)).invariantWithdrawAllTokens()
    116
            {} catch {
    117
                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
            address target = getAccount(targetAccId);
    127
            uint256 targetDripsAccId = getDripsAccountId(target);
    128
    129
            uint32 maxEnd = getMaxEndForAllUsers();
    130
    131
            uint32 firstCycle = getCycleFromTimestamp(STARTING_TIMESTAMP);
    132 10×
            uint32 lastCycle = getCycleFromTimestamp(maxEnd);
    133
    134 27×
            require(maxEnd > 0, "no cycles");
    135 10×
            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
            int256 sumAmtDelta = 0;
    141
    142 30×
            for (uint32 cycle = firstCycle; cycle <= lastCycle; cycle++) {
    143 83×
                (int128 thisCycle, int128 nextCycle) = drips.getAmtDeltaForCycle(
    144
                    targetDripsAccId,
    145 11×
                    token,
    146
                    cycle
    147
                );
    148
    149 12×
                sumAmtDelta += int256(thisCycle);
    150 12×
                sumAmtDelta += int256(nextCycle);
    151
            }
    152
    153
            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
        function _setSplits(
    17
            uint8 senderAccId,
    18
            SplitsReceiver[] memory unsortedReceivers
    19
        ) internal {
    20 24×
            address sender = getAccount(senderAccId);
    21
    22 21×
            SplitsReceiver[] memory newReceivers = bubbleSortSplitsReceivers(
    23
                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
        ) 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
                accountId: receiverDripsAccId,
    50
                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
                accountId: receiverDripsAccId,
    94
                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
        ) public {
    121 16×
            address sender = getAccount(senderAccId);
    122
    123
            // sum all the existing weights
    124
            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
            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
            address target = getAccount(targetAccId);
    22
            uint256 targetDripsAccId = getDripsAccountId(target);
    23
    24 91×
            uint128 splittableBefore = drips.splittable(targetDripsAccId, token);
    25
    26
            // check if we are splitting to ourselves
    27
            uint32 splitToSelfWeight;
    28
            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
            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
                assert(
    50 21×
                    splittableAfter == splittableBefore - splitAmt - collectableAmt
    51
                );
    52
            } 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
                    splitAmt -
    59
                    collectableAmt +
    60
                    splitToSelfAmount;
    61
    62
                // calculate difference between expected and actual
    63 12×
                int256 difference = int256(uint256(splittableAfter)) -
    64
                    int256(uint256(expectedSplittableAfter));
    65
    66
                // check if difference is within tolerance
    67
                assert(
    68 14×
                    difference >= -int256(SPLIT_ROUNDING_TOLERANCE) &&
    69
                        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
            address target = getAccount(targetAccId);
    80
            uint256 targetDripsAccId = getDripsAccountId(target);
    81
    82 91×
            uint128 colBalBefore = drips.collectable(targetDripsAccId, token);
    83
            (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
            address target = getAccount(targetAccId);
    96
            uint256 targetDripsAccId = getDripsAccountId(target);
    97
    98 91×
            uint128 amountToBeSplit = drips.splittable(targetDripsAccId, token);
    99
            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
                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
                    expectedAfter =
    144 27×
                        splittableBefore[i] -
    145
                        amountToBeSplit +
    146 15×
                        amounts[i];
    147
                }
    148
    149
                // calculate difference between expected and actual
    150 26×
                int256 difference = int256(uint256(splittableAfter[i])) -
    151
                    int256(uint256(expectedAfter));
    152
    153
                // check if difference is within tolerance
    154
                assert(
    155 14×
                    difference >= -int256(SPLIT_ROUNDING_TOLERANCE) &&
    156
                        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
            address target = getAccount(targetAccId);
    167
            uint256 targetDripsAccId = getDripsAccountId(target);
    168
    169 91×
            uint128 splittable = drips.splittable(targetDripsAccId, token);
    170
    171 83×
            (uint128 collectableAmtView, uint128 splitAmtView) = drips.splitResult(
    172
                targetDripsAccId,
    173
                getSplitsReceivers(target),
    174
                splittable
    175
            );
    176
    177 10×
            (uint128 collectableAmtActual, uint128 splitAmtActual) = split(
    178
                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
            address target = getAccount(targetAccId);
    191
            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
            try
    210 54×
                EchidnaSplitsHelpers(address(this)).setSplitsWithClamping(
    211
                    senderAccId,
    212
                    receiverAccId,
    213
                    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
                    senderAccId,
    234
                    receiverAccId,
    235
                    weight
    236
                )
    237
            {} catch (bytes memory reason) {
    238
                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
        ) 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
                receiverDripsAccId,
    32 33×
                token,
    33
                senderDripsAccId,
    34
                historyHash,
    35
                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
            returns (uint128)
    50
        {
    51
            return
    52 12×
                _squeeze(
    53
                    receiverAccId,
    54
                    senderAccId,
    55
                    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
        ) 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
            (
    83
                bytes32 historyHash,
    84
                StreamsHistory[] memory history
    85
            ) = getFuzzedStreamsHistory(
    86
                    senderAccId,
    87
                    hashIndex,
    88
                    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
            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
                targetAccId,
    110 54×
                ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]
    111
            );
    112 10×
            squeezeWithDefaultHistory(
    113
                targetAccId,
    114 54×
                ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]
    115
            );
    116 10×
            squeezeWithDefaultHistory(
    117
                targetAccId,
    118 54×
                ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]
    119
            );
    120 10×
            squeezeWithDefaultHistory(
    121
                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
            squeezeAllSenders(targetAccId);
    135
            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
        ) 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
                    ? false
    179
                    : 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
            address receiver = getAccount(receiverAccId);
    25
            address sender = getAccount(senderAccId);
    26
    27
            uint256 receiverDripsAccId = getDripsAccountId(receiver);
    28
    29
            uint128 squeezableBefore = getSqueezableAmount(sender, receiver);
    30 91×
            uint128 splittableBefore = drips.splittable(receiverDripsAccId, token);
    31
    32
            uint128 squeezedAmt = squeezeWithDefaultHistory(
    33
                receiverAccId,
    34
                senderAccId
    35
            );
    36
    37
            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
        {
    60
            address receiver = getAccount(receiverAccId);
    61
            address sender = getAccount(senderAccId);
    62
    63
            uint128 squeezable = getSqueezableAmount(sender, receiver);
    64
            uint128 squeezed = squeezeWithDefaultHistory(
    65
                receiverAccId,
    66
                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
            address target = getAccount(targetAccId);
    78
    79
            // store the current squeezable and receivable amount
    80
            uint128 squeezable = getTotalSqueezableAmountForUser(target);
    81
            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
            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
        ) public {
    113
            address receiver = getAccount(receiverAccId);
    114
            address sender = getAccount(senderAccId);
    115
    116
            uint128 squeezableBefore = getSqueezableAmount(sender, receiver);
    117
    118
            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
            uint128 squeezedAmt = _squeeze(
    125
                receiverAccId,
    126
                senderAccId,
    127
                bytes32(0),
    128
                history
    129
            );
    130
    131
            uint128 squeezableAfter = getSqueezableAmount(sender, receiver);
    132
    133
            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
        ) external {
    151
            address receiver = getAccount(receiverAccId);
    152
            address sender = getAccount(senderAccId);
    153
    154
            uint128 amount0 = squeezeWithFuzzedHistory(
    155
                receiverAccId,
    156
                senderAccId,
    157
                hashIndex,
    158
                receiversRandomSeed
    159
            );
    160
    161
            uint128 amount1 = squeezeWithFuzzedHistory(
    162
                receiverAccId,
    163
                senderAccId,
    164
                hashIndex,
    165
                receiversRandomSeed
    166
            );
    167
    168
            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
        ) external {
    189
            address receiver = getAccount(receiverAccId);
    190
            address sender = getAccount(senderAccId);
    191
    192
            uint128 squeezableBefore = getSqueezableAmount(sender, receiver);
    193
    194
            setStreams(
    195
                receiverAccId,
    196
                senderAccId,
    197
                amountPerSec,
    198
                startTime,
    199
                duration,
    200
                balanceDelta
    201
            );
    202
    203
            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
        ) external {
    218
            address receiver = getAccount(receiverAccId);
    219
            address sender = getAccount(senderAccId);
    220
    221
            uint128 squeezableBefore = getSqueezableAmount(sender, receiver);
    222
    223
            setStreamBalanceWithdrawAll(senderAccId);
    224
    225
            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
            try
    241 68×
                EchidnaSqueezeHelpers(address(this)).squeezeWithDefaultHistory(
    242
                    receiverAccId,
    243
                    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
        ) public {
    264
            address sender = getAccount(senderAccId);
    265 22×
            require(
    266
                getStreamsHistory(sender).length >= 2,
    267
                "need at least 2 history entries"
    268
            );
    269
    270
            try
    271 70×
                EchidnaSqueezeHelpers(address(this)).squeezeWithFuzzedHistory(
    272
                    receiverAccId,
    273
                    senderAccId,
    274
                    hashIndex,
    275
                    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
        ) internal returns (int128) {
    36 21×
            StreamReceiver[] memory newReceivers = bubbleSortStreamReceivers(
    37
                unsortedNewReceivers
    38
            );
    39
    40 176×
            hevm.prank(from);
    41 177×
            int128 realBalanceDelta = driver.setStreams(
    42 22×
                token,
    43
                currReceivers,
    44
                balanceDelta,
    45
                newReceivers,
    46 22×
                maxEndHint1,
    47 22×
                maxEndHint2,
    48
                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
        ) 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
                StreamConfigImpl.create(
    81
                    0, // streamId is arbitrary and can be ignored
    82
                    amountPerSec,
    83
                    startTime,
    84
                    duration
    85
                )
    86
            );
    87
    88 14×
            int128 realBalanceDelta = _setStreams(
    89
                from,
    90 10×
                getStreamReceivers(from),
    91
                balanceDelta,
    92
                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
        ) 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
                fromAccId,
    128
                toAccId,
    129
                amountPerSec,
    130
                startTime,
    131
                duration,
    132
                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
        ) 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
                    0, // streamId is arbitrary and can be ignored
    163
                    amountPerSec,
    164
                    startTime,
    165
                    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
                from,
    179
                oldReceivers,
    180
                balanceDelta,
    181
                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
        ) 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
            return
    214
                addStream(
    215
                    fromAccId,
    216
                    toAccId,
    217
                    amountPerSec,
    218
                    startTime,
    219
                    duration,
    220
                    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
        ) public {
    238
            address receiver = getAccount(toAccId);
    239
            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
            amountPerSec =
    245
                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
            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
            returns (int128)
    298
        {
    299 16×
            address target = getAccount(targetAccId);
    300
    301 14×
            int128 realBalanceDelta = _setStreams(
    302
                target,
    303 10×
                getStreamReceivers(target),
    304
                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
        ) 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
            returns (int128)
    337
        {
    338 24×
            address target = getAccount(targetAccId);
    339 24×
            uint256 targetDripsAccId = getDripsAccountId(target);
    340
    341 18×
            int128 realBalanceDelta = _setStreams(
    342
                target,
    343 15×
                getStreamReceivers(target),
    344
                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
            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
            returns (uint160)
    369
        {
    370
            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
            uint32 minStartTime;
    385 16×
            if (CYCLE_FUZZING_BUFFER_SECONDS >= block.timestamp) {
    386 0
                minStartTime = 1;
    387
            } else {
    388
                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
            returns (int128)
    419
        {
    420 35×
            if (balanceDelta > 0) {
    421
                balanceDelta =
    422 21×
                    balanceDelta %
    423 249×
                    (int128(uint128(token.balanceOf(from))) + 1);
    424
            } else {
    425 18×
                balanceDelta = balanceDelta % int128(uint128(STARTING_BALANCE));
    426
            }
    427
            return balanceDelta;
    428
        }
    429
    }
    92% src/echidna/EchidnaStreamsTests.sol
    Lines covered: 109 / 118 (92%)
    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
        {
    19
            address target = getAccount(targetAccId);
    20
            uint256 targetDripsAccId = getDripsAccountId(target);
    21
    22 79×
            uint256 tokenBalanceBefore = token.balanceOf(target);
    23 81×
            uint128 streamBalanceBefore = drips.balanceAt(
    24
                targetDripsAccId,
    25 11×
                token,
    26
                getStreamReceivers(target),
    27
                uint32(block.timestamp)
    28
            );
    29
    30
            int128 realBalanceDelta = setStreamBalance(targetAccId, balanceDelta);
    31
    32 79×
            uint256 tokenBalanceAfter = token.balanceOf(target);
    33 81×
            uint128 streamBalanceAfter = drips.balanceAt(
    34
                targetDripsAccId,
    35 11×
                token,
    36
                getStreamReceivers(target),
    37
                uint32(block.timestamp)
    38
            );
    39
    40 11×
            if (balanceDelta >= 0) {
    41 10×
                assert(realBalanceDelta == balanceDelta);
    42
            } else {
    43
                assert(realBalanceDelta <= 0);
    44 11×
                assert(realBalanceDelta >= balanceDelta);
    45
            }
    46
    47
            assert(
    48
                int256(tokenBalanceAfter) ==
    49 10×
                    int256(tokenBalanceBefore) - realBalanceDelta
    50
            );
    51
            assert(
    52
                int128(streamBalanceAfter) ==
    53
                    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
            address from = getAccount(fromAccId);
    72
            address to = getAccount(toAccId);
    73
            uint256 fromDripsAccId = getDripsAccountId(from);
    74
            uint256 toDripsAccId = getDripsAccountId(to);
    75
    76
            amtPerSecAdded = clampAmountPerSec(amtPerSecAdded);
    77
    78
            // the timestamps we are comparing
    79
            uint256 currentTimestamp = block.timestamp;
    80 16×
            uint256 futureTimestamp = getCurrentCycleEnd() + 1;
    81
    82
            // retrieve initial balances
    83
            uint128 balanceInitial = getStreamBalanceForUser(
    84
                from,
    85
                uint32(block.timestamp)
    86
            );
    87
            uint128 receivableInitial = getReceivableAmountForAllUsers();
    88
    89
            // look at balances in the future if we wouldnt do anything
    90 62×
            hevm.warp(futureTimestamp);
    91
            uint128 balanceBaseline = getStreamBalanceForUser(
    92
                from,
    93
                uint32(block.timestamp)
    94
            );
    95
            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
            balanceAdded = uint128(clampBalanceDelta(int128(balanceAdded), from));
    102 22×
            require(balanceAdded / amtPerSecAdded >= SECONDS_PER_CYCLE);
    103
            addStream(
    104
                fromAccId,
    105
                toAccId,
    106
                amtPerSecAdded,
    107
                0,
    108
                0,
    109
                int128(balanceAdded)
    110
            );
    111
    112
            // retrieve balances after adding stream
    113
            uint128 balanceBefore = getStreamBalanceForUser(
    114
                from,
    115
                uint32(block.timestamp)
    116
            );
    117
            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
            uint128 balanceAfter = getStreamBalanceForUser(
    124
                from,
    125
                uint32(block.timestamp)
    126
            );
    127
            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
                balanceAfter -
    141
                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
            try
    165 72×
                EchidnaStreamsHelpers(address(this)).setStreamsWithClamping(
    166
                    fromAccId,
    167
                    toAccId,
    168
                    amountPerSec,
    169
                    startTime,
    170
                    duration,
    171
                    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 27×
        function testAddStreamShouldNotRevert(
    188
            uint8 fromAccId,
    189
            uint8 toAccId,
    190
            uint160 amountPerSec,
    191
            uint32 startTime,
    192
            uint32 duration,
    193
            int128 balanceDelta
    194
        ) public {
    195
            try
    196 72×
                EchidnaStreamsHelpers(address(this)).addStreamWithClamping(
    197
                    fromAccId,
    198
                    toAccId,
    199
                    amountPerSec,
    200
                    startTime,
    201
                    duration,
    202
                    balanceDelta
    203
                )
    204 0
            {} catch (bytes memory reason) {
    205 0
                bytes4 errorSelector = bytes4(reason);
    206 0
                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
        ) public {
    223
            address target = getAccount(targetAccId);
    224 14×
            require(getStreamReceivers(target).length > 0);
    225
    226
            try
    227 53×
                EchidnaStreamsHelpers(address(this)).removeStream(
    228
                    targetAccId,
    229
                    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
            try
    246 53×
                EchidnaStreamsHelpers(address(this)).setStreamBalanceWithClamping(
    247
                    targetAccId,
    248
                    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
            try
    263 67×
                EchidnaStreamsHelpers(address(this)).setStreamBalanceWithdrawAll(
    264
                    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
        function getDripsBalancesTotalForAllUsers() internal returns (uint256) {
    18
            uint256 user0Total = getDripsBalancesTotalForUser(ADDRESS_USER0);
    19
            uint256 user1Total = getDripsBalancesTotalForUser(ADDRESS_USER1);
    20
            uint256 user2Total = getDripsBalancesTotalForUser(ADDRESS_USER2);
    21
            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
        function getDripsBalancesTotalForUser(address target)
    35
            internal
    36
            returns (uint256)
    37
        {
    38
            uint256 targetDripsAccId = getDripsAccountId(target);
    39
    40
            uint128 balance = getCurrentStreamBalanceForUser(target);
    41
            uint128 squeezable = getTotalSqueezableAmountForUser(target);
    42
            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
            returns (uint128)
    60
        {
    61 16×
            uint256 targetDripsAccId = getDripsAccountId(target);
    62
    63
            uint128 balance;
    64
            try
    65 158×
                drips.balanceAt(
    66
                    targetDripsAccId,
    67 22×
                    token,
    68 10×
                    getStreamReceivers(target),
    69
                    timestamp
    70
                )
    71
            returns (uint128 _balance) {
    72
                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
        function getCurrentStreamBalanceForUser(address target)
    87
            internal
    88
            returns (uint128)
    89
        {
    90
            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
        function getReceivableAmountForAllUsers() internal returns (uint128) {
    100
            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
            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
            returns (uint128)
    118
        {
    119 174×
            uint128 receivable = drips.receiveStreamsResult(
    120 10×
                getDripsAccountId(target),
    121 22×
                token,
    122
                type(uint32).max
    123
            );
    124
            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
        function getTotalSqueezableAmountForUser(address target)
    136
            internal
    137
            returns (uint128)
    138
        {
    139
            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
            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
            returns (uint128)
    160
        {
    161 16×
            uint256 senderDripsAccId = getDripsAccountId(sender);
    162 16×
            uint256 receiverDripsAccId = getDripsAccountId(receiver);
    163
    164 164×
            uint128 amount = drips.squeezeStreamsResult(
    165
                receiverDripsAccId,
    166 22×
                token,
    167
                senderDripsAccId,
    168
                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
        function getMaxEndForAllUsers() internal returns (uint32) {
    182
            uint32 maxMaxEnd;
    183
    184
            uint32 maxEndUser0 = getMaxEndForUser(ADDRESS_USER0);
    185 14×
            if (maxEndUser0 > maxMaxEnd) maxMaxEnd = maxEndUser0;
    186
            uint32 maxEndUser1 = getMaxEndForUser(ADDRESS_USER1);
    187 14×
            if (maxEndUser1 > maxMaxEnd) maxMaxEnd = maxEndUser1;
    188
            uint32 maxEndUser2 = getMaxEndForUser(ADDRESS_USER2);
    189 14×
            if (maxEndUser2 > maxMaxEnd) maxMaxEnd = maxEndUser2;
    190
            uint32 maxEndUser3 = getMaxEndForUser(ADDRESS_USER3);
    191 14×
            if (maxEndUser3 > maxMaxEnd) maxMaxEnd = maxEndUser3;
    192
    193
            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
        function getMaxEndForUser(address target) internal returns (uint32) {
    202
            uint256 targetDripsAccId = getDripsAccountId(target);
    203 95×
            (, , , , uint32 maxEnd) = drips.streamsState(targetDripsAccId, token);
    204
            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
        function getCurrentCycleStart() internal returns (uint32) {
    212
            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
        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
        function getCycleFromTimestamp(uint256 timestamp)
    230
            internal
    231
            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
        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
        uint256 internal constant SPLIT_ROUNDING_TOLERANCE = 1;
    54
    55
        // Toggles for certain tests
    56
        bool internal constant TOGGLE_EXPERIMENTAL_TESTS_ENABLED = true;
    57
        bool internal constant TOGGLE_HEAVY_TESTS_ENABLED = true;
    58
        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
        constructor() EchidnaConfig() {
    18
            // Deploy ERC20 token
    19 50×
            token = new ERC20PresetFixedSupply(
    20
                "Test Token",
    21
                "TEST",
    22
                STARTING_BALANCE * 4,
    23
                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
                address(0),
    35
                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
            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
        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
            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
                startingHash,
    70
                receiversHash,
    71
                updateTime,
    72
                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
            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
            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
            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
            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
            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
            returns (SplitsReceiver[] memory)
    176
        {
    177 15×
            uint256 n = unsorted.length;
    178 35×
            if (n <= 1) return unsorted;
    179
    180
            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
        ) 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
        ) 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
                    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
        ) 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
        ) public view returns (int128, int128) {
    26
            // Manually calculate the storage slot because it is a private variable
    27
            // in Streams
    28
            StreamsStorage storage streamsStorage;
    29 24×
            bytes32 slot = _erc1967Slot("eip1967.streams.storage");
    30
            assembly {
    31
                streamsStorage.slot := slot
    32
            }
    33
    34
            // Return the amtDelta for the given cycle
    35 36×
            StreamsState storage state = streamsStorage.states[erc20][accountId];
    36
            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
        function t(bool b, string memory reason) internal {
    17
            if (!b) {
    18 17×
                emit AssertionFailed(reason);
    19
                assert(false);
    20
            }
    21
        }
    22
    23
        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
    }