Files
47
Total Lines
7965
Coverage
88.0%
1814 / 2061 lines
Actions
0.0%
lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol
Lines covered: 0 / 1 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1967.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | /** |
| 7 | * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. |
| 8 | * |
| 9 | * _Available since v4.8.3._ |
| 10 | */ |
| 11 | interface IERC1967 { |
| 12 | /** |
| 13 | * @dev Emitted when the implementation is upgraded. |
| 14 | */ |
| 15 | event Upgraded(address indexed implementation); |
| 16 | |
| 17 | /** |
| 18 | * @dev Emitted when the admin account has changed. |
| 19 | */ |
| 20 | event AdminChanged(address previousAdmin, address newAdmin); |
| 21 | |
| 22 | /** |
| 23 | * @dev Emitted when the beacon is changed. |
| 24 | */ |
| 25 | event BeaconUpgraded(address indexed beacon); |
| 26 | } |
| 27 |
0.0%
lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol
Lines covered: 0 / 0 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | /** |
| 7 | * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified |
| 8 | * proxy whose upgrades are fully controlled by the current implementation. |
| 9 | */ |
| 10 | interface IERC1822Proxiable { |
| 11 | /** |
| 12 | * @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation |
| 13 | * address. |
| 14 | * |
| 15 | * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks |
| 16 | * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this |
| 17 | * function revert if invoked through a proxy. |
| 18 | */ |
| 19 | function proxiableUUID() external view returns (bytes32); |
| 20 | } |
| 21 |
87.0%
lib/openzeppelin-contracts/contracts/metatx/ERC2771Context.sol
Lines covered: 7 / 8 (87.0%)
| 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 | _trustedForwarder = trustedForwarder; |
| 18 | } |
| 19 | |
| 20 | function isTrustedForwarder(address forwarder) public view virtual returns (bool) { |
| 21 | return forwarder == _trustedForwarder; |
| 22 | } |
| 23 | |
| 24 | function _msgSender() internal view virtual override returns (address sender) { |
| 25 | 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 | sender := shr(96, calldataload(sub(calldatasize(), 20))) |
| 30 | } |
| 31 | } else { |
| 32 | 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 | } |
| 44 |
0.0%
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol
Lines covered: 0 / 5 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | import "../Proxy.sol"; |
| 7 | import "./ERC1967Upgrade.sol"; |
| 8 | |
| 9 | /** |
| 10 | * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an |
| 11 | * implementation address that can be changed. This address is stored in storage in the location specified by |
| 12 | * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the |
| 13 | * implementation behind the proxy. |
| 14 | */ |
| 15 | contract ERC1967Proxy is Proxy, ERC1967Upgrade { |
| 16 | /** |
| 17 | * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. |
| 18 | * |
| 19 | * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded |
| 20 | * function call, and allows initializing the storage of the proxy like a Solidity constructor. |
| 21 | */ |
| 22 | constructor(address _logic, bytes memory _data) payable { |
| 23 | _upgradeToAndCall(_logic, _data, false); |
| 24 | } |
| 25 | |
| 26 | /** |
| 27 | * @dev Returns the current implementation address. |
| 28 | */ |
| 29 | function _implementation() internal view virtual override returns (address impl) { |
| 30 | return ERC1967Upgrade._getImplementation(); |
| 31 | } |
| 32 | } |
| 33 |
0.0%
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol
Lines covered: 0 / 30 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Upgrade.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.2; |
| 5 | |
| 6 | import "../beacon/IBeacon.sol"; |
| 7 | import "../../interfaces/IERC1967.sol"; |
| 8 | import "../../interfaces/draft-IERC1822.sol"; |
| 9 | import "../../utils/Address.sol"; |
| 10 | import "../../utils/StorageSlot.sol"; |
| 11 | |
| 12 | /** |
| 13 | * @dev This abstract contract provides getters and event emitting update functions for |
| 14 | * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. |
| 15 | * |
| 16 | * _Available since v4.1._ |
| 17 | */ |
| 18 | abstract contract ERC1967Upgrade is IERC1967 { |
| 19 | // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 |
| 20 | bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; |
| 21 | |
| 22 | /** |
| 23 | * @dev Storage slot with the address of the current implementation. |
| 24 | * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is |
| 25 | * validated in the constructor. |
| 26 | */ |
| 27 | bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; |
| 28 | |
| 29 | /** |
| 30 | * @dev Returns the current implementation address. |
| 31 | */ |
| 32 | function _getImplementation() internal view returns (address) { |
| 33 | return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * @dev Stores a new address in the EIP1967 implementation slot. |
| 38 | */ |
| 39 | function _setImplementation(address newImplementation) private { |
| 40 | require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); |
| 41 | StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; |
| 42 | } |
| 43 | |
| 44 | /** |
| 45 | * @dev Perform implementation upgrade |
| 46 | * |
| 47 | * Emits an {Upgraded} event. |
| 48 | */ |
| 49 | function _upgradeTo(address newImplementation) internal { |
| 50 | _setImplementation(newImplementation); |
| 51 | emit Upgraded(newImplementation); |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * @dev Perform implementation upgrade with additional setup call. |
| 56 | * |
| 57 | * Emits an {Upgraded} event. |
| 58 | */ |
| 59 | function _upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { |
| 60 | _upgradeTo(newImplementation); |
| 61 | if (data.length > 0 || forceCall) { |
| 62 | Address.functionDelegateCall(newImplementation, data); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. |
| 68 | * |
| 69 | * Emits an {Upgraded} event. |
| 70 | */ |
| 71 | function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { |
| 72 | // Upgrades from old implementations will perform a rollback test. This test requires the new |
| 73 | // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing |
| 74 | // this special case will break upgrade paths from old UUPS implementation to new ones. |
| 75 | if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { |
| 76 | _setImplementation(newImplementation); |
| 77 | } else { |
| 78 | try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { |
| 79 | require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); |
| 80 | } catch { |
| 81 | revert("ERC1967Upgrade: new implementation is not UUPS"); |
| 82 | } |
| 83 | _upgradeToAndCall(newImplementation, data, forceCall); |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * @dev Storage slot with the admin of the contract. |
| 89 | * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is |
| 90 | * validated in the constructor. |
| 91 | */ |
| 92 | bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; |
| 93 | |
| 94 | /** |
| 95 | * @dev Returns the current admin. |
| 96 | */ |
| 97 | function _getAdmin() internal view returns (address) { |
| 98 | return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * @dev Stores a new address in the EIP1967 admin slot. |
| 103 | */ |
| 104 | function _setAdmin(address newAdmin) private { |
| 105 | require(newAdmin != address(0), "ERC1967: new admin is the zero address"); |
| 106 | StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * @dev Changes the admin of the proxy. |
| 111 | * |
| 112 | * Emits an {AdminChanged} event. |
| 113 | */ |
| 114 | function _changeAdmin(address newAdmin) internal { |
| 115 | emit AdminChanged(_getAdmin(), newAdmin); |
| 116 | _setAdmin(newAdmin); |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. |
| 121 | * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. |
| 122 | */ |
| 123 | bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; |
| 124 | |
| 125 | /** |
| 126 | * @dev Returns the current beacon. |
| 127 | */ |
| 128 | function _getBeacon() internal view returns (address) { |
| 129 | return StorageSlot.getAddressSlot(_BEACON_SLOT).value; |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * @dev Stores a new beacon in the EIP1967 beacon slot. |
| 134 | */ |
| 135 | function _setBeacon(address newBeacon) private { |
| 136 | require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract"); |
| 137 | require( |
| 138 | Address.isContract(IBeacon(newBeacon).implementation()), |
| 139 | "ERC1967: beacon implementation is not a contract" |
| 140 | ); |
| 141 | StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does |
| 146 | * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). |
| 147 | * |
| 148 | * Emits a {BeaconUpgraded} event. |
| 149 | */ |
| 150 | function _upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { |
| 151 | _setBeacon(newBeacon); |
| 152 | emit BeaconUpgraded(newBeacon); |
| 153 | if (data.length > 0 || forceCall) { |
| 154 | Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 |
0.0%
lib/openzeppelin-contracts/contracts/proxy/Proxy.sol
Lines covered: 0 / 14 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | /** |
| 7 | * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM |
| 8 | * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to |
| 9 | * be specified by overriding the virtual {_implementation} function. |
| 10 | * |
| 11 | * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a |
| 12 | * different contract through the {_delegate} function. |
| 13 | * |
| 14 | * The success and return data of the delegated call will be returned back to the caller of the proxy. |
| 15 | */ |
| 16 | abstract contract Proxy { |
| 17 | /** |
| 18 | * @dev Delegates the current call to `implementation`. |
| 19 | * |
| 20 | * This function does not return to its internal call site, it will return directly to the external caller. |
| 21 | */ |
| 22 | function _delegate(address implementation) internal virtual { |
| 23 | assembly { |
| 24 | // Copy msg.data. We take full control of memory in this inline assembly |
| 25 | // block because it will not return to Solidity code. We overwrite the |
| 26 | // Solidity scratch pad at memory position 0. |
| 27 | calldatacopy(0, 0, calldatasize()) |
| 28 | |
| 29 | // Call the implementation. |
| 30 | // out and outsize are 0 because we don't know the size yet. |
| 31 | let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) |
| 32 | |
| 33 | // Copy the returned data. |
| 34 | returndatacopy(0, 0, returndatasize()) |
| 35 | |
| 36 | switch result |
| 37 | // delegatecall returns 0 on error. |
| 38 | case 0 { |
| 39 | revert(0, returndatasize()) |
| 40 | } |
| 41 | default { |
| 42 | return(0, returndatasize()) |
| 43 | } |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function |
| 49 | * and {_fallback} should delegate. |
| 50 | */ |
| 51 | function _implementation() internal view virtual returns (address); |
| 52 | |
| 53 | /** |
| 54 | * @dev Delegates the current call to the address returned by `_implementation()`. |
| 55 | * |
| 56 | * This function does not return to its internal call site, it will return directly to the external caller. |
| 57 | */ |
| 58 | function _fallback() internal virtual { |
| 59 | _beforeFallback(); |
| 60 | _delegate(_implementation()); |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other |
| 65 | * function in the contract matches the call data. |
| 66 | */ |
| 67 | fallback() external payable virtual { |
| 68 | _fallback(); |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data |
| 73 | * is empty. |
| 74 | */ |
| 75 | receive() external payable virtual { |
| 76 | _fallback(); |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` |
| 81 | * call, or as part of the Solidity `fallback` or `receive` functions. |
| 82 | * |
| 83 | * If overridden should call `super._beforeFallback()`. |
| 84 | */ |
| 85 | function _beforeFallback() internal virtual {} |
| 86 | } |
| 87 |
0.0%
lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol
Lines covered: 0 / 0 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | /** |
| 7 | * @dev This is the interface that {BeaconProxy} expects of its beacon. |
| 8 | */ |
| 9 | interface IBeacon { |
| 10 | /** |
| 11 | * @dev Must return an address that can be used as a delegate call target. |
| 12 | * |
| 13 | * {BeaconProxy} will check that this address is a contract. |
| 14 | */ |
| 15 | function implementation() external view returns (address); |
| 16 | } |
| 17 |
8.0%
lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol
Lines covered: 1 / 12 (8.0%)
| 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 | 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 | require(address(this) != __self, "Function must be called through delegatecall"); |
| 34 | 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 | 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 | function proxiableUUID() external view virtual override notDelegated returns (bytes32) { |
| 56 | 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 | function upgradeTo(address newImplementation) public virtual onlyProxy { |
| 69 | _authorizeUpgrade(newImplementation); |
| 70 | _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 | function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy { |
| 84 | _authorizeUpgrade(newImplementation); |
| 85 | _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 | } |
| 100 |
61.0%
lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol
Lines covered: 47 / 77 (61.0%)
| 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 | 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 | constructor(string memory name_, string memory symbol_) { |
| 55 | _name = name_; |
| 56 | _symbol = symbol_; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * @dev Returns the name of the token. |
| 61 | */ |
| 62 | function name() public view virtual override returns (string memory) { |
| 63 | return _name; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * @dev Returns the symbol of the token, usually a shorter version of the |
| 68 | * name. |
| 69 | */ |
| 70 | function symbol() public view virtual override returns (string memory) { |
| 71 | 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 | function decimals() public view virtual override returns (uint8) { |
| 88 | return 18; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * @dev See {IERC20-totalSupply}. |
| 93 | */ |
| 94 | function totalSupply() public view virtual override returns (uint256) { |
| 95 | return _totalSupply; |
| 96 | } |
| 97 | |
| 98 | /** |
| 99 | * @dev See {IERC20-balanceOf}. |
| 100 | */ |
| 101 | function balanceOf(address account) public view virtual override returns (uint256) { |
| 102 | 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 | function transfer(address to, uint256 amount) public virtual override returns (bool) { |
| 114 | address owner = _msgSender(); |
| 115 | _transfer(owner, to, amount); |
| 116 | return true; |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * @dev See {IERC20-allowance}. |
| 121 | */ |
| 122 | function allowance(address owner, address spender) public view virtual override returns (uint256) { |
| 123 | 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 | function approve(address spender, uint256 amount) public virtual override returns (bool) { |
| 137 | address owner = _msgSender(); |
| 138 | _approve(owner, spender, amount); |
| 139 | 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 | function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { |
| 159 | address spender = _msgSender(); |
| 160 | _spendAllowance(from, spender, amount); |
| 161 | _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 | function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { |
| 178 | address owner = _msgSender(); |
| 179 | _approve(owner, spender, allowance(owner, spender) + addedValue); |
| 180 | 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 | function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { |
| 198 | address owner = _msgSender(); |
| 199 | uint256 currentAllowance = allowance(owner, spender); |
| 200 | require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); |
| 201 | unchecked { |
| 202 | _approve(owner, spender, currentAllowance - subtractedValue); |
| 203 | } |
| 204 | |
| 205 | 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 | function _transfer(address from, address to, uint256 amount) internal virtual { |
| 223 | require(from != address(0), "ERC20: transfer from the zero address"); |
| 224 | require(to != address(0), "ERC20: transfer to the zero address"); |
| 225 | |
| 226 | _beforeTokenTransfer(from, to, amount); |
| 227 | |
| 228 | uint256 fromBalance = _balances[from]; |
| 229 | require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); |
| 230 | unchecked { |
| 231 | _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 | _balances[to] += amount; |
| 235 | } |
| 236 | |
| 237 | emit Transfer(from, to, amount); |
| 238 | |
| 239 | _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 | function _mint(address account, uint256 amount) internal virtual { |
| 252 | require(account != address(0), "ERC20: mint to the zero address"); |
| 253 | |
| 254 | _beforeTokenTransfer(address(0), account, amount); |
| 255 | |
| 256 | _totalSupply += amount; |
| 257 | unchecked { |
| 258 | // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. |
| 259 | _balances[account] += amount; |
| 260 | } |
| 261 | emit Transfer(address(0), account, amount); |
| 262 | |
| 263 | _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 | function _burn(address account, uint256 amount) internal virtual { |
| 278 | require(account != address(0), "ERC20: burn from the zero address"); |
| 279 | |
| 280 | _beforeTokenTransfer(account, address(0), amount); |
| 281 | |
| 282 | uint256 accountBalance = _balances[account]; |
| 283 | require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); |
| 284 | unchecked { |
| 285 | _balances[account] = accountBalance - amount; |
| 286 | // Overflow not possible: amount <= accountBalance <= totalSupply. |
| 287 | _totalSupply -= amount; |
| 288 | } |
| 289 | |
| 290 | emit Transfer(account, address(0), amount); |
| 291 | |
| 292 | _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 | function _approve(address owner, address spender, uint256 amount) internal virtual { |
| 309 | require(owner != address(0), "ERC20: approve from the zero address"); |
| 310 | require(spender != address(0), "ERC20: approve to the zero address"); |
| 311 | |
| 312 | _allowances[owner][spender] = amount; |
| 313 | 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 | function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { |
| 325 | uint256 currentAllowance = allowance(owner, spender); |
| 326 | if (currentAllowance != type(uint256).max) { |
| 327 | require(currentAllowance >= amount, "ERC20: insufficient allowance"); |
| 328 | unchecked { |
| 329 | _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 | 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 | function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} |
| 365 | } |
| 366 |
0.0%
lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol
Lines covered: 0 / 0 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | /** |
| 7 | * @dev Interface of the ERC20 standard as defined in the EIP. |
| 8 | */ |
| 9 | interface IERC20 { |
| 10 | /** |
| 11 | * @dev Emitted when `value` tokens are moved from one account (`from`) to |
| 12 | * another (`to`). |
| 13 | * |
| 14 | * Note that `value` may be zero. |
| 15 | */ |
| 16 | event Transfer(address indexed from, address indexed to, uint256 value); |
| 17 | |
| 18 | /** |
| 19 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by |
| 20 | * a call to {approve}. `value` is the new allowance. |
| 21 | */ |
| 22 | event Approval(address indexed owner, address indexed spender, uint256 value); |
| 23 | |
| 24 | /** |
| 25 | * @dev Returns the amount of tokens in existence. |
| 26 | */ |
| 27 | function totalSupply() external view returns (uint256); |
| 28 | |
| 29 | /** |
| 30 | * @dev Returns the amount of tokens owned by `account`. |
| 31 | */ |
| 32 | function balanceOf(address account) external view returns (uint256); |
| 33 | |
| 34 | /** |
| 35 | * @dev Moves `amount` tokens from the caller's account to `to`. |
| 36 | * |
| 37 | * Returns a boolean value indicating whether the operation succeeded. |
| 38 | * |
| 39 | * Emits a {Transfer} event. |
| 40 | */ |
| 41 | function transfer(address to, uint256 amount) external returns (bool); |
| 42 | |
| 43 | /** |
| 44 | * @dev Returns the remaining number of tokens that `spender` will be |
| 45 | * allowed to spend on behalf of `owner` through {transferFrom}. This is |
| 46 | * zero by default. |
| 47 | * |
| 48 | * This value changes when {approve} or {transferFrom} are called. |
| 49 | */ |
| 50 | function allowance(address owner, address spender) external view returns (uint256); |
| 51 | |
| 52 | /** |
| 53 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. |
| 54 | * |
| 55 | * Returns a boolean value indicating whether the operation succeeded. |
| 56 | * |
| 57 | * IMPORTANT: Beware that changing an allowance with this method brings the risk |
| 58 | * that someone may use both the old and the new allowance by unfortunate |
| 59 | * transaction ordering. One possible solution to mitigate this race |
| 60 | * condition is to first reduce the spender's allowance to 0 and set the |
| 61 | * desired value afterwards: |
| 62 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 |
| 63 | * |
| 64 | * Emits an {Approval} event. |
| 65 | */ |
| 66 | function approve(address spender, uint256 amount) external returns (bool); |
| 67 | |
| 68 | /** |
| 69 | * @dev Moves `amount` tokens from `from` to `to` using the |
| 70 | * allowance mechanism. `amount` is then deducted from the caller's |
| 71 | * allowance. |
| 72 | * |
| 73 | * Returns a boolean value indicating whether the operation succeeded. |
| 74 | * |
| 75 | * Emits a {Transfer} event. |
| 76 | */ |
| 77 | function transferFrom(address from, address to, uint256 amount) external returns (bool); |
| 78 | } |
| 79 |
0.0%
lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol
Lines covered: 0 / 5 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Burnable.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | import "../ERC20.sol"; |
| 7 | import "../../../utils/Context.sol"; |
| 8 | |
| 9 | /** |
| 10 | * @dev Extension of {ERC20} that allows token holders to destroy both their own |
| 11 | * tokens and those that they have an allowance for, in a way that can be |
| 12 | * recognized off-chain (via event analysis). |
| 13 | */ |
| 14 | abstract contract ERC20Burnable is Context, ERC20 { |
| 15 | /** |
| 16 | * @dev Destroys `amount` tokens from the caller. |
| 17 | * |
| 18 | * See {ERC20-_burn}. |
| 19 | */ |
| 20 | function burn(uint256 amount) public virtual { |
| 21 | _burn(_msgSender(), amount); |
| 22 | } |
| 23 | |
| 24 | /** |
| 25 | * @dev Destroys `amount` tokens from `account`, deducting from the caller's |
| 26 | * allowance. |
| 27 | * |
| 28 | * See {ERC20-_burn} and {ERC20-allowance}. |
| 29 | * |
| 30 | * Requirements: |
| 31 | * |
| 32 | * - the caller must have allowance for ``accounts``'s tokens of at least |
| 33 | * `amount`. |
| 34 | */ |
| 35 | function burnFrom(address account, uint256 amount) public virtual { |
| 36 | _spendAllowance(account, _msgSender(), amount); |
| 37 | _burn(account, amount); |
| 38 | } |
| 39 | } |
| 40 |
0.0%
lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol
Lines covered: 0 / 0 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | import "../IERC20.sol"; |
| 7 | |
| 8 | /** |
| 9 | * @dev Interface for the optional metadata functions from the ERC20 standard. |
| 10 | * |
| 11 | * _Available since v4.1._ |
| 12 | */ |
| 13 | interface IERC20Metadata is IERC20 { |
| 14 | /** |
| 15 | * @dev Returns the name of the token. |
| 16 | */ |
| 17 | function name() external view returns (string memory); |
| 18 | |
| 19 | /** |
| 20 | * @dev Returns the symbol of the token. |
| 21 | */ |
| 22 | function symbol() external view returns (string memory); |
| 23 | |
| 24 | /** |
| 25 | * @dev Returns the decimals places of the token. |
| 26 | */ |
| 27 | function decimals() external view returns (uint8); |
| 28 | } |
| 29 |
0.0%
lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol
Lines covered: 0 / 0 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/IERC20Permit.sol) |
| 3 | |
| 4 | pragma solidity ^0.8.0; |
| 5 | |
| 6 | /** |
| 7 | * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in |
| 8 | * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. |
| 9 | * |
| 10 | * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by |
| 11 | * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't |
| 12 | * need to send a transaction, and thus is not required to hold Ether at all. |
| 13 | */ |
| 14 | interface IERC20Permit { |
| 15 | /** |
| 16 | * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, |
| 17 | * given ``owner``'s signed approval. |
| 18 | * |
| 19 | * IMPORTANT: The same issues {IERC20-approve} has related to transaction |
| 20 | * ordering also apply here. |
| 21 | * |
| 22 | * Emits an {Approval} event. |
| 23 | * |
| 24 | * Requirements: |
| 25 | * |
| 26 | * - `spender` cannot be the zero address. |
| 27 | * - `deadline` must be a timestamp in the future. |
| 28 | * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` |
| 29 | * over the EIP712-formatted function arguments. |
| 30 | * - the signature must use ``owner``'s current nonce (see {nonces}). |
| 31 | * |
| 32 | * For more information on the signature format, see the |
| 33 | * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP |
| 34 | * section]. |
| 35 | */ |
| 36 | function permit( |
| 37 | address owner, |
| 38 | address spender, |
| 39 | uint256 value, |
| 40 | uint256 deadline, |
| 41 | uint8 v, |
| 42 | bytes32 r, |
| 43 | bytes32 s |
| 44 | ) external; |
| 45 | |
| 46 | /** |
| 47 | * @dev Returns the current nonce for `owner`. This value must be |
| 48 | * included whenever a signature is generated for {permit}. |
| 49 | * |
| 50 | * Every successful call to {permit} increases ``owner``'s nonce by one. This |
| 51 | * prevents a signature from being used multiple times. |
| 52 | */ |
| 53 | function nonces(address owner) external view returns (uint256); |
| 54 | |
| 55 | /** |
| 56 | * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. |
| 57 | */ |
| 58 | // solhint-disable-next-line func-name-mixedcase |
| 59 | function DOMAIN_SEPARATOR() external view returns (bytes32); |
| 60 | } |
| 61 |
100.0%
lib/openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol
Lines covered: 3 / 3 (100.0%)
| 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 | contract ERC20PresetFixedSupply is ERC20Burnable { |
| 22 | /** |
| 23 | * @dev Mints `initialSupply` amount of token and transfers them to `owner`. |
| 24 | * |
| 25 | * See {ERC20-constructor}. |
| 26 | */ |
| 27 | constructor(string memory name, string memory symbol, uint256 initialSupply, address owner) ERC20(name, symbol) { |
| 28 | _mint(owner, initialSupply); |
| 29 | } |
| 30 | } |
| 31 |
87.0%
lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
Lines covered: 7 / 8 (87.0%)
| 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 | 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 | function safeTransfer(IERC20 token, address to, uint256 value) internal { |
| 27 | _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 | function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { |
| 35 | _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 | 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 | bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); |
| 123 | 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 | } |
| 144 |
62.0%
lib/openzeppelin-contracts/contracts/utils/Address.sol
Lines covered: 18 / 29 (62.0%)
| 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 | 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 | 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 | 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 | function functionCall( |
| 100 | address target, |
| 101 | bytes memory data, |
| 102 | string memory errorMessage |
| 103 | ) internal returns (bytes memory) { |
| 104 | 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 | function functionCallWithValue( |
| 129 | address target, |
| 130 | bytes memory data, |
| 131 | uint256 value, |
| 132 | string memory errorMessage |
| 133 | ) internal returns (bytes memory) { |
| 134 | require(address(this).balance >= value, "Address: insufficient balance for call"); |
| 135 | (bool success, bytes memory returndata) = target.call{value: value}(data); |
| 136 | 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 | function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { |
| 171 | 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 | function functionDelegateCall( |
| 181 | address target, |
| 182 | bytes memory data, |
| 183 | string memory errorMessage |
| 184 | ) internal returns (bytes memory) { |
| 185 | (bool success, bytes memory returndata) = target.delegatecall(data); |
| 186 | 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 | function verifyCallResultFromTarget( |
| 196 | address target, |
| 197 | bool success, |
| 198 | bytes memory returndata, |
| 199 | string memory errorMessage |
| 200 | ) internal view returns (bytes memory) { |
| 201 | if (success) { |
| 202 | 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 | require(isContract(target), "Address: call to non-contract"); |
| 206 | } |
| 207 | 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 | revert(errorMessage); |
| 242 | } |
| 243 | } |
| 244 | } |
| 245 |
100.0%
lib/openzeppelin-contracts/contracts/utils/Context.sol
Lines covered: 2 / 2 (100.0%)
| 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 | function _msgSender() internal view virtual returns (address) { |
| 18 | return msg.sender; |
| 19 | } |
| 20 | |
| 21 | function _msgData() internal view virtual returns (bytes calldata) { |
| 22 | return msg.data; |
| 23 | } |
| 24 | } |
| 25 |
0.0%
lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol
Lines covered: 0 / 5 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (utils/StorageSlot.sol) |
| 3 | // This file was procedurally generated from scripts/generate/templates/StorageSlot.js. |
| 4 | |
| 5 | pragma solidity ^0.8.0; |
| 6 | |
| 7 | /** |
| 8 | * @dev Library for reading and writing primitive types to specific storage slots. |
| 9 | * |
| 10 | * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. |
| 11 | * This library helps with reading and writing to such slots without the need for inline assembly. |
| 12 | * |
| 13 | * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. |
| 14 | * |
| 15 | * Example usage to set ERC1967 implementation slot: |
| 16 | * ```solidity |
| 17 | * contract ERC1967 { |
| 18 | * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; |
| 19 | * |
| 20 | * function _getImplementation() internal view returns (address) { |
| 21 | * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; |
| 22 | * } |
| 23 | * |
| 24 | * function _setImplementation(address newImplementation) internal { |
| 25 | * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); |
| 26 | * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; |
| 27 | * } |
| 28 | * } |
| 29 | * ``` |
| 30 | * |
| 31 | * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ |
| 32 | * _Available since v4.9 for `string`, `bytes`._ |
| 33 | */ |
| 34 | library StorageSlot { |
| 35 | struct AddressSlot { |
| 36 | address value; |
| 37 | } |
| 38 | |
| 39 | struct BooleanSlot { |
| 40 | bool value; |
| 41 | } |
| 42 | |
| 43 | struct Bytes32Slot { |
| 44 | bytes32 value; |
| 45 | } |
| 46 | |
| 47 | struct Uint256Slot { |
| 48 | uint256 value; |
| 49 | } |
| 50 | |
| 51 | struct StringSlot { |
| 52 | string value; |
| 53 | } |
| 54 | |
| 55 | struct BytesSlot { |
| 56 | bytes value; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * @dev Returns an `AddressSlot` with member `value` located at `slot`. |
| 61 | */ |
| 62 | function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { |
| 63 | /// @solidity memory-safe-assembly |
| 64 | assembly { |
| 65 | r.slot := slot |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * @dev Returns an `BooleanSlot` with member `value` located at `slot`. |
| 71 | */ |
| 72 | function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { |
| 73 | /// @solidity memory-safe-assembly |
| 74 | assembly { |
| 75 | r.slot := slot |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. |
| 81 | */ |
| 82 | function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { |
| 83 | /// @solidity memory-safe-assembly |
| 84 | assembly { |
| 85 | r.slot := slot |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * @dev Returns an `Uint256Slot` with member `value` located at `slot`. |
| 91 | */ |
| 92 | function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { |
| 93 | /// @solidity memory-safe-assembly |
| 94 | assembly { |
| 95 | r.slot := slot |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * @dev Returns an `StringSlot` with member `value` located at `slot`. |
| 101 | */ |
| 102 | function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { |
| 103 | /// @solidity memory-safe-assembly |
| 104 | assembly { |
| 105 | r.slot := slot |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * @dev Returns an `StringSlot` representation of the string storage pointer `store`. |
| 111 | */ |
| 112 | function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { |
| 113 | /// @solidity memory-safe-assembly |
| 114 | assembly { |
| 115 | r.slot := store.slot |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * @dev Returns an `BytesSlot` with member `value` located at `slot`. |
| 121 | */ |
| 122 | function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { |
| 123 | /// @solidity memory-safe-assembly |
| 124 | assembly { |
| 125 | r.slot := slot |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. |
| 131 | */ |
| 132 | function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { |
| 133 | /// @solidity memory-safe-assembly |
| 134 | assembly { |
| 135 | r.slot := store.slot |
| 136 | } |
| 137 | } |
| 138 | } |
| 139 |
0.0%
lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol
Lines covered: 0 / 35 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableSet.sol) |
| 3 | // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. |
| 4 | |
| 5 | pragma solidity ^0.8.0; |
| 6 | |
| 7 | /** |
| 8 | * @dev Library for managing |
| 9 | * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive |
| 10 | * types. |
| 11 | * |
| 12 | * Sets have the following properties: |
| 13 | * |
| 14 | * - Elements are added, removed, and checked for existence in constant time |
| 15 | * (O(1)). |
| 16 | * - Elements are enumerated in O(n). No guarantees are made on the ordering. |
| 17 | * |
| 18 | * ```solidity |
| 19 | * contract Example { |
| 20 | * // Add the library methods |
| 21 | * using EnumerableSet for EnumerableSet.AddressSet; |
| 22 | * |
| 23 | * // Declare a set state variable |
| 24 | * EnumerableSet.AddressSet private mySet; |
| 25 | * } |
| 26 | * ``` |
| 27 | * |
| 28 | * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) |
| 29 | * and `uint256` (`UintSet`) are supported. |
| 30 | * |
| 31 | * [WARNING] |
| 32 | * ==== |
| 33 | * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure |
| 34 | * unusable. |
| 35 | * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. |
| 36 | * |
| 37 | * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an |
| 38 | * array of EnumerableSet. |
| 39 | * ==== |
| 40 | */ |
| 41 | library EnumerableSet { |
| 42 | // To implement this library for multiple types with as little code |
| 43 | // repetition as possible, we write it in terms of a generic Set type with |
| 44 | // bytes32 values. |
| 45 | // The Set implementation uses private functions, and user-facing |
| 46 | // implementations (such as AddressSet) are just wrappers around the |
| 47 | // underlying Set. |
| 48 | // This means that we can only create new EnumerableSets for types that fit |
| 49 | // in bytes32. |
| 50 | |
| 51 | struct Set { |
| 52 | // Storage of set values |
| 53 | bytes32[] _values; |
| 54 | // Position of the value in the `values` array, plus 1 because index 0 |
| 55 | // means a value is not in the set. |
| 56 | mapping(bytes32 => uint256) _indexes; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * @dev Add a value to a set. O(1). |
| 61 | * |
| 62 | * Returns true if the value was added to the set, that is if it was not |
| 63 | * already present. |
| 64 | */ |
| 65 | function _add(Set storage set, bytes32 value) private returns (bool) { |
| 66 | if (!_contains(set, value)) { |
| 67 | set._values.push(value); |
| 68 | // The value is stored at length-1, but we add 1 to all indexes |
| 69 | // and use 0 as a sentinel value |
| 70 | set._indexes[value] = set._values.length; |
| 71 | return true; |
| 72 | } else { |
| 73 | return false; |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * @dev Removes a value from a set. O(1). |
| 79 | * |
| 80 | * Returns true if the value was removed from the set, that is if it was |
| 81 | * present. |
| 82 | */ |
| 83 | function _remove(Set storage set, bytes32 value) private returns (bool) { |
| 84 | // We read and store the value's index to prevent multiple reads from the same storage slot |
| 85 | uint256 valueIndex = set._indexes[value]; |
| 86 | |
| 87 | if (valueIndex != 0) { |
| 88 | // Equivalent to contains(set, value) |
| 89 | // To delete an element from the _values array in O(1), we swap the element to delete with the last one in |
| 90 | // the array, and then remove the last element (sometimes called as 'swap and pop'). |
| 91 | // This modifies the order of the array, as noted in {at}. |
| 92 | |
| 93 | uint256 toDeleteIndex = valueIndex - 1; |
| 94 | uint256 lastIndex = set._values.length - 1; |
| 95 | |
| 96 | if (lastIndex != toDeleteIndex) { |
| 97 | bytes32 lastValue = set._values[lastIndex]; |
| 98 | |
| 99 | // Move the last value to the index where the value to delete is |
| 100 | set._values[toDeleteIndex] = lastValue; |
| 101 | // Update the index for the moved value |
| 102 | set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex |
| 103 | } |
| 104 | |
| 105 | // Delete the slot where the moved value was stored |
| 106 | set._values.pop(); |
| 107 | |
| 108 | // Delete the index for the deleted slot |
| 109 | delete set._indexes[value]; |
| 110 | |
| 111 | return true; |
| 112 | } else { |
| 113 | return false; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * @dev Returns true if the value is in the set. O(1). |
| 119 | */ |
| 120 | function _contains(Set storage set, bytes32 value) private view returns (bool) { |
| 121 | return set._indexes[value] != 0; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * @dev Returns the number of values on the set. O(1). |
| 126 | */ |
| 127 | function _length(Set storage set) private view returns (uint256) { |
| 128 | return set._values.length; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * @dev Returns the value stored at position `index` in the set. O(1). |
| 133 | * |
| 134 | * Note that there are no guarantees on the ordering of values inside the |
| 135 | * array, and it may change when more values are added or removed. |
| 136 | * |
| 137 | * Requirements: |
| 138 | * |
| 139 | * - `index` must be strictly less than {length}. |
| 140 | */ |
| 141 | function _at(Set storage set, uint256 index) private view returns (bytes32) { |
| 142 | return set._values[index]; |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * @dev Return the entire set in an array |
| 147 | * |
| 148 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
| 149 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
| 150 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function |
| 151 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
| 152 | */ |
| 153 | function _values(Set storage set) private view returns (bytes32[] memory) { |
| 154 | return set._values; |
| 155 | } |
| 156 | |
| 157 | // Bytes32Set |
| 158 | |
| 159 | struct Bytes32Set { |
| 160 | Set _inner; |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * @dev Add a value to a set. O(1). |
| 165 | * |
| 166 | * Returns true if the value was added to the set, that is if it was not |
| 167 | * already present. |
| 168 | */ |
| 169 | function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { |
| 170 | return _add(set._inner, value); |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * @dev Removes a value from a set. O(1). |
| 175 | * |
| 176 | * Returns true if the value was removed from the set, that is if it was |
| 177 | * present. |
| 178 | */ |
| 179 | function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { |
| 180 | return _remove(set._inner, value); |
| 181 | } |
| 182 | |
| 183 | /** |
| 184 | * @dev Returns true if the value is in the set. O(1). |
| 185 | */ |
| 186 | function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { |
| 187 | return _contains(set._inner, value); |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * @dev Returns the number of values in the set. O(1). |
| 192 | */ |
| 193 | function length(Bytes32Set storage set) internal view returns (uint256) { |
| 194 | return _length(set._inner); |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * @dev Returns the value stored at position `index` in the set. O(1). |
| 199 | * |
| 200 | * Note that there are no guarantees on the ordering of values inside the |
| 201 | * array, and it may change when more values are added or removed. |
| 202 | * |
| 203 | * Requirements: |
| 204 | * |
| 205 | * - `index` must be strictly less than {length}. |
| 206 | */ |
| 207 | function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { |
| 208 | return _at(set._inner, index); |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * @dev Return the entire set in an array |
| 213 | * |
| 214 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
| 215 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
| 216 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function |
| 217 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
| 218 | */ |
| 219 | function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { |
| 220 | bytes32[] memory store = _values(set._inner); |
| 221 | bytes32[] memory result; |
| 222 | |
| 223 | /// @solidity memory-safe-assembly |
| 224 | assembly { |
| 225 | result := store |
| 226 | } |
| 227 | |
| 228 | return result; |
| 229 | } |
| 230 | |
| 231 | // AddressSet |
| 232 | |
| 233 | struct AddressSet { |
| 234 | Set _inner; |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * @dev Add a value to a set. O(1). |
| 239 | * |
| 240 | * Returns true if the value was added to the set, that is if it was not |
| 241 | * already present. |
| 242 | */ |
| 243 | function add(AddressSet storage set, address value) internal returns (bool) { |
| 244 | return _add(set._inner, bytes32(uint256(uint160(value)))); |
| 245 | } |
| 246 | |
| 247 | /** |
| 248 | * @dev Removes a value from a set. O(1). |
| 249 | * |
| 250 | * Returns true if the value was removed from the set, that is if it was |
| 251 | * present. |
| 252 | */ |
| 253 | function remove(AddressSet storage set, address value) internal returns (bool) { |
| 254 | return _remove(set._inner, bytes32(uint256(uint160(value)))); |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * @dev Returns true if the value is in the set. O(1). |
| 259 | */ |
| 260 | function contains(AddressSet storage set, address value) internal view returns (bool) { |
| 261 | return _contains(set._inner, bytes32(uint256(uint160(value)))); |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * @dev Returns the number of values in the set. O(1). |
| 266 | */ |
| 267 | function length(AddressSet storage set) internal view returns (uint256) { |
| 268 | return _length(set._inner); |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * @dev Returns the value stored at position `index` in the set. O(1). |
| 273 | * |
| 274 | * Note that there are no guarantees on the ordering of values inside the |
| 275 | * array, and it may change when more values are added or removed. |
| 276 | * |
| 277 | * Requirements: |
| 278 | * |
| 279 | * - `index` must be strictly less than {length}. |
| 280 | */ |
| 281 | function at(AddressSet storage set, uint256 index) internal view returns (address) { |
| 282 | return address(uint160(uint256(_at(set._inner, index)))); |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * @dev Return the entire set in an array |
| 287 | * |
| 288 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
| 289 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
| 290 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function |
| 291 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
| 292 | */ |
| 293 | function values(AddressSet storage set) internal view returns (address[] memory) { |
| 294 | bytes32[] memory store = _values(set._inner); |
| 295 | address[] memory result; |
| 296 | |
| 297 | /// @solidity memory-safe-assembly |
| 298 | assembly { |
| 299 | result := store |
| 300 | } |
| 301 | |
| 302 | return result; |
| 303 | } |
| 304 | |
| 305 | // UintSet |
| 306 | |
| 307 | struct UintSet { |
| 308 | Set _inner; |
| 309 | } |
| 310 | |
| 311 | /** |
| 312 | * @dev Add a value to a set. O(1). |
| 313 | * |
| 314 | * Returns true if the value was added to the set, that is if it was not |
| 315 | * already present. |
| 316 | */ |
| 317 | function add(UintSet storage set, uint256 value) internal returns (bool) { |
| 318 | return _add(set._inner, bytes32(value)); |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * @dev Removes a value from a set. O(1). |
| 323 | * |
| 324 | * Returns true if the value was removed from the set, that is if it was |
| 325 | * present. |
| 326 | */ |
| 327 | function remove(UintSet storage set, uint256 value) internal returns (bool) { |
| 328 | return _remove(set._inner, bytes32(value)); |
| 329 | } |
| 330 | |
| 331 | /** |
| 332 | * @dev Returns true if the value is in the set. O(1). |
| 333 | */ |
| 334 | function contains(UintSet storage set, uint256 value) internal view returns (bool) { |
| 335 | return _contains(set._inner, bytes32(value)); |
| 336 | } |
| 337 | |
| 338 | /** |
| 339 | * @dev Returns the number of values in the set. O(1). |
| 340 | */ |
| 341 | function length(UintSet storage set) internal view returns (uint256) { |
| 342 | return _length(set._inner); |
| 343 | } |
| 344 | |
| 345 | /** |
| 346 | * @dev Returns the value stored at position `index` in the set. O(1). |
| 347 | * |
| 348 | * Note that there are no guarantees on the ordering of values inside the |
| 349 | * array, and it may change when more values are added or removed. |
| 350 | * |
| 351 | * Requirements: |
| 352 | * |
| 353 | * - `index` must be strictly less than {length}. |
| 354 | */ |
| 355 | function at(UintSet storage set, uint256 index) internal view returns (uint256) { |
| 356 | return uint256(_at(set._inner, index)); |
| 357 | } |
| 358 | |
| 359 | /** |
| 360 | * @dev Return the entire set in an array |
| 361 | * |
| 362 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
| 363 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
| 364 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function |
| 365 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
| 366 | */ |
| 367 | function values(UintSet storage set) internal view returns (uint256[] memory) { |
| 368 | bytes32[] memory store = _values(set._inner); |
| 369 | uint256[] memory result; |
| 370 | |
| 371 | /// @solidity memory-safe-assembly |
| 372 | assembly { |
| 373 | result := store |
| 374 | } |
| 375 | |
| 376 | return result; |
| 377 | } |
| 378 | } |
| 379 |
81.0%
src/AddressDriver.sol
Lines covered: 26 / 32 (81.0%)
| 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 | contract AddressDriver is DriverTransferUtils, Managed { |
| 13 | /// @notice The Drips address used by this driver. |
| 14 | Drips public immutable drips; |
| 15 | /// @notice The driver ID which this driver uses when calling Drips. |
| 16 | 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 | drips = drips_; |
| 23 | 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 | function calcAccountId(address addr) public view returns (uint256 accountId) { |
| 32 | // By assignment we get `accountId` value: |
| 33 | // `zeros (224 bits) | driverId (32 bits)` |
| 34 | 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 | 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 | 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 | function collect(IERC20 erc20, address transferTo) public whenNotPaused returns (uint128 amt) { |
| 59 | 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 | function give(uint256 receiver, IERC20 erc20, uint128 amt) public whenNotPaused { |
| 74 | _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 | 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 | 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 | function setSplits(SplitsReceiver[] calldata receivers) public whenNotPaused { |
| 168 | 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 | function emitAccountMetadata(AccountMetadata[] calldata accountMetadata) public whenNotPaused { |
| 176 | if (accountMetadata.length != 0) { |
| 177 | drips.emitAccountMetadata(_callerAccountId(), accountMetadata); |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 |
86.0%
src/Drips.sol
Lines covered: 127 / 146 (86.0%)
| 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 | 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 | uint256 public constant MAX_STREAMS_RECEIVERS = _MAX_STREAMS_RECEIVERS; |
| 59 | /// @notice The additional decimals for all amtPerSec values. |
| 60 | uint8 public constant AMT_PER_SEC_EXTRA_DECIMALS = _AMT_PER_SEC_EXTRA_DECIMALS; |
| 61 | /// @notice The multiplier for all amtPerSec values. |
| 62 | 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 | uint256 public constant MAX_SPLITS_RECEIVERS = _MAX_SPLITS_RECEIVERS; |
| 66 | /// @notice The total splits weight of an account |
| 67 | 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 | 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 | uint32 public immutable cycleSecs; |
| 80 | /// @notice The minimum amtPerSec of a stream. It's 1 token per cycle. |
| 81 | uint160 public immutable minAmtPerSec; |
| 82 | /// @notice The ERC-1967 storage slot holding a single `DripsStorage` structure. |
| 83 | 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 | Streams(cycleSecs_, _erc1967Slot("eip1967.streams.storage")) |
| 140 | Splits(_erc1967Slot("eip1967.splits.storage")) |
| 141 | { |
| 142 | cycleSecs = Streams._cycleSecs; |
| 143 | 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 | uint32 driverId = uint32(accountId >> DRIVER_ID_OFFSET); |
| 156 | _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 | 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 | function registerDriver(address driverAddr) public whenNotPaused returns (uint32 driverId) { |
| 178 | require(driverAddr != address(0), "Driver registered for 0 address"); |
| 179 | DripsStorage storage dripsStorage = _dripsStorage(); |
| 180 | driverId = dripsStorage.nextDriverId++; |
| 181 | dripsStorage.driverAddresses[driverId] = driverAddr; |
| 182 | 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 | function driverAddress(uint32 driverId) public view returns (address driverAddr) { |
| 190 | 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 | function updateDriverAddress(uint32 driverId, address newDriverAddr) public whenNotPaused { |
| 199 | _assertCallerIsDriver(driverId); |
| 200 | _dripsStorage().driverAddresses[driverId] = newDriverAddr; |
| 201 | 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 | function nextDriverId() public view returns (uint32 driverId) { |
| 207 | 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 | function balances(IERC20 erc20) |
| 226 | public |
| 227 | view |
| 228 | returns (uint128 streamsBalance, uint128 splitsBalance) |
| 229 | { |
| 230 | Balance storage balance = _dripsStorage().balances[erc20]; |
| 231 | 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 | _verifyBalanceIncrease(erc20, amt); |
| 242 | _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 | _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 | _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 | _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 | function _moveBalanceFromStreamsToSplits(IERC20 erc20, uint128 amt) internal { |
| 279 | Balance storage balance = _dripsStorage().balances[erc20]; |
| 280 | balance.streams -= amt; |
| 281 | 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 | function _verifyBalanceIncrease(IERC20 erc20, uint128 amt) internal view { |
| 290 | (uint256 streamsBalance, uint128 splitsBalance) = balances(erc20); |
| 291 | uint256 newTotalBalance = streamsBalance + splitsBalance + amt; |
| 292 | require(newTotalBalance <= MAX_TOTAL_BALANCE, "Total balance too high"); |
| 293 | 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 | function withdraw(IERC20 erc20, address receiver, uint256 amt) public { |
| 312 | (uint128 streamsBalance, uint128 splitsBalance) = balances(erc20); |
| 313 | uint256 withdrawable = _tokenBalance(erc20) - streamsBalance - splitsBalance; |
| 314 | require(amt <= withdrawable, "Withdrawal amount too high"); |
| 315 | emit Withdrawn(erc20, receiver, amt); |
| 316 | erc20.safeTransfer(receiver, amt); |
| 317 | } |
| 318 | |
| 319 | function _tokenBalance(IERC20 erc20) internal view returns (uint256) { |
| 320 | 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 | 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 | function receiveStreamsResult(uint256 accountId, IERC20 erc20, uint32 maxCycles) |
| 355 | public |
| 356 | view |
| 357 | returns (uint128 receivableAmt) |
| 358 | { |
| 359 | (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 | function receiveStreams(uint256 accountId, IERC20 erc20, uint32 maxCycles) |
| 377 | public |
| 378 | whenNotPaused |
| 379 | returns (uint128 receivedAmt) |
| 380 | { |
| 381 | receivedAmt = Streams._receiveStreams(accountId, erc20, maxCycles); |
| 382 | if (receivedAmt != 0) { |
| 383 | _moveBalanceFromStreamsToSplits(erc20, receivedAmt); |
| 384 | 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 | 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 | amt = Streams._squeezeStreams(accountId, erc20, senderId, historyHash, streamsHistory); |
| 416 | if (amt != 0) { |
| 417 | _moveBalanceFromStreamsToSplits(erc20, amt); |
| 418 | 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 | 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 | (amt,,,,) = |
| 443 | 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 | 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 | function splitResult(uint256 accountId, SplitsReceiver[] memory currReceivers, uint128 amount) |
| 471 | public |
| 472 | view |
| 473 | returns (uint128 collectableAmt, uint128 splitAmt) |
| 474 | { |
| 475 | 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 | function split(uint256 accountId, IERC20 erc20, SplitsReceiver[] memory currReceivers) |
| 502 | public |
| 503 | whenNotPaused |
| 504 | returns (uint128 collectableAmt, uint128 splitAmt) |
| 505 | { |
| 506 | 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 | function collectable(uint256 accountId, IERC20 erc20) public view returns (uint128 amt) { |
| 519 | 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 | 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 | 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 | function give(uint256 accountId, uint256 receiver, IERC20 erc20, uint128 amt) |
| 558 | public |
| 559 | whenNotPaused |
| 560 | onlyDriver(accountId) |
| 561 | { |
| 562 | 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 | 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 | 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 | function balanceAt( |
| 609 | uint256 accountId, |
| 610 | IERC20 erc20, |
| 611 | StreamReceiver[] memory currReceivers, |
| 612 | uint32 timestamp |
| 613 | ) public view returns (uint128 balance) { |
| 614 | 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 | 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 | if (balanceDelta > 0) _increaseStreamsBalance(erc20, uint128(balanceDelta)); |
| 680 | realBalanceDelta = Streams._setStreams( |
| 681 | accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 |
| 682 | ); |
| 683 | 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 | function hashStreams(StreamReceiver[] memory receivers) |
| 694 | public |
| 695 | pure |
| 696 | returns (bytes32 streamsHash) |
| 697 | { |
| 698 | 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 | function hashStreamsHistory( |
| 711 | bytes32 oldStreamsHistoryHash, |
| 712 | bytes32 streamsHash, |
| 713 | uint32 updateTime, |
| 714 | uint32 maxEnd |
| 715 | ) public pure returns (bytes32 streamsHistoryHash) { |
| 716 | 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 | function setSplits(uint256 accountId, SplitsReceiver[] memory receivers) |
| 740 | public |
| 741 | whenNotPaused |
| 742 | onlyDriver(accountId) |
| 743 | { |
| 744 | 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 | function splitsHash(uint256 accountId) public view returns (bytes32 currSplitsHash) { |
| 751 | 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 | function hashSplits(SplitsReceiver[] memory receivers) |
| 759 | public |
| 760 | pure |
| 761 | returns (bytes32 receiversHash) |
| 762 | { |
| 763 | 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 | function emitAccountMetadata(uint256 accountId, AccountMetadata[] calldata accountMetadata) |
| 772 | public |
| 773 | whenNotPaused |
| 774 | onlyDriver(accountId) |
| 775 | { |
| 776 | unchecked { |
| 777 | for (uint256 i = 0; i < accountMetadata.length; i++) { |
| 778 | AccountMetadata calldata metadata = accountMetadata[i]; |
| 779 | emit AccountMetadataEmitted(accountId, metadata.key, metadata.value); |
| 780 | } |
| 781 | } |
| 782 | } |
| 783 | |
| 784 | /// @notice Returns the Drips storage. |
| 785 | /// @return storageRef The storage. |
| 786 | function _dripsStorage() internal view returns (DripsStorage storage storageRef) { |
| 787 | bytes32 slot = _dripsStorageSlot; |
| 788 | // slither-disable-next-line assembly |
| 789 | assembly { |
| 790 | storageRef.slot := slot |
| 791 | } |
| 792 | } |
| 793 | } |
| 794 |
100.0%
src/DriverTransferUtils.sol
Lines covered: 16 / 16 (100.0%)
| 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 | amt = drips.collect(accountId, erc20); |
| 32 | 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 | if (amt > 0) _transferFromCaller(drips, erc20, amt); |
| 55 | 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 | 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 | if (balanceDelta > 0) _transferFromCaller(drips, erc20, uint128(balanceDelta)); |
| 120 | realBalanceDelta = drips.setStreams( |
| 121 | accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 |
| 122 | ); |
| 123 | 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 | function _transferFromCaller(Drips drips, IERC20 erc20, uint128 amt) internal { |
| 131 | SafeERC20.safeTransferFrom(erc20, _msgSender(), address(drips), amt); |
| 132 | } |
| 133 | } |
| 134 |
22.0%
src/Managed.sol
Lines covered: 12 / 53 (22.0%)
| 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 | 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 | 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 | 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 | 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 | 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 | _managedStorage().isPaused = true; |
| 80 | } |
| 81 | |
| 82 | /// @notice Returns the current implementation address. |
| 83 | function implementation() public view returns (address) { |
| 84 | return _getImplementation(); |
| 85 | } |
| 86 | |
| 87 | /// @notice Returns the address of the current admin. |
| 88 | function admin() public view returns (address) { |
| 89 | return _getAdmin(); |
| 90 | } |
| 91 | |
| 92 | /// @notice Returns the proposed address to change the admin to. |
| 93 | function proposedAdmin() public view returns (address) { |
| 94 | 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 | function proposeNewAdmin(address newAdmin) public onlyAdmin { |
| 103 | emit NewAdminProposed(msg.sender, newAdmin); |
| 104 | _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 | function acceptAdmin() public { |
| 111 | require(proposedAdmin() == msg.sender, "Caller not the proposed admin"); |
| 112 | _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 | function renounceAdmin() public onlyAdmin { |
| 119 | _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 | function _updateAdmin(address newAdmin) internal { |
| 125 | emit AdminChanged(admin(), newAdmin); |
| 126 | _managedStorage().proposedAdmin = address(0); |
| 127 | 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 | function grantPauser(address pauser) public onlyAdmin { |
| 133 | require(_managedStorage().pausers.add(pauser), "Address already is a pauser"); |
| 134 | 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 | function revokePauser(address pauser) public onlyAdmin { |
| 140 | require(_managedStorage().pausers.remove(pauser), "Address is not a pauser"); |
| 141 | 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 | function isPauser(address pauser) public view returns (bool isAddrPauser) { |
| 148 | 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 | function allPausers() public view returns (address[] memory pausersList) { |
| 155 | return _managedStorage().pausers.values(); |
| 156 | } |
| 157 | |
| 158 | /// @notice Returns true if the contract is paused, and false otherwise. |
| 159 | function isPaused() public view returns (bool) { |
| 160 | return _managedStorage().isPaused; |
| 161 | } |
| 162 | |
| 163 | /// @notice Triggers stopped state. Callable only by the admin or a pauser. |
| 164 | function pause() public onlyAdminOrPauser whenNotPaused { |
| 165 | _managedStorage().isPaused = true; |
| 166 | emit Paused(msg.sender); |
| 167 | } |
| 168 | |
| 169 | /// @notice Returns to normal state. Callable only by the admin or a pauser. |
| 170 | function unpause() public onlyAdminOrPauser whenPaused { |
| 171 | _managedStorage().isPaused = false; |
| 172 | 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 | 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 | return bytes32(uint256(keccak256(bytes(name))) - 1024); |
| 184 | } |
| 185 | |
| 186 | /// @notice Returns the Managed storage. |
| 187 | /// @return storageRef The storage. |
| 188 | function _managedStorage() internal view returns (ManagedStorage storage storageRef) { |
| 189 | bytes32 slot = _managedStorageSlot; |
| 190 | // slither-disable-next-line assembly |
| 191 | assembly { |
| 192 | storageRef.slot := slot |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | /// @notice Authorizes the contract upgrade. See `UUPSUpgradeable` docs for more details. |
| 197 | function _authorizeUpgrade(address /* newImplementation */ ) internal view override onlyAdmin { |
| 198 | return; |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | /// @notice A generic proxy for contracts implementing `Managed`. |
| 203 | contract ManagedProxy is ERC1967Proxy { |
| 204 | constructor(Managed logic, address admin) ERC1967Proxy(address(logic), new bytes(0)) { |
| 205 | _changeAdmin(admin); |
| 206 | } |
| 207 | } |
| 208 |
100.0%
src/Splits.sol
Lines covered: 87 / 87 (100.0%)
| 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 | 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 | _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 | 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 | for (uint256 i = currReceivers.length; i != 0;) { |
| 138 | splitsWeight += currReceivers[--i].weight; |
| 139 | } |
| 140 | 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 | function _split(uint256 accountId, IERC20 erc20, SplitsReceiver[] memory currReceivers) |
| 157 | internal |
| 158 | returns (uint128 collectableAmt, uint128 splitAmt) |
| 159 | { |
| 160 | _assertCurrSplits(accountId, currReceivers); |
| 161 | SplitsBalance storage balance = _splitsStorage().splitsStates[accountId].balances[erc20]; |
| 162 | |
| 163 | collectableAmt = balance.splittable; |
| 164 | if (collectableAmt == 0) { |
| 165 | return (0, 0); |
| 166 | } |
| 167 | balance.splittable = 0; |
| 168 | |
| 169 | unchecked { |
| 170 | uint256 splitsWeight = 0; |
| 171 | for (uint256 i = 0; i < currReceivers.length; i++) { |
| 172 | splitsWeight += currReceivers[i].weight; |
| 173 | uint128 currSplitAmt = splitAmt; |
| 174 | splitAmt = uint128(collectableAmt * splitsWeight / _TOTAL_SPLITS_WEIGHT); |
| 175 | currSplitAmt = splitAmt - currSplitAmt; |
| 176 | uint256 receiver = currReceivers[i].accountId; |
| 177 | _addSplittable(receiver, erc20, currSplitAmt); |
| 178 | emit Split(accountId, receiver, erc20, currSplitAmt); |
| 179 | } |
| 180 | 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 | balance.collectable += collectableAmt; |
| 184 | } |
| 185 | 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 | function _collectable(uint256 accountId, IERC20 erc20) internal view returns (uint128 amt) { |
| 193 | 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 | SplitsBalance storage balance = _splitsStorage().splitsStates[accountId].balances[erc20]; |
| 202 | amt = balance.collectable; |
| 203 | balance.collectable = 0; |
| 204 | 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 | 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 | function _setSplits(uint256 accountId, SplitsReceiver[] memory receivers) internal { |
| 237 | SplitsState storage state = _splitsStorage().splitsStates[accountId]; |
| 238 | bytes32 newSplitsHash = _hashSplits(receivers); |
| 239 | if (newSplitsHash == state.splitsHash) return; |
| 240 | emit SplitsSet(accountId, newSplitsHash); |
| 241 | _assertSplitsValid(receivers, newSplitsHash); |
| 242 | 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 | require(receivers.length <= _MAX_SPLITS_RECEIVERS, "Too many splits receivers"); |
| 252 | uint256 totalWeight = 0; |
| 253 | uint256 prevAccountId = 0; |
| 254 | for (uint256 i = 0; i < receivers.length; i++) { |
| 255 | SplitsReceiver memory receiver = receivers[i]; |
| 256 | uint32 weight = receiver.weight; |
| 257 | require(weight != 0, "Splits receiver weight is zero"); |
| 258 | totalWeight += weight; |
| 259 | uint256 accountId = receiver.accountId; |
| 260 | if (accountId <= prevAccountId) require(i == 0, "Splits receivers not sorted"); |
| 261 | prevAccountId = accountId; |
| 262 | emit SplitsReceiverSeen(receiversHash, accountId, weight); |
| 263 | } |
| 264 | 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 | function _assertCurrSplits(uint256 accountId, SplitsReceiver[] memory currReceivers) |
| 273 | internal |
| 274 | view |
| 275 | { |
| 276 | require( |
| 277 | _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 | function _splitsHash(uint256 accountId) internal view returns (bytes32 currSplitsHash) { |
| 285 | 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 | function _hashSplits(SplitsReceiver[] memory receivers) |
| 293 | internal |
| 294 | pure |
| 295 | returns (bytes32 receiversHash) |
| 296 | { |
| 297 | if (receivers.length == 0) { |
| 298 | return bytes32(0); |
| 299 | } |
| 300 | return keccak256(abi.encode(receivers)); |
| 301 | } |
| 302 | |
| 303 | /// @notice Returns the Splits storage. |
| 304 | /// @return splitsStorage The storage. |
| 305 | function _splitsStorage() private view returns (SplitsStorage storage splitsStorage) { |
| 306 | bytes32 slot = _splitsStorageSlot; |
| 307 | // slither-disable-next-line assembly |
| 308 | assembly { |
| 309 | splitsStorage.slot := slot |
| 310 | } |
| 311 | } |
| 312 | } |
| 313 |
99.0%
src/Streams.sol
Lines covered: 412 / 414 (99.0%)
| 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 | 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 | 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 | 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 | 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 | 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 | config = (config << 32) | duration_; |
| 85 | 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 | 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 | return uint160(StreamConfig.unwrap(config) >> 64); |
| 108 | } |
| 109 | |
| 110 | /// @notice Extracts start from a `StreamConfig` |
| 111 | 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 | return uint32(StreamConfig.unwrap(config) >> 32); |
| 119 | } |
| 120 | |
| 121 | /// @notice Extracts duration from a `StreamConfig` |
| 122 | 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 | 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 | 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 | 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 | _cycleSecs = cycleSecs; |
| 278 | _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 | 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 | (receivedAmt, receivableCycles, fromCycle, toCycle, finalAmtPerCycle) = |
| 299 | _receiveStreamsResult(accountId, erc20, maxCycles); |
| 300 | if (fromCycle != toCycle) { |
| 301 | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 302 | state.nextReceivableCycle = toCycle; |
| 303 | mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
| 304 | unchecked { |
| 305 | for (uint32 cycle = fromCycle; cycle < toCycle; cycle++) { |
| 306 | 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 | if (finalAmtPerCycle != 0) { |
| 311 | amtDeltas[toCycle].thisCycle += finalAmtPerCycle; |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | 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 | 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 | (fromCycle, toCycle) = _receivableStreamsCyclesRange(accountId, erc20); |
| 342 | if (toCycle - fromCycle > maxCycles) { |
| 343 | receivableCycles = toCycle - fromCycle - maxCycles; |
| 344 | toCycle -= receivableCycles; |
| 345 | } |
| 346 | mapping(uint32 cycle => AmtDelta) storage amtDeltas = |
| 347 | _streamsStorage().states[erc20][accountId].amtDeltas; |
| 348 | for (uint32 cycle = fromCycle; cycle < toCycle; cycle++) { |
| 349 | AmtDelta memory amtDelta = amtDeltas[cycle]; |
| 350 | 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 | receivedAmt += uint128(amtPerCycle); |
| 354 | 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 | (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 | function _receivableStreamsCyclesRange(uint256 accountId, IERC20 erc20) |
| 382 | private |
| 383 | view |
| 384 | returns (uint32 fromCycle, uint32 toCycle) |
| 385 | { |
| 386 | fromCycle = _streamsStorage().states[erc20][accountId].nextReceivableCycle; |
| 387 | toCycle = _cycleOf(_currTimestamp()); |
| 388 | // slither-disable-next-line timestamp |
| 389 | 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 | function _squeezeStreams( |
| 411 | uint256 accountId, |
| 412 | IERC20 erc20, |
| 413 | uint256 senderId, |
| 414 | bytes32 historyHash, |
| 415 | StreamsHistory[] memory streamsHistory |
| 416 | ) internal returns (uint128 amt) { |
| 417 | unchecked { |
| 418 | uint256 squeezedNum; |
| 419 | uint256[] memory squeezedRevIdxs; |
| 420 | bytes32[] memory historyHashes; |
| 421 | uint256 currCycleConfigs; |
| 422 | (amt, squeezedNum, squeezedRevIdxs, historyHashes, currCycleConfigs) = |
| 423 | _squeezeStreamsResult(accountId, erc20, senderId, historyHash, streamsHistory); |
| 424 | bytes32[] memory squeezedHistoryHashes = new bytes32[](squeezedNum); |
| 425 | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 426 | uint32[2 ** 32] storage nextSqueezed = state.nextSqueezed[senderId]; |
| 427 | 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 | uint256 revIdx = squeezedRevIdxs[squeezedNum - i - 1]; |
| 431 | squeezedHistoryHashes[i] = historyHashes[historyHashes.length - revIdx]; |
| 432 | nextSqueezed[currCycleConfigs - revIdx] = _currTimestamp(); |
| 433 | } |
| 434 | uint32 cycleStart = _currCycleStart(); |
| 435 | _addDeltaRange( |
| 436 | state, cycleStart, cycleStart + 1, -int160(amt * _AMT_PER_SEC_MULTIPLIER) |
| 437 | ); |
| 438 | 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 | 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 | StreamsState storage sender = _streamsStorage().states[erc20][senderId]; |
| 484 | historyHashes = |
| 485 | _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 | if (sender.updateTime >= _currCycleStart()) { |
| 491 | currCycleConfigs = sender.lastUpdatedCycleConfigs; |
| 492 | } |
| 493 | } |
| 494 | squeezedRevIdxs = new uint256[](streamsHistory.length); |
| 495 | uint32[2 ** 32] storage nextSqueezed = |
| 496 | _streamsStorage().states[erc20][accountId].nextSqueezed[senderId]; |
| 497 | uint32 squeezeEndCap = _currTimestamp(); |
| 498 | unchecked { |
| 499 | for (uint256 i = 1; i <= streamsHistory.length && i <= currCycleConfigs; i++) { |
| 500 | StreamsHistory memory historyEntry = streamsHistory[streamsHistory.length - i]; |
| 501 | if (historyEntry.receivers.length != 0) { |
| 502 | uint32 squeezeStartCap = nextSqueezed[currCycleConfigs - i]; |
| 503 | if (squeezeStartCap < _currCycleStart()) squeezeStartCap = _currCycleStart(); |
| 504 | // Benchmark fixture: intentionally reintroduce the historical |
| 505 | // pre-update-time squeeze bug |
| 506 | // fixed upstream by drips-network/contracts@f3be74c. |
| 507 | if (squeezeStartCap < squeezeEndCap) { |
| 508 | 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 | amt += _squeezedAmt(accountId, historyEntry, squeezeStartCap, squeezeEndCap); |
| 512 | } |
| 513 | } |
| 514 | 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 | function _verifyStreamsHistory( |
| 529 | bytes32 historyHash, |
| 530 | StreamsHistory[] memory streamsHistory, |
| 531 | bytes32 finalHistoryHash |
| 532 | ) private pure returns (bytes32[] memory historyHashes) { |
| 533 | historyHashes = new bytes32[](streamsHistory.length); |
| 534 | for (uint256 i = 0; i < streamsHistory.length; i++) { |
| 535 | StreamsHistory memory historyEntry = streamsHistory[i]; |
| 536 | bytes32 streamsHash = historyEntry.streamsHash; |
| 537 | if (historyEntry.receivers.length != 0) { |
| 538 | require(streamsHash == 0, "Entry with hash and receivers"); |
| 539 | streamsHash = _hashStreams(historyEntry.receivers); |
| 540 | } |
| 541 | historyHashes[i] = historyHash; |
| 542 | historyHash = _hashStreamsHistory( |
| 543 | historyHash, streamsHash, historyEntry.updateTime, historyEntry.maxEnd |
| 544 | ); |
| 545 | } |
| 546 | // slither-disable-next-line incorrect-equality,timestamp |
| 547 | 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 | function _squeezedAmt( |
| 557 | uint256 accountId, |
| 558 | StreamsHistory memory historyEntry, |
| 559 | uint32 squeezeStartCap, |
| 560 | uint32 squeezeEndCap |
| 561 | ) private view returns (uint128 squeezedAmt) { |
| 562 | unchecked { |
| 563 | StreamReceiver[] memory receivers = historyEntry.receivers; |
| 564 | // Binary search for the `idx` of the first occurrence of `accountId` |
| 565 | uint256 idx = 0; |
| 566 | for (uint256 idxCap = receivers.length; idx < idxCap;) { |
| 567 | uint256 idxMid = (idx + idxCap) / 2; |
| 568 | if (receivers[idxMid].accountId < accountId) { |
| 569 | idx = idxMid + 1; |
| 570 | } else { |
| 571 | idxCap = idxMid; |
| 572 | } |
| 573 | } |
| 574 | uint32 updateTime = historyEntry.updateTime; |
| 575 | uint32 maxEnd = historyEntry.maxEnd; |
| 576 | uint256 amt = 0; |
| 577 | for (; idx < receivers.length; idx++) { |
| 578 | StreamReceiver memory receiver = receivers[idx]; |
| 579 | if (receiver.accountId != accountId) break; |
| 580 | (uint32 start, uint32 end) = |
| 581 | _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 | amt += _streamedAmt(receiver.config.amtPerSec(), start, end); |
| 585 | } |
| 586 | 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 | 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 | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 610 | return ( |
| 611 | state.streamsHash, |
| 612 | state.streamsHistoryHash, |
| 613 | state.updateTime, |
| 614 | state.balance, |
| 615 | 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 | function _balanceAt( |
| 630 | uint256 accountId, |
| 631 | IERC20 erc20, |
| 632 | StreamReceiver[] memory currReceivers, |
| 633 | uint32 timestamp |
| 634 | ) internal view returns (uint128 balance) { |
| 635 | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 636 | require(timestamp >= state.updateTime, "Timestamp before the last update"); |
| 637 | _verifyStreamsReceivers(currReceivers, state); |
| 638 | 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 | 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 | for (uint256 i = 0; i < receivers.length; i++) { |
| 661 | StreamReceiver memory receiver = receivers[i]; |
| 662 | (uint32 start, uint32 end) = _streamRange({ |
| 663 | receiver: receiver, |
| 664 | updateTime: lastUpdate, |
| 665 | maxEnd: maxEnd, |
| 666 | startCap: lastUpdate, |
| 667 | endCap: timestamp |
| 668 | }); |
| 669 | 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 | 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 | unchecked { |
| 726 | StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
| 727 | _verifyStreamsReceivers(currReceivers, state); |
| 728 | uint32 lastUpdate = state.updateTime; |
| 729 | uint128 newBalance; |
| 730 | uint32 newMaxEnd; |
| 731 | { |
| 732 | uint32 currMaxEnd = state.maxEnd; |
| 733 | int128 currBalance = int128( |
| 734 | _calcBalance( |
| 735 | state.balance, lastUpdate, currMaxEnd, currReceivers, _currTimestamp() |
| 736 | ) |
| 737 | ); |
| 738 | realBalanceDelta = balanceDelta; |
| 739 | // Cap `realBalanceDelta` at withdrawal of the entire `currBalance` |
| 740 | if (realBalanceDelta < -currBalance) { |
| 741 | 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 | newBalance = uint128(currBalance + realBalanceDelta); |
| 746 | newMaxEnd = _calcMaxEnd(newBalance, newReceivers, maxEndHint1, maxEndHint2); |
| 747 | _updateReceiverStates( |
| 748 | _streamsStorage().states[erc20], |
| 749 | currReceivers, |
| 750 | lastUpdate, |
| 751 | currMaxEnd, |
| 752 | newReceivers, |
| 753 | newMaxEnd |
| 754 | ); |
| 755 | } |
| 756 | state.updateTime = _currTimestamp(); |
| 757 | state.maxEnd = newMaxEnd; |
| 758 | state.balance = newBalance; |
| 759 | bytes32 streamsHistory = state.streamsHistoryHash; |
| 760 | // slither-disable-next-line timestamp |
| 761 | if (streamsHistory != 0 && _cycleOf(lastUpdate) != _cycleOf(_currTimestamp())) { |
| 762 | state.lastUpdatedCycleConfigs = 2; |
| 763 | } else { |
| 764 | state.lastUpdatedCycleConfigs++; |
| 765 | } |
| 766 | bytes32 newStreamsHash = _hashStreams(newReceivers); |
| 767 | state.streamsHistoryHash = |
| 768 | _hashStreamsHistory(streamsHistory, newStreamsHash, _currTimestamp(), newMaxEnd); |
| 769 | emit StreamsSet(accountId, erc20, newStreamsHash, streamsHistory, newBalance, newMaxEnd); |
| 770 | // slither-disable-next-line timestamp |
| 771 | if (newStreamsHash != state.streamsHash) { |
| 772 | state.streamsHash = newStreamsHash; |
| 773 | for (uint256 i = 0; i < newReceivers.length; i++) { |
| 774 | StreamReceiver memory receiver = newReceivers[i]; |
| 775 | 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 | function _verifyStreamsReceivers( |
| 785 | StreamReceiver[] memory currReceivers, |
| 786 | StreamsState storage state |
| 787 | ) private view { |
| 788 | 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 | function _calcMaxEnd( |
| 802 | uint128 balance, |
| 803 | StreamReceiver[] memory receivers, |
| 804 | uint32 hint1, |
| 805 | uint32 hint2 |
| 806 | ) private view returns (uint32 maxEnd) { |
| 807 | (uint256[] memory configs, uint256 configsLen) = _buildConfigs(receivers); |
| 808 | |
| 809 | uint256 enoughEnd = _currTimestamp(); |
| 810 | // slither-disable-start incorrect-equality,timestamp |
| 811 | if (configsLen == 0 || balance == 0) { |
| 812 | return uint32(enoughEnd); |
| 813 | } |
| 814 | |
| 815 | uint256 notEnoughEnd = type(uint32).max; |
| 816 | if (_isBalanceEnough(balance, configs, configsLen, notEnoughEnd)) { |
| 817 | return uint32(notEnoughEnd); |
| 818 | } |
| 819 | |
| 820 | if (hint1 > enoughEnd && hint1 < notEnoughEnd) { |
| 821 | if (_isBalanceEnough(balance, configs, configsLen, hint1)) { |
| 822 | enoughEnd = hint1; |
| 823 | } else { |
| 824 | notEnoughEnd = hint1; |
| 825 | } |
| 826 | } |
| 827 | |
| 828 | if (hint2 > enoughEnd && hint2 < notEnoughEnd) { |
| 829 | if (_isBalanceEnough(balance, configs, configsLen, hint2)) { |
| 830 | enoughEnd = hint2; |
| 831 | } else { |
| 832 | notEnoughEnd = hint2; |
| 833 | } |
| 834 | } |
| 835 | |
| 836 | while (true) { |
| 837 | uint256 end; |
| 838 | unchecked { |
| 839 | end = (enoughEnd + notEnoughEnd) / 2; |
| 840 | } |
| 841 | if (end == enoughEnd) { |
| 842 | return uint32(end); |
| 843 | } |
| 844 | 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 | 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 | for (uint256 i = 0; i < configsLen; i++) { |
| 868 | (uint256 amtPerSec, uint256 start, uint256 end) = _getConfig(configs, i); |
| 869 | // slither-disable-next-line timestamp |
| 870 | if (maxEnd <= start) { |
| 871 | continue; |
| 872 | } |
| 873 | // slither-disable-next-line timestamp |
| 874 | if (end > maxEnd) { |
| 875 | end = maxEnd; |
| 876 | } |
| 877 | spent += _streamedAmt(amtPerSec, start, end); |
| 878 | if (spent > balance) { |
| 879 | 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 | function _buildConfigs(StreamReceiver[] memory receivers) |
| 893 | private |
| 894 | view |
| 895 | returns (uint256[] memory configs, uint256 configsLen) |
| 896 | { |
| 897 | unchecked { |
| 898 | require(receivers.length <= _MAX_STREAMS_RECEIVERS, "Too many streams receivers"); |
| 899 | configs = new uint256[](receivers.length); |
| 900 | for (uint256 i = 0; i < receivers.length; i++) { |
| 901 | StreamReceiver memory receiver = receivers[i]; |
| 902 | if (i > 0) { |
| 903 | require(_isOrdered(receivers[i - 1], receiver), "Streams receivers not sorted"); |
| 904 | } |
| 905 | 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 | function _addConfig( |
| 916 | uint256[] memory configs, |
| 917 | uint256 configsLen, |
| 918 | StreamReceiver memory receiver |
| 919 | ) private view returns (uint256 newConfigsLen) { |
| 920 | uint160 amtPerSec = receiver.config.amtPerSec(); |
| 921 | require(amtPerSec >= _minAmtPerSec, "Stream receiver amtPerSec too low"); |
| 922 | (uint32 start, uint32 end) = |
| 923 | _streamRangeInFuture(receiver, _currTimestamp(), type(uint32).max); |
| 924 | // slither-disable-next-line incorrect-equality,timestamp |
| 925 | if (start == end) { |
| 926 | return configsLen; |
| 927 | } |
| 928 | // By assignment we get `config` value: |
| 929 | // `zeros (96 bits) | amtPerSec (160 bits)` |
| 930 | 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 | 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 | config = (config << 32) | end; |
| 941 | configs[configsLen] = config; |
| 942 | unchecked { |
| 943 | 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 | 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 | 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 | 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 | start = uint32(config >> 32); |
| 973 | // By casting down we get value: |
| 974 | // `end (32 bits)` |
| 975 | 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 | function _hashStreams(StreamReceiver[] memory receivers) |
| 984 | internal |
| 985 | pure |
| 986 | returns (bytes32 streamsHash) |
| 987 | { |
| 988 | if (receivers.length == 0) { |
| 989 | return bytes32(0); |
| 990 | } |
| 991 | 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 | function _hashStreamsHistory( |
| 1004 | bytes32 oldStreamsHistoryHash, |
| 1005 | bytes32 streamsHash, |
| 1006 | uint32 updateTime, |
| 1007 | uint32 maxEnd |
| 1008 | ) internal pure returns (bytes32 streamsHistoryHash) { |
| 1009 | 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 | 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 | while (true) { |
| 1038 | bool pickCurr = currIdx < currReceivers.length; |
| 1039 | // slither-disable-next-line uninitialized-local |
| 1040 | StreamReceiver memory currRecv; |
| 1041 | if (pickCurr) { |
| 1042 | currRecv = currReceivers[currIdx]; |
| 1043 | } |
| 1044 | |
| 1045 | bool pickNew = newIdx < newReceivers.length; |
| 1046 | // slither-disable-next-line uninitialized-local |
| 1047 | StreamReceiver memory newRecv; |
| 1048 | if (pickNew) { |
| 1049 | 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 | if (pickCurr && pickNew) { |
| 1055 | if ( |
| 1056 | currRecv.accountId != newRecv.accountId |
| 1057 | || currRecv.config.amtPerSec() != newRecv.config.amtPerSec() |
| 1058 | ) { |
| 1059 | pickCurr = _isOrdered(currRecv, newRecv); |
| 1060 | pickNew = !pickCurr; |
| 1061 | } |
| 1062 | } |
| 1063 | |
| 1064 | if (pickCurr && pickNew) { |
| 1065 | // Shift the existing stream to fulfil the new configuration |
| 1066 | StreamsState storage state = states[currRecv.accountId]; |
| 1067 | (uint32 currStart, uint32 currEnd) = |
| 1068 | _streamRangeInFuture(currRecv, lastUpdate, currMaxEnd); |
| 1069 | (uint32 newStart, uint32 newEnd) = |
| 1070 | _streamRangeInFuture(newRecv, _currTimestamp(), newMaxEnd); |
| 1071 | 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 | _addDeltaRange(state, currStart, newStart, -amtPerSec); |
| 1077 | _addDeltaRange(state, currEnd, newEnd, amtPerSec); |
| 1078 | // Ensure that the account receives the updated cycles |
| 1079 | uint32 currStartCycle = _cycleOf(currStart); |
| 1080 | 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 | if (currStartCycle > newStartCycle && state.nextReceivableCycle > newStartCycle) { |
| 1086 | state.nextReceivableCycle = newStartCycle; |
| 1087 | } |
| 1088 | } else if (pickCurr) { |
| 1089 | // Remove an existing stream |
| 1090 | // slither-disable-next-line similar-names |
| 1091 | StreamsState storage state = states[currRecv.accountId]; |
| 1092 | (uint32 start, uint32 end) = _streamRangeInFuture(currRecv, lastUpdate, currMaxEnd); |
| 1093 | // slither-disable-next-line similar-names |
| 1094 | int256 amtPerSec = int256(uint256(currRecv.config.amtPerSec())); |
| 1095 | _addDeltaRange(state, start, end, -amtPerSec); |
| 1096 | } else if (pickNew) { |
| 1097 | // Create a new stream |
| 1098 | StreamsState storage state = states[newRecv.accountId]; |
| 1099 | // slither-disable-next-line uninitialized-local |
| 1100 | (uint32 start, uint32 end) = |
| 1101 | _streamRangeInFuture(newRecv, _currTimestamp(), newMaxEnd); |
| 1102 | int256 amtPerSec = int256(uint256(newRecv.config.amtPerSec())); |
| 1103 | _addDeltaRange(state, start, end, amtPerSec); |
| 1104 | // Ensure that the account receives the updated cycles |
| 1105 | uint32 startCycle = _cycleOf(start); |
| 1106 | // slither-disable-next-line timestamp |
| 1107 | uint32 nextReceivableCycle = state.nextReceivableCycle; |
| 1108 | if (nextReceivableCycle == 0 || nextReceivableCycle > startCycle) { |
| 1109 | state.nextReceivableCycle = startCycle; |
| 1110 | } |
| 1111 | } else { |
| 1112 | break; |
| 1113 | } |
| 1114 | |
| 1115 | unchecked { |
| 1116 | if (pickCurr) { |
| 1117 | currIdx++; |
| 1118 | } |
| 1119 | if (pickNew) { |
| 1120 | 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 | function _streamRangeInFuture(StreamReceiver memory receiver, uint32 updateTime, uint32 maxEnd) |
| 1130 | private |
| 1131 | view |
| 1132 | returns (uint32 start, uint32 end) |
| 1133 | { |
| 1134 | 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 | 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 | start = receiver.config.start(); |
| 1152 | // slither-disable-start timestamp |
| 1153 | if (start == 0) { |
| 1154 | start = updateTime; |
| 1155 | } |
| 1156 | uint40 end; |
| 1157 | unchecked { |
| 1158 | end = uint40(start) + receiver.config.duration(); |
| 1159 | } |
| 1160 | // slither-disable-next-line incorrect-equality |
| 1161 | if (end == start || end > maxEnd) { |
| 1162 | end = maxEnd; |
| 1163 | } |
| 1164 | if (start < startCap) { |
| 1165 | start = startCap; |
| 1166 | } |
| 1167 | if (end > endCap) { |
| 1168 | end = endCap; |
| 1169 | } |
| 1170 | if (end < start) { |
| 1171 | end = start; |
| 1172 | } |
| 1173 | // slither-disable-end timestamp |
| 1174 | 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 | function _addDeltaRange(StreamsState storage state, uint32 start, uint32 end, int256 amtPerSec) |
| 1183 | private |
| 1184 | { |
| 1185 | // slither-disable-next-line incorrect-equality,timestamp |
| 1186 | if (start == end) { |
| 1187 | return; |
| 1188 | } |
| 1189 | mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
| 1190 | _addDelta(amtDeltas, start, amtPerSec); |
| 1191 | _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 | function _addDelta( |
| 1199 | mapping(uint32 cycle => AmtDelta) storage amtDeltas, |
| 1200 | uint256 timestamp, |
| 1201 | int256 amtPerSec |
| 1202 | ) private { |
| 1203 | 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 | int256 amtPerSecMultiplier = int160(_AMT_PER_SEC_MULTIPLIER); |
| 1207 | int256 fullCycle = (int256(uint256(_cycleSecs)) * amtPerSec) / amtPerSecMultiplier; |
| 1208 | // slither-disable-next-line weak-prng |
| 1209 | int256 nextCycle = (int256(timestamp % _cycleSecs) * amtPerSec) / amtPerSecMultiplier; |
| 1210 | 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 | amtDelta.thisCycle += int128(fullCycle - nextCycle); |
| 1216 | 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 | function _isOrdered(StreamReceiver memory prev, StreamReceiver memory next) |
| 1224 | private |
| 1225 | pure |
| 1226 | returns (bool) |
| 1227 | { |
| 1228 | if (prev.accountId != next.accountId) { |
| 1229 | return prev.accountId < next.accountId; |
| 1230 | } |
| 1231 | 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 | 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 | uint256 cycleSecs = _cycleSecs; |
| 1265 | // slither-disable-next-line assembly |
| 1266 | assembly { |
| 1267 | let endedCycles := sub(div(end, cycleSecs), div(start, cycleSecs)) |
| 1268 | // slither-disable-next-line divide-before-multiply |
| 1269 | let amtPerCycle := div(mul(cycleSecs, amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
| 1270 | amt := mul(endedCycles, amtPerCycle) |
| 1271 | // slither-disable-next-line weak-prng |
| 1272 | let amtEnd := div(mul(mod(end, cycleSecs), amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
| 1273 | amt := add(amt, amtEnd) |
| 1274 | // slither-disable-next-line weak-prng |
| 1275 | let amtStart := div(mul(mod(start, cycleSecs), amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
| 1276 | 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 | function _cycleOf(uint32 timestamp) private view returns (uint32 cycle) { |
| 1284 | unchecked { |
| 1285 | 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 | 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 | function _currCycleStart() private view returns (uint32 timestamp) { |
| 1298 | unchecked { |
| 1299 | uint32 currTimestamp = _currTimestamp(); |
| 1300 | // slither-disable-next-line weak-prng |
| 1301 | return currTimestamp - (currTimestamp % _cycleSecs); |
| 1302 | } |
| 1303 | } |
| 1304 | |
| 1305 | /// @notice Returns the Streams storage. |
| 1306 | /// @return streamsStorage The storage. |
| 1307 | function _streamsStorage() private view returns (StreamsStorage storage streamsStorage) { |
| 1308 | bytes32 slot = _streamsStorageSlot; |
| 1309 | // slither-disable-next-line assembly |
| 1310 | assembly { |
| 1311 | streamsStorage.slot := slot |
| 1312 | } |
| 1313 | } |
| 1314 | } |
| 1315 |
0.0%
src/echidna/Echidna.sol
Lines covered: 0 / 1 (0.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 | contract Echidna is |
| 29 | EchidnaBasicTests, |
| 30 | EchidnaSplitsTests, |
| 31 | EchidnaStreamsTests, |
| 32 | EchidnaSqueezeTests, |
| 33 | EchidnaInvariantTests |
| 34 | { |
| 35 | |
| 36 | } |
| 37 |
97.0%
src/echidna/EchidnaBasicHelpers.sol
Lines covered: 48 / 49 (97.0%)
| 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 | 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 | 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 | hevm.prank(from); |
| 27 | 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 | function giveClampedAmount( |
| 37 | uint8 fromAccId, |
| 38 | uint8 toAccId, |
| 39 | uint128 amount |
| 40 | ) public { |
| 41 | address from = getAccount(fromAccId); |
| 42 | |
| 43 | uint128 min = 1000; |
| 44 | uint128 max = uint128(token.balanceOf(from)); |
| 45 | 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 | function receiveStreams(uint8 targetAccId, uint32 maxCycles) |
| 59 | public |
| 60 | returns (uint128) |
| 61 | { |
| 62 | address target = getAccount(targetAccId); |
| 63 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 64 | |
| 65 | uint128 receivedAmt = drips.receiveStreams( |
| 66 | targetDripsAccId, |
| 67 | token, |
| 68 | maxCycles |
| 69 | ); |
| 70 | |
| 71 | 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 | function receiveStreamsAllCycles(uint8 targetAccId) |
| 81 | public |
| 82 | returns (uint128) |
| 83 | { |
| 84 | 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 | function split(uint8 targetAccId) public returns (uint128, uint128) { |
| 95 | address target = getAccount(targetAccId); |
| 96 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 97 | |
| 98 | (uint128 collectableAmt, uint128 splitAmt) = drips.split( |
| 99 | targetDripsAccId, |
| 100 | token, |
| 101 | getSplitsReceivers(target) |
| 102 | ); |
| 103 | |
| 104 | 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 | function collect(uint8 fromAccId, uint8 toAccId) public returns (uint128) { |
| 116 | address from = getAccount(fromAccId); |
| 117 | address to = getAccount(toAccId); |
| 118 | |
| 119 | hevm.prank(from); |
| 120 | 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 | function collectToSelf(uint8 targetAccId) public { |
| 130 | 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 | function splitAndCollectToSelf(uint8 targetAccId) public { |
| 139 | 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 | function receiveStreamsSplitAndCollectToSelf(uint8 targetAccId) public { |
| 149 | receiveStreamsAllCycles(targetAccId); |
| 150 | splitAndCollectToSelf(targetAccId); |
| 151 | } |
| 152 | } |
| 153 |
95.0%
src/echidna/EchidnaBasicTests.sol
Lines covered: 66 / 69 (95.0%)
| 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 | 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 | 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 | require(amount <= token.balanceOf(from)); |
| 28 | |
| 29 | hevm.prank(from); |
| 30 | try driver.give(toDripsAccId, token, amount) {} catch { |
| 31 | 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 | function testReceiveStreams(uint8 targetAccId, uint32 maxCycles) public { |
| 41 | address target = getAccount(targetAccId); |
| 42 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 43 | |
| 44 | uint128 splittableBefore = drips.splittable(targetDripsAccId, token); |
| 45 | uint128 receivedAmt = receiveStreams(targetAccId, maxCycles); |
| 46 | uint128 splittableAfter = drips.splittable(targetDripsAccId, token); |
| 47 | |
| 48 | assert(splittableAfter == splittableBefore + receivedAmt); |
| 49 | |
| 50 | if (receivedAmt > 0) { |
| 51 | assert(splittableAfter > splittableBefore); |
| 52 | } else { |
| 53 | assert(splittableAfter == splittableBefore); |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * @notice Receiving streams should never revert |
| 59 | * @param targetAccId Account id of the receiver |
| 60 | */ |
| 61 | function testReceiveStreamsShouldNotRevert(uint8 targetAccId) public { |
| 62 | address target = getAccount(targetAccId); |
| 63 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 64 | |
| 65 | try |
| 66 | 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 | function testReceiveStreamsViewConsistency( |
| 79 | uint8 targetAccId, |
| 80 | uint32 maxCycles |
| 81 | ) public { |
| 82 | address target = getAccount(targetAccId); |
| 83 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 84 | |
| 85 | require(maxCycles > 0); |
| 86 | |
| 87 | uint128 receivable = drips.receiveStreamsResult( |
| 88 | targetDripsAccId, |
| 89 | token, |
| 90 | maxCycles |
| 91 | ); |
| 92 | uint32 receivableCycles = drips.receivableStreamsCycles( |
| 93 | targetDripsAccId, |
| 94 | token |
| 95 | ); |
| 96 | |
| 97 | 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 | function testReceiveStreamsViewVsActual(uint8 targetAccId, uint32 maxCycles) |
| 109 | public |
| 110 | { |
| 111 | address target = getAccount(targetAccId); |
| 112 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 113 | |
| 114 | uint128 receivable = drips.receiveStreamsResult( |
| 115 | targetDripsAccId, |
| 116 | token, |
| 117 | maxCycles |
| 118 | ); |
| 119 | |
| 120 | uint128 received = drips.receiveStreams( |
| 121 | targetDripsAccId, |
| 122 | token, |
| 123 | maxCycles |
| 124 | ); |
| 125 | |
| 126 | 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 | 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 | uint128 colBalBefore = drips.collectable(fromDripsAccId, token); |
| 141 | uint256 tokenBalBefore = token.balanceOf(to); |
| 142 | |
| 143 | uint128 collected = collect(fromAccId, toAccId); |
| 144 | |
| 145 | uint128 colBalAfter = drips.collectable(fromDripsAccId, token); |
| 146 | uint256 tokenBalAfter = token.balanceOf(to); |
| 147 | |
| 148 | assert(colBalAfter == colBalBefore - collected); |
| 149 | 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 | function testCollectShouldNotRevert(uint8 fromAccId, uint8 toAccId) public { |
| 158 | address from = getAccount(fromAccId); |
| 159 | address to = getAccount(toAccId); |
| 160 | |
| 161 | hevm.prank(from); |
| 162 | try driver.collect(token, to) {} catch { |
| 163 | assert(false); |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 |
97.0%
src/echidna/EchidnaInvariantTests.sol
Lines covered: 69 / 71 (97.0%)
| 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 | 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 | function invariantWithdrawShouldAlwaysFail(uint256 amount) public { |
| 23 | require(amount > 0, "withdraw amount must be > 0"); |
| 24 | |
| 25 | try drips.withdraw(token, address(this), amount) { |
| 26 | 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 | function invariantAmtPerSecVsMinAmtPerSec(uint8 targetAccId, uint256 index) |
| 36 | public |
| 37 | { |
| 38 | address target = getAccount(targetAccId); |
| 39 | |
| 40 | StreamReceiver[] memory receivers = getStreamReceivers(target); |
| 41 | require(receivers.length > 0, "no receivers"); |
| 42 | |
| 43 | index = index % receivers.length; |
| 44 | uint160 amtPerSec = receivers[index].config.amtPerSec(); |
| 45 | |
| 46 | 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 | function invariantAccountingVsTokenBalance() public { |
| 54 | 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 | function invariantWithdrawAllTokens() external heavy { |
| 65 | // remove all splits to prevent tokens from getting stuck in case |
| 66 | // there are splits to self |
| 67 | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 68 | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 69 | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 70 | removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 71 | |
| 72 | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 73 | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 74 | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 75 | squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 76 | |
| 77 | receiveStreamsSplitAndCollectToSelf( |
| 78 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] |
| 79 | ); |
| 80 | receiveStreamsSplitAndCollectToSelf( |
| 81 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] |
| 82 | ); |
| 83 | receiveStreamsSplitAndCollectToSelf( |
| 84 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] |
| 85 | ); |
| 86 | receiveStreamsSplitAndCollectToSelf( |
| 87 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3] |
| 88 | ); |
| 89 | |
| 90 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 91 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 92 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 93 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 94 | |
| 95 | uint256 dripsBalance = token.balanceOf(address(drips)); |
| 96 | uint256 user0Balance = token.balanceOf(ADDRESS_USER0); |
| 97 | uint256 user1Balance = token.balanceOf(ADDRESS_USER1); |
| 98 | uint256 user2Balance = token.balanceOf(ADDRESS_USER2); |
| 99 | uint256 user3Balance = token.balanceOf(ADDRESS_USER3); |
| 100 | |
| 101 | uint256 totalUserBalance = user0Balance + |
| 102 | user1Balance + |
| 103 | user2Balance + |
| 104 | user3Balance; |
| 105 | |
| 106 | assert(dripsBalance == 0); |
| 107 | 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 | 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 | 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 | uint32 lastCycle = getCycleFromTimestamp(maxEnd); |
| 133 | |
| 134 | require(maxEnd > 0, "no cycles"); |
| 135 | require(firstCycle != lastCycle, "only one cycle"); |
| 136 | |
| 137 | // limit amount of cycles for gas & memory savings |
| 138 | require(lastCycle - firstCycle < 1000, "too many cycles"); |
| 139 | |
| 140 | int256 sumAmtDelta = 0; |
| 141 | |
| 142 | for (uint32 cycle = firstCycle; cycle <= lastCycle; cycle++) { |
| 143 | (int128 thisCycle, int128 nextCycle) = drips.getAmtDeltaForCycle( |
| 144 | targetDripsAccId, |
| 145 | token, |
| 146 | cycle |
| 147 | ); |
| 148 | |
| 149 | sumAmtDelta += int256(thisCycle); |
| 150 | sumAmtDelta += int256(nextCycle); |
| 151 | } |
| 152 | |
| 153 | assert(sumAmtDelta == 0); |
| 154 | } |
| 155 | } |
| 156 |
98.0%
src/echidna/EchidnaSplitsHelpers.sol
Lines covered: 54 / 55 (98.0%)
| 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 | 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 | address sender = getAccount(senderAccId); |
| 21 | |
| 22 | SplitsReceiver[] memory newReceivers = bubbleSortSplitsReceivers( |
| 23 | unsortedReceivers |
| 24 | ); |
| 25 | |
| 26 | updateSplitsReceivers(sender, newReceivers); |
| 27 | |
| 28 | hevm.prank(sender); |
| 29 | 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 | function setSplits( |
| 39 | uint8 senderAccId, |
| 40 | uint8 receiverAccId, |
| 41 | uint32 weight |
| 42 | ) public { |
| 43 | address sender = getAccount(senderAccId); |
| 44 | address receiver = getAccount(receiverAccId); |
| 45 | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 46 | |
| 47 | SplitsReceiver[] memory receivers = new SplitsReceiver[](1); |
| 48 | receivers[0] = SplitsReceiver({ |
| 49 | accountId: receiverDripsAccId, |
| 50 | weight: weight |
| 51 | }); |
| 52 | updateSplitsReceivers(sender, receivers); |
| 53 | |
| 54 | _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 | function setSplitsWithClamping( |
| 66 | uint8 senderAccId, |
| 67 | uint8 receiverAccId, |
| 68 | uint32 weight |
| 69 | ) public { |
| 70 | weight = clampSplitWeight(weight, 0); // there are no existing weights |
| 71 | 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 | function addSplitsReceiver( |
| 81 | uint8 senderAccId, |
| 82 | uint8 receiverAccId, |
| 83 | uint32 weight |
| 84 | ) public { |
| 85 | address sender = getAccount(senderAccId); |
| 86 | address receiver = getAccount(receiverAccId); |
| 87 | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 88 | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 89 | |
| 90 | SplitsReceiver[] memory oldReceivers = getSplitsReceivers(sender); |
| 91 | |
| 92 | SplitsReceiver memory addedReceiver = SplitsReceiver({ |
| 93 | accountId: receiverDripsAccId, |
| 94 | weight: weight |
| 95 | }); |
| 96 | |
| 97 | SplitsReceiver[] memory newReceivers = new SplitsReceiver[]( |
| 98 | oldReceivers.length + 1 |
| 99 | ); |
| 100 | for (uint256 i = 0; i < oldReceivers.length; i++) { |
| 101 | newReceivers[i] = oldReceivers[i]; |
| 102 | } |
| 103 | newReceivers[newReceivers.length - 1] = addedReceiver; |
| 104 | |
| 105 | _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 | function addSplitsReceiverWithClamping( |
| 117 | uint8 senderAccId, |
| 118 | uint8 receiverAccId, |
| 119 | uint32 weight |
| 120 | ) public { |
| 121 | address sender = getAccount(senderAccId); |
| 122 | |
| 123 | // sum all the existing weights |
| 124 | uint32 existingWeights; |
| 125 | SplitsReceiver[] memory receivers = getSplitsReceivers(sender); |
| 126 | for (uint256 i = 0; i < receivers.length; i++) { |
| 127 | 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 | if (existingWeights >= drips.TOTAL_SPLITS_WEIGHT()) return; |
| 133 | |
| 134 | weight = clampSplitWeight(weight, existingWeights); |
| 135 | |
| 136 | addSplitsReceiver(senderAccId, receiverAccId, weight); |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * @notice Remove any existing splits |
| 141 | * @param targetAccId Target account id |
| 142 | */ |
| 143 | function removeAllSplits(uint8 targetAccId) public { |
| 144 | SplitsReceiver[] memory receivers = new SplitsReceiver[](0); |
| 145 | _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 | function clampSplitWeight(uint32 weight, uint32 existingWeights) |
| 155 | public |
| 156 | view |
| 157 | returns (uint32) |
| 158 | { |
| 159 | return (weight % (drips.TOTAL_SPLITS_WEIGHT() - existingWeights)) + 1; |
| 160 | } |
| 161 | } |
| 162 |
96.0%
src/echidna/EchidnaSplitsTests.sol
Lines covered: 99 / 103 (96.0%)
| 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 | 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 | function testSplittableAfterSplit(uint8 targetAccId) public { |
| 21 | address target = getAccount(targetAccId); |
| 22 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 23 | |
| 24 | 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 | for (uint256 i = 0; i < receivers.length; i++) { |
| 30 | if (receivers[i].accountId == targetDripsAccId) { |
| 31 | splitToSelfWeight += receivers[i].weight; |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // calculate amount to split to self |
| 36 | uint128 splitToSelfAmount = uint128( |
| 37 | (splittableBefore * splitToSelfWeight) / drips.TOTAL_SPLITS_WEIGHT() |
| 38 | ); |
| 39 | |
| 40 | (uint128 collectableAmt, uint128 splitAmt) = split(targetAccId); |
| 41 | |
| 42 | uint128 splittableAfter = drips.splittable(targetDripsAccId, token); |
| 43 | |
| 44 | // sanity check |
| 45 | assert((splitAmt + collectableAmt) <= splittableBefore); |
| 46 | |
| 47 | if (splitToSelfWeight == 0) { |
| 48 | // if we're not splitting to ourselves, things are simple |
| 49 | assert( |
| 50 | 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 | uint128 expectedSplittableAfter = splittableBefore - |
| 58 | splitAmt - |
| 59 | collectableAmt + |
| 60 | splitToSelfAmount; |
| 61 | |
| 62 | // calculate difference between expected and actual |
| 63 | int256 difference = int256(uint256(splittableAfter)) - |
| 64 | int256(uint256(expectedSplittableAfter)); |
| 65 | |
| 66 | // check if difference is within tolerance |
| 67 | assert( |
| 68 | 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 | function testCollectableAfterSplit(uint8 targetAccId) public { |
| 79 | address target = getAccount(targetAccId); |
| 80 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 81 | |
| 82 | uint128 colBalBefore = drips.collectable(targetDripsAccId, token); |
| 83 | (uint128 collectableAmt, ) = split(targetAccId); |
| 84 | uint128 colBalAfter = drips.collectable(targetDripsAccId, token); |
| 85 | |
| 86 | 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 | function testReceiversReceivedSplit(uint8 targetAccId) public { |
| 95 | address target = getAccount(targetAccId); |
| 96 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 97 | |
| 98 | uint128 amountToBeSplit = drips.splittable(targetDripsAccId, token); |
| 99 | SplitsReceiver[] memory receivers = getSplitsReceivers(target); |
| 100 | |
| 101 | // storage for all receivers |
| 102 | uint128[] memory splittableBefore = new uint128[](receivers.length); |
| 103 | uint128[] memory splittableAfter = new uint128[](receivers.length); |
| 104 | uint32[] memory weights = new uint32[](receivers.length); |
| 105 | uint128[] memory amounts = new uint128[](receivers.length); |
| 106 | |
| 107 | for (uint256 i = 0; i < receivers.length; i++) { |
| 108 | // store splittable before |
| 109 | splittableBefore[i] = drips.splittable( |
| 110 | receivers[i].accountId, |
| 111 | token |
| 112 | ); |
| 113 | |
| 114 | // calculate amount the receiver should get |
| 115 | weights[i] = receivers[i].weight; |
| 116 | amounts[i] = uint128( |
| 117 | (amountToBeSplit * weights[i]) / drips.TOTAL_SPLITS_WEIGHT() |
| 118 | ); |
| 119 | } |
| 120 | |
| 121 | // split |
| 122 | (uint128 collectableAmt, uint128 splitAmt) = split(targetAccId); |
| 123 | |
| 124 | // store splittable after |
| 125 | for (uint256 i = 0; i < receivers.length; i++) { |
| 126 | splittableAfter[i] = drips.splittable( |
| 127 | receivers[i].accountId, |
| 128 | token |
| 129 | ); |
| 130 | } |
| 131 | |
| 132 | for (uint256 i = 0; i < receivers.length; i++) { |
| 133 | // calculate expected amount after the split |
| 134 | uint128 expectedAfter; |
| 135 | if (receivers[i].accountId != targetDripsAccId) { |
| 136 | // splitting so someone else is trivial |
| 137 | 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 | splittableBefore[i] - |
| 145 | amountToBeSplit + |
| 146 | amounts[i]; |
| 147 | } |
| 148 | |
| 149 | // calculate difference between expected and actual |
| 150 | int256 difference = int256(uint256(splittableAfter[i])) - |
| 151 | int256(uint256(expectedAfter)); |
| 152 | |
| 153 | // check if difference is within tolerance |
| 154 | assert( |
| 155 | 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 | function testSplitViewVsActual(uint8 targetAccId) public { |
| 166 | address target = getAccount(targetAccId); |
| 167 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 168 | |
| 169 | uint128 splittable = drips.splittable(targetDripsAccId, token); |
| 170 | |
| 171 | (uint128 collectableAmtView, uint128 splitAmtView) = drips.splitResult( |
| 172 | targetDripsAccId, |
| 173 | getSplitsReceivers(target), |
| 174 | splittable |
| 175 | ); |
| 176 | |
| 177 | (uint128 collectableAmtActual, uint128 splitAmtActual) = split( |
| 178 | targetAccId |
| 179 | ); |
| 180 | |
| 181 | assert(collectableAmtView == collectableAmtActual); |
| 182 | assert(splitAmtView == splitAmtActual); |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * @notice Splitting should never revert |
| 187 | * @param targetAccId Account id execute split on |
| 188 | */ |
| 189 | function testSplitShouldNotRevert(uint8 targetAccId) public { |
| 190 | address target = getAccount(targetAccId); |
| 191 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 192 | |
| 193 | try EchidnaBasicHelpers(address(this)).split(targetAccId) {} catch { |
| 194 | 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 | function testSetSplitsShouldNotRevert( |
| 205 | uint8 senderAccId, |
| 206 | uint8 receiverAccId, |
| 207 | uint32 weight |
| 208 | ) public { |
| 209 | try |
| 210 | EchidnaSplitsHelpers(address(this)).setSplitsWithClamping( |
| 211 | senderAccId, |
| 212 | receiverAccId, |
| 213 | weight |
| 214 | ) |
| 215 | {} catch { |
| 216 | 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 | function testAddSplitsShouldNotRevert( |
| 227 | uint8 senderAccId, |
| 228 | uint8 receiverAccId, |
| 229 | uint32 weight |
| 230 | ) public { |
| 231 | try |
| 232 | EchidnaSplitsHelpers(address(this)).addSplitsReceiverWithClamping( |
| 233 | senderAccId, |
| 234 | receiverAccId, |
| 235 | weight |
| 236 | ) |
| 237 | {} catch (bytes memory reason) { |
| 238 | bytes4 errorSelector = bytes4(reason); |
| 239 | if (errorSelector == EchidnaStorage.DuplicateError.selector) { |
| 240 | // ignore this case, it means we tried to add a duplicate stream |
| 241 | } else { |
| 242 | assert(false); |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 |
98.0%
src/echidna/EchidnaSqueezeHelpers.sol
Lines covered: 75 / 76 (98.0%)
| 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 | 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 | function _squeeze( |
| 20 | uint8 receiverAccId, |
| 21 | uint8 senderAccId, |
| 22 | bytes32 historyHash, |
| 23 | StreamsHistory[] memory history |
| 24 | ) internal returns (uint128) { |
| 25 | address receiver = getAccount(receiverAccId); |
| 26 | address sender = getAccount(senderAccId); |
| 27 | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 28 | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 29 | |
| 30 | uint128 amount = drips.squeezeStreams( |
| 31 | receiverDripsAccId, |
| 32 | token, |
| 33 | senderDripsAccId, |
| 34 | historyHash, |
| 35 | history |
| 36 | ); |
| 37 | |
| 38 | 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 | function squeezeWithDefaultHistory(uint8 receiverAccId, uint8 senderAccId) |
| 48 | public |
| 49 | returns (uint128) |
| 50 | { |
| 51 | return |
| 52 | _squeeze( |
| 53 | receiverAccId, |
| 54 | senderAccId, |
| 55 | bytes32(0), |
| 56 | 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 | function squeezeWithFuzzedHistory( |
| 72 | uint8 receiverAccId, |
| 73 | uint8 senderAccId, |
| 74 | uint256 hashIndex, |
| 75 | bytes32 receiversRandomSeed |
| 76 | ) public returns (uint128) { |
| 77 | address receiver = getAccount(receiverAccId); |
| 78 | address sender = getAccount(senderAccId); |
| 79 | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 80 | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 81 | |
| 82 | ( |
| 83 | bytes32 historyHash, |
| 84 | StreamsHistory[] memory history |
| 85 | ) = getFuzzedStreamsHistory( |
| 86 | senderAccId, |
| 87 | hashIndex, |
| 88 | receiversRandomSeed |
| 89 | ); |
| 90 | |
| 91 | 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 | 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 | function squeezeAllSenders(uint8 targetAccId) public { |
| 108 | squeezeWithDefaultHistory( |
| 109 | targetAccId, |
| 110 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] |
| 111 | ); |
| 112 | squeezeWithDefaultHistory( |
| 113 | targetAccId, |
| 114 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] |
| 115 | ); |
| 116 | squeezeWithDefaultHistory( |
| 117 | targetAccId, |
| 118 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] |
| 119 | ); |
| 120 | squeezeWithDefaultHistory( |
| 121 | targetAccId, |
| 122 | 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 | 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 | function getFuzzedStreamsHistory( |
| 147 | uint8 targetAccId, |
| 148 | uint256 hashIndex, |
| 149 | bytes32 receiversRandomSeed |
| 150 | ) internal returns (bytes32, StreamsHistory[] memory) { |
| 151 | address target = getAccount(targetAccId); |
| 152 | |
| 153 | // get the history structs and hashes |
| 154 | StreamsHistory[] memory historyStructs = getStreamsHistory(target); |
| 155 | bytes32[] memory historyHashes = getStreamsHistoryHashes(target); |
| 156 | |
| 157 | // having a hashed history requires at least 2 history entries |
| 158 | require(historyStructs.length >= 2, "need at least 2 history entries"); |
| 159 | |
| 160 | // hashIndex must be within bounds and cant be the last entry |
| 161 | hashIndex = hashIndex % (historyHashes.length - 1); |
| 162 | |
| 163 | // get the history hash at the index |
| 164 | bytes32 historyHash = historyHashes[hashIndex]; |
| 165 | |
| 166 | // create a history array with all entries after the hashIndex |
| 167 | StreamsHistory[] memory history = new StreamsHistory[]( |
| 168 | historyStructs.length - 1 - hashIndex |
| 169 | ); |
| 170 | for (uint256 i = hashIndex + 1; i < historyStructs.length; i++) { |
| 171 | history[i - hashIndex - 1] = historyStructs[i]; |
| 172 | } |
| 173 | |
| 174 | // hash receivers based on 'receiversRandomSeed' |
| 175 | for (uint256 i = 0; i < history.length; i++) { |
| 176 | receiversRandomSeed = keccak256(bytes.concat(receiversRandomSeed)); |
| 177 | bool hashBool = (uint256(receiversRandomSeed) % 2) == 0 |
| 178 | ? false |
| 179 | : true; |
| 180 | |
| 181 | if (hashBool) { |
| 182 | history[i].streamsHash = drips.hashStreams( |
| 183 | history[i].receivers |
| 184 | ); |
| 185 | history[i].receivers = new StreamReceiver[](0); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | return (historyHash, history); |
| 190 | } |
| 191 | } |
| 192 |
99.0%
src/echidna/EchidnaSqueezeTests.sol
Lines covered: 112 / 113 (99.0%)
| 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 | 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 | 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 | uint128 splittableBefore = drips.splittable(receiverDripsAccId, token); |
| 31 | |
| 32 | uint128 squeezedAmt = squeezeWithDefaultHistory( |
| 33 | receiverAccId, |
| 34 | senderAccId |
| 35 | ); |
| 36 | |
| 37 | uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
| 38 | uint128 splittableAfter = drips.splittable(receiverDripsAccId, token); |
| 39 | |
| 40 | assert(squeezableAfter == squeezableBefore - squeezedAmt); |
| 41 | assert(splittableAfter == splittableBefore + squeezedAmt); |
| 42 | |
| 43 | if (squeezedAmt > 0) { |
| 44 | assert(squeezableAfter < squeezableBefore); |
| 45 | assert(splittableAfter > splittableBefore); |
| 46 | } else { |
| 47 | assert(squeezableAfter == squeezableBefore); |
| 48 | 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 | 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 | 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 | 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 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
| 86 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
| 87 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
| 88 | setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
| 89 | |
| 90 | // warp to the point in time where the streams are receivable |
| 91 | hevm.warp(getCurrentCycleEnd() + 1); |
| 92 | |
| 93 | uint128 receivableAfter = getReceivableAmountForUser(target); |
| 94 | |
| 95 | // sanity check |
| 96 | assert(receivableAfter >= receivableBefore); |
| 97 | |
| 98 | uint128 receiveableDelta = receivableAfter - receivableBefore; |
| 99 | |
| 100 | // squeezable before should match receivable now |
| 101 | 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 | 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 | for (uint256 i = 0; i < history.length; i++) { |
| 120 | history[i].streamsHash = drips.hashStreams(history[i].receivers); |
| 121 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | function testSqueezeWithDefaultHistoryShouldNotRevert( |
| 237 | uint8 receiverAccId, |
| 238 | uint8 senderAccId |
| 239 | ) public { |
| 240 | try |
| 241 | EchidnaSqueezeHelpers(address(this)).squeezeWithDefaultHistory( |
| 242 | receiverAccId, |
| 243 | senderAccId |
| 244 | ) |
| 245 | {} catch { |
| 246 | 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 | function testSqueezeWithFuzzedHistoryShouldNotRevert( |
| 259 | uint8 receiverAccId, |
| 260 | uint8 senderAccId, |
| 261 | uint256 hashIndex, |
| 262 | bytes32 receiversRandomSeed |
| 263 | ) public { |
| 264 | address sender = getAccount(senderAccId); |
| 265 | require( |
| 266 | getStreamsHistory(sender).length >= 2, |
| 267 | "need at least 2 history entries" |
| 268 | ); |
| 269 | |
| 270 | try |
| 271 | EchidnaSqueezeHelpers(address(this)).squeezeWithFuzzedHistory( |
| 272 | receiverAccId, |
| 273 | senderAccId, |
| 274 | hashIndex, |
| 275 | receiversRandomSeed |
| 276 | ) |
| 277 | {} catch { |
| 278 | assert(false); |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | } |
| 283 |
98.0%
src/echidna/EchidnaStreamsHelpers.sol
Lines covered: 163 / 165 (98.0%)
| 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 | 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 | function _setStreams( |
| 31 | address from, |
| 32 | StreamReceiver[] memory currReceivers, |
| 33 | int128 balanceDelta, |
| 34 | StreamReceiver[] memory unsortedNewReceivers |
| 35 | ) internal returns (int128) { |
| 36 | StreamReceiver[] memory newReceivers = bubbleSortStreamReceivers( |
| 37 | unsortedNewReceivers |
| 38 | ); |
| 39 | |
| 40 | hevm.prank(from); |
| 41 | int128 realBalanceDelta = driver.setStreams( |
| 42 | token, |
| 43 | currReceivers, |
| 44 | balanceDelta, |
| 45 | newReceivers, |
| 46 | maxEndHint1, |
| 47 | maxEndHint2, |
| 48 | from |
| 49 | ); |
| 50 | |
| 51 | updateStreamReceivers(from, newReceivers); |
| 52 | |
| 53 | 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 | 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 | address from = getAccount(fromAccId); |
| 75 | address to = getAccount(toAccId); |
| 76 | |
| 77 | StreamReceiver[] memory receivers = new StreamReceiver[](1); |
| 78 | receivers[0] = StreamReceiver( |
| 79 | getDripsAccountId(to), |
| 80 | StreamConfigImpl.create( |
| 81 | 0, // streamId is arbitrary and can be ignored |
| 82 | amountPerSec, |
| 83 | startTime, |
| 84 | duration |
| 85 | ) |
| 86 | ); |
| 87 | |
| 88 | int128 realBalanceDelta = _setStreams( |
| 89 | from, |
| 90 | getStreamReceivers(from), |
| 91 | balanceDelta, |
| 92 | receivers |
| 93 | ); |
| 94 | |
| 95 | 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 | 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 | address from = getAccount(fromAccId); |
| 119 | address to = getAccount(toAccId); |
| 120 | |
| 121 | amountPerSec = clampAmountPerSec(amountPerSec); |
| 122 | startTime = clampStartTime(startTime); |
| 123 | duration = clampDuration(duration); |
| 124 | balanceDelta = clampBalanceDelta(balanceDelta, from); |
| 125 | |
| 126 | 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 | 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 | address from = getAccount(fromAccId); |
| 155 | address to = getAccount(toAccId); |
| 156 | |
| 157 | StreamReceiver[] memory oldReceivers = getStreamReceivers(from); |
| 158 | |
| 159 | StreamReceiver memory addedReceiver = StreamReceiver( |
| 160 | getDripsAccountId(to), |
| 161 | StreamConfigImpl.create( |
| 162 | 0, // streamId is arbitrary and can be ignored |
| 163 | amountPerSec, |
| 164 | startTime, |
| 165 | duration |
| 166 | ) |
| 167 | ); |
| 168 | |
| 169 | StreamReceiver[] memory newReceivers = new StreamReceiver[]( |
| 170 | oldReceivers.length + 1 |
| 171 | ); |
| 172 | for (uint256 i = 0; i < oldReceivers.length; i++) { |
| 173 | newReceivers[i] = oldReceivers[i]; |
| 174 | } |
| 175 | newReceivers[newReceivers.length - 1] = addedReceiver; |
| 176 | |
| 177 | int128 realBalanceDelta = _setStreams( |
| 178 | from, |
| 179 | oldReceivers, |
| 180 | balanceDelta, |
| 181 | newReceivers |
| 182 | ); |
| 183 | |
| 184 | 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 | 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 | address from = getAccount(fromAccId); |
| 207 | |
| 208 | amountPerSec = clampAmountPerSec(amountPerSec); |
| 209 | startTime = clampStartTime(startTime); |
| 210 | duration = clampDuration(duration); |
| 211 | 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 | 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 | uint160 minAmtPerSec = drips.minAmtPerSec() * SECONDS_PER_CYCLE; |
| 244 | amountPerSec = |
| 245 | minAmtPerSec + |
| 246 | (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 | int128 balanceDelta = (int128(uint128(amountPerSec)) * 100) / 1e9; |
| 251 | if (uint128(balanceDelta) > token.balanceOf(sender)) { |
| 252 | balanceDelta = int128(uint128(token.balanceOf(sender))); |
| 253 | } |
| 254 | |
| 255 | // add the stream |
| 256 | addStream(fromAccId, toAccId, amountPerSec, 0, 0, balanceDelta); |
| 257 | |
| 258 | // warp 1 second forward so there is something to squeeze |
| 259 | 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 | function removeStream(uint8 targetAccId, uint256 indexSeed) public { |
| 268 | address target = getAccount(targetAccId); |
| 269 | |
| 270 | StreamReceiver[] memory oldReceivers = getStreamReceivers(target); |
| 271 | |
| 272 | uint256 index = indexSeed % oldReceivers.length; |
| 273 | |
| 274 | StreamReceiver[] memory newReceivers = new StreamReceiver[]( |
| 275 | oldReceivers.length - 1 |
| 276 | ); |
| 277 | uint256 j = 0; |
| 278 | for (uint256 i = 0; i < oldReceivers.length; i++) { |
| 279 | if (i != index) { |
| 280 | newReceivers[j] = oldReceivers[i]; |
| 281 | j++; |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | _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 | function setStreamBalance(uint8 targetAccId, int128 balanceDelta) |
| 296 | public |
| 297 | returns (int128) |
| 298 | { |
| 299 | address target = getAccount(targetAccId); |
| 300 | |
| 301 | int128 realBalanceDelta = _setStreams( |
| 302 | target, |
| 303 | getStreamReceivers(target), |
| 304 | balanceDelta, |
| 305 | getStreamReceivers(target) |
| 306 | ); |
| 307 | |
| 308 | 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 | function setStreamBalanceWithClamping( |
| 320 | uint8 targetAccId, |
| 321 | int128 balanceDelta |
| 322 | ) public { |
| 323 | address target = getAccount(targetAccId); |
| 324 | balanceDelta = clampBalanceDelta(balanceDelta, target); |
| 325 | 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 | function setStreamBalanceWithdrawAll(uint8 targetAccId) |
| 335 | public |
| 336 | returns (int128) |
| 337 | { |
| 338 | address target = getAccount(targetAccId); |
| 339 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 340 | |
| 341 | int128 realBalanceDelta = _setStreams( |
| 342 | target, |
| 343 | getStreamReceivers(target), |
| 344 | type(int128).min, |
| 345 | 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 | function setMaxEndHints(uint32 _maxEndHint1, uint32 _maxEndHint2) public { |
| 356 | require(TOGGLE_MAXENDHINTS_ENABLED); |
| 357 | maxEndHint1 = _maxEndHint1; |
| 358 | 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 | function clampAmountPerSec(uint160 amountPerSec) |
| 367 | internal |
| 368 | returns (uint160) |
| 369 | { |
| 370 | return |
| 371 | drips.minAmtPerSec() + |
| 372 | (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 | function clampStartTime(uint32 startTime) internal returns (uint32) { |
| 381 | 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 | if (CYCLE_FUZZING_BUFFER_SECONDS >= block.timestamp) { |
| 386 | minStartTime = 1; |
| 387 | } else { |
| 388 | minStartTime = |
| 389 | uint32(block.timestamp) - |
| 390 | CYCLE_FUZZING_BUFFER_SECONDS; |
| 391 | } |
| 392 | |
| 393 | uint32 maxStartTime = uint32(block.timestamp) + |
| 394 | CYCLE_FUZZING_BUFFER_SECONDS; |
| 395 | |
| 396 | 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 | function clampDuration(uint32 duration) internal returns (uint32) { |
| 405 | if (duration == 0) return 0; |
| 406 | |
| 407 | 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 | function clampBalanceDelta(int128 balanceDelta, address from) |
| 417 | internal |
| 418 | returns (int128) |
| 419 | { |
| 420 | if (balanceDelta > 0) { |
| 421 | balanceDelta = |
| 422 | balanceDelta % |
| 423 | (int128(uint128(token.balanceOf(from))) + 1); |
| 424 | } else { |
| 425 | balanceDelta = balanceDelta % int128(uint128(STARTING_BALANCE)); |
| 426 | } |
| 427 | return balanceDelta; |
| 428 | } |
| 429 | } |
| 430 |
98.0%
src/echidna/EchidnaStreamsTests.sol
Lines covered: 116 / 118 (98.0%)
| 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 | 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 | function testSetStreamBalance(uint8 targetAccId, int128 balanceDelta) |
| 17 | public |
| 18 | { |
| 19 | address target = getAccount(targetAccId); |
| 20 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 21 | |
| 22 | uint256 tokenBalanceBefore = token.balanceOf(target); |
| 23 | uint128 streamBalanceBefore = drips.balanceAt( |
| 24 | targetDripsAccId, |
| 25 | token, |
| 26 | getStreamReceivers(target), |
| 27 | uint32(block.timestamp) |
| 28 | ); |
| 29 | |
| 30 | int128 realBalanceDelta = setStreamBalance(targetAccId, balanceDelta); |
| 31 | |
| 32 | uint256 tokenBalanceAfter = token.balanceOf(target); |
| 33 | uint128 streamBalanceAfter = drips.balanceAt( |
| 34 | targetDripsAccId, |
| 35 | token, |
| 36 | getStreamReceivers(target), |
| 37 | uint32(block.timestamp) |
| 38 | ); |
| 39 | |
| 40 | if (balanceDelta >= 0) { |
| 41 | assert(realBalanceDelta == balanceDelta); |
| 42 | } else { |
| 43 | assert(realBalanceDelta <= 0); |
| 44 | assert(realBalanceDelta >= balanceDelta); |
| 45 | } |
| 46 | |
| 47 | assert( |
| 48 | int256(tokenBalanceAfter) == |
| 49 | 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 | function testBalanceAtInFuture( |
| 67 | uint8 fromAccId, |
| 68 | uint8 toAccId, |
| 69 | uint160 amtPerSecAdded |
| 70 | ) 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 | 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 | hevm.warp(futureTimestamp); |
| 91 | uint128 balanceBaseline = getStreamBalanceForUser( |
| 92 | from, |
| 93 | uint32(block.timestamp) |
| 94 | ); |
| 95 | uint128 receivableBaseline = getReceivableAmountForAllUsers(); |
| 96 | hevm.warp(currentTimestamp); |
| 97 | |
| 98 | // add a stream |
| 99 | // make sure we add enough balance to complete the cycle |
| 100 | uint128 balanceAdded = uint128(amtPerSecAdded) * SECONDS_PER_CYCLE; |
| 101 | balanceAdded = uint128(clampBalanceDelta(int128(balanceAdded), from)); |
| 102 | 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 | 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 | assert(balanceInitial >= balanceBaseline); |
| 131 | assert(balanceBefore >= balanceAfter); |
| 132 | assert(receivableAfter >= receivableBaseline); |
| 133 | |
| 134 | // the amount that would have been streamed if we do nothing |
| 135 | uint128 baselineBalanceStreamed = balanceInitial - balanceBaseline; |
| 136 | |
| 137 | // calculate expected balance change including the effect of the |
| 138 | // added stream |
| 139 | uint128 expectedBalanceChange = balanceBefore - |
| 140 | balanceAfter - |
| 141 | baselineBalanceStreamed; |
| 142 | uint128 expectedReceivedChange = receivableAfter - receivableBaseline; |
| 143 | |
| 144 | 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 | 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 | EchidnaStreamsHelpers(address(this)).setStreamsWithClamping( |
| 166 | fromAccId, |
| 167 | toAccId, |
| 168 | amountPerSec, |
| 169 | startTime, |
| 170 | duration, |
| 171 | balanceDelta |
| 172 | ) |
| 173 | {} catch { |
| 174 | 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 | 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 | EchidnaStreamsHelpers(address(this)).addStreamWithClamping( |
| 197 | fromAccId, |
| 198 | toAccId, |
| 199 | amountPerSec, |
| 200 | startTime, |
| 201 | duration, |
| 202 | balanceDelta |
| 203 | ) |
| 204 | {} catch (bytes memory reason) { |
| 205 | bytes4 errorSelector = bytes4(reason); |
| 206 | if (errorSelector == EchidnaStorage.DuplicateError.selector) { |
| 207 | // ignore this case, it means we tried to add a duplicate stream |
| 208 | } else { |
| 209 | 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 | function testRemoveStreamShouldNotRevert( |
| 220 | uint8 targetAccId, |
| 221 | uint256 indexSeed |
| 222 | ) public { |
| 223 | address target = getAccount(targetAccId); |
| 224 | require(getStreamReceivers(target).length > 0); |
| 225 | |
| 226 | try |
| 227 | EchidnaStreamsHelpers(address(this)).removeStream( |
| 228 | targetAccId, |
| 229 | indexSeed |
| 230 | ) |
| 231 | {} catch { |
| 232 | 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 | function testSetStreamBalanceShouldNotRevert( |
| 242 | uint8 targetAccId, |
| 243 | int128 balanceDelta |
| 244 | ) public { |
| 245 | try |
| 246 | EchidnaStreamsHelpers(address(this)).setStreamBalanceWithClamping( |
| 247 | targetAccId, |
| 248 | balanceDelta |
| 249 | ) |
| 250 | {} catch { |
| 251 | 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 | function testSetStreamBalanceWithdrawAllShouldNotRevert(uint8 targetAccId) |
| 260 | public |
| 261 | { |
| 262 | try |
| 263 | EchidnaStreamsHelpers(address(this)).setStreamBalanceWithdrawAll( |
| 264 | targetAccId |
| 265 | ) |
| 266 | {} catch { |
| 267 | assert(false); |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 |
97.0%
src/echidna/base/EchidnaAccounting.sol
Lines covered: 87 / 89 (97.0%)
| 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 | 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 | 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 | uint128 collectable = drips.collectable(targetDripsAccId, token); |
| 44 | uint128 splittable = drips.splittable(targetDripsAccId, token); |
| 45 | |
| 46 | 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 | function getStreamBalanceForUser(address target, uint32 timestamp) |
| 58 | internal |
| 59 | returns (uint128) |
| 60 | { |
| 61 | uint256 targetDripsAccId = getDripsAccountId(target); |
| 62 | |
| 63 | uint128 balance; |
| 64 | try |
| 65 | drips.balanceAt( |
| 66 | targetDripsAccId, |
| 67 | token, |
| 68 | 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 | assert(false); |
| 76 | } |
| 77 | |
| 78 | 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 | receivable += getReceivableAmountForUser(ADDRESS_USER0); |
| 102 | receivable += getReceivableAmountForUser(ADDRESS_USER1); |
| 103 | receivable += getReceivableAmountForUser(ADDRESS_USER2); |
| 104 | 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 | function getReceivableAmountForUser(address target) |
| 116 | internal |
| 117 | returns (uint128) |
| 118 | { |
| 119 | uint128 receivable = drips.receiveStreamsResult( |
| 120 | getDripsAccountId(target), |
| 121 | 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 | amount += getSqueezableAmount(ADDRESS_USER0, target); |
| 141 | amount += getSqueezableAmount(ADDRESS_USER1, target); |
| 142 | amount += getSqueezableAmount(ADDRESS_USER2, target); |
| 143 | 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 | function getSqueezableAmount(address sender, address receiver) |
| 158 | internal |
| 159 | returns (uint128) |
| 160 | { |
| 161 | uint256 senderDripsAccId = getDripsAccountId(sender); |
| 162 | uint256 receiverDripsAccId = getDripsAccountId(receiver); |
| 163 | |
| 164 | uint128 amount = drips.squeezeStreamsResult( |
| 165 | receiverDripsAccId, |
| 166 | token, |
| 167 | senderDripsAccId, |
| 168 | bytes32(0), |
| 169 | getStreamsHistory(sender) |
| 170 | ); |
| 171 | |
| 172 | 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 | if (maxEndUser0 > maxMaxEnd) maxMaxEnd = maxEndUser0; |
| 186 | uint32 maxEndUser1 = getMaxEndForUser(ADDRESS_USER1); |
| 187 | if (maxEndUser1 > maxMaxEnd) maxMaxEnd = maxEndUser1; |
| 188 | uint32 maxEndUser2 = getMaxEndForUser(ADDRESS_USER2); |
| 189 | if (maxEndUser2 > maxMaxEnd) maxMaxEnd = maxEndUser2; |
| 190 | uint32 maxEndUser3 = getMaxEndForUser(ADDRESS_USER3); |
| 191 | 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 | (, , , , 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 | 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 | 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 | return uint32(timestamp / SECONDS_PER_CYCLE + 1); |
| 234 | } |
| 235 | } |
| 236 |
0.0%
src/echidna/base/EchidnaBase.sol
Lines covered: 0 / 1 (0.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 | contract EchidnaBase is |
| 12 | EchidnaSetup, |
| 13 | EchidnaStorage, |
| 14 | EchidnaAccounting |
| 15 | {} |
| 16 |
88.0%
src/echidna/base/EchidnaConfig.sol
Lines covered: 23 / 26 (88.0%)
| 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 | contract EchidnaConfig { |
| 18 | // Addresses used for the simulated users |
| 19 | address internal constant ADDRESS_USER0 = address(0x10000); |
| 20 | address internal constant ADDRESS_USER1 = address(0x20000); |
| 21 | address internal constant ADDRESS_USER2 = address(0x30000); |
| 22 | 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 | uint256 internal constant STARTING_BALANCE = 1_000_000_000e18; |
| 33 | |
| 34 | // Amount of seconds in a Drips cycle |
| 35 | 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 | 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 | (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 | if (!TOGGLE_HEAVY_TESTS_ENABLED) return; |
| 69 | _; |
| 70 | } |
| 71 | |
| 72 | constructor() { |
| 73 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] = 0; |
| 74 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] = 64; |
| 75 | ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] = 128; |
| 76 | 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 | function getAccount(uint8 rawId) internal pure returns (address) { |
| 86 | uint256 id = uint256(rawId) / 64; |
| 87 | |
| 88 | if (id == 0) return ADDRESS_USER0; |
| 89 | if (id == 1) return ADDRESS_USER1; |
| 90 | if (id == 2) return ADDRESS_USER2; |
| 91 | if (id == 3) return ADDRESS_USER3; |
| 92 | |
| 93 | require(false, "Unknown account ID"); |
| 94 | } |
| 95 | } |
| 96 |
96.0%
src/echidna/base/EchidnaSetup.sol
Lines covered: 27 / 28 (96.0%)
| 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 | contract EchidnaSetup is EchidnaConfig { |
| 11 | IHevm hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); |
| 12 | |
| 13 | ERC20PresetFixedSupply token; |
| 14 | DripsEchidna drips; |
| 15 | AddressDriverEchidna driver; |
| 16 | |
| 17 | constructor() EchidnaConfig() { |
| 18 | // Deploy ERC20 token |
| 19 | token = new ERC20PresetFixedSupply( |
| 20 | "Test Token", |
| 21 | "TEST", |
| 22 | STARTING_BALANCE * 4, |
| 23 | address(this) |
| 24 | ); |
| 25 | |
| 26 | // Deploy Drips |
| 27 | drips = new DripsEchidna(SECONDS_PER_CYCLE); |
| 28 | drips.unpause_noModifiers(); |
| 29 | |
| 30 | // Deploy AddressDriver |
| 31 | uint32 driverId = drips.registerDriver(address(this)); |
| 32 | driver = new AddressDriverEchidna( |
| 33 | drips, |
| 34 | address(0), |
| 35 | driverId |
| 36 | ); |
| 37 | driver.unpause_noModifiers(); |
| 38 | drips.updateDriverAddress(driverId, address(driver)); |
| 39 | |
| 40 | // Set up token balances |
| 41 | token.transfer(ADDRESS_USER0, STARTING_BALANCE); |
| 42 | hevm.prank(ADDRESS_USER0); |
| 43 | token.approve(address(driver), type(uint256).max); |
| 44 | token.transfer(ADDRESS_USER1, STARTING_BALANCE); |
| 45 | hevm.prank(ADDRESS_USER1); |
| 46 | token.approve(address(driver), type(uint256).max); |
| 47 | token.transfer(ADDRESS_USER2, STARTING_BALANCE); |
| 48 | hevm.prank(ADDRESS_USER2); |
| 49 | token.approve(address(driver), type(uint256).max); |
| 50 | token.transfer(ADDRESS_USER3, STARTING_BALANCE); |
| 51 | hevm.prank(ADDRESS_USER3); |
| 52 | token.approve(address(driver), type(uint256).max); |
| 53 | |
| 54 | // Store starting timestamp |
| 55 | STARTING_TIMESTAMP = block.timestamp; |
| 56 | } |
| 57 | } |
| 58 |
98.0%
src/echidna/base/EchidnaStorage.sol
Lines covered: 85 / 86 (98.0%)
| 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 | 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 | ) internal { |
| 34 | // Hash the new stream receivers |
| 35 | bytes32 receiversHash = drips.hashStreams(receivers); |
| 36 | |
| 37 | // Update the stream receivers for 'sender' |
| 38 | delete userToStreamReceivers[sender]; |
| 39 | for (uint256 i = 0; i < receivers.length; i++) { |
| 40 | userToStreamReceivers[sender].push(receivers[i]); |
| 41 | } |
| 42 | |
| 43 | // Add a new entry to the streams history |
| 44 | uint256 nextIndex = userToStreamsHistory[sender].length; |
| 45 | userToStreamsHistory[sender].push(); |
| 46 | userToStreamsHistory[sender][nextIndex].streamsHash = bytes32(0); |
| 47 | for (uint256 i = 0; i < receivers.length; i++) { |
| 48 | userToStreamsHistory[sender][nextIndex].receivers.push( |
| 49 | receivers[i] |
| 50 | ); |
| 51 | } |
| 52 | (, , uint32 updateTime, , uint32 maxEnd) = drips.streamsState( |
| 53 | getDripsAccountId(sender), |
| 54 | token |
| 55 | ); |
| 56 | userToStreamsHistory[sender][nextIndex].updateTime = updateTime; |
| 57 | userToStreamsHistory[sender][nextIndex].maxEnd = maxEnd; |
| 58 | |
| 59 | // Generate starting hash. If there is no previous history, use 0 |
| 60 | bytes32 startingHash; |
| 61 | if (nextIndex == 0) { |
| 62 | startingHash = bytes32(0); |
| 63 | } else { |
| 64 | startingHash = userToStreamsHistoryHashes[sender][nextIndex - 1]; |
| 65 | } |
| 66 | |
| 67 | // Add new entry to the streams history hashes |
| 68 | bytes32 historyHash = drips.hashStreamsHistory( |
| 69 | startingHash, |
| 70 | receiversHash, |
| 71 | updateTime, |
| 72 | maxEnd |
| 73 | ); |
| 74 | 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 | function updateSplitsReceivers( |
| 83 | address sender, |
| 84 | SplitsReceiver[] memory receivers |
| 85 | ) internal { |
| 86 | delete userToSplitsReceivers[sender]; |
| 87 | for (uint256 i = 0; i < receivers.length; i++) { |
| 88 | 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 | function getStreamReceivers(address sender) |
| 98 | internal |
| 99 | returns (StreamReceiver[] memory) |
| 100 | { |
| 101 | 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 | function getStreamsHistory(address sender) |
| 110 | internal |
| 111 | returns (StreamsHistory[] memory) |
| 112 | { |
| 113 | 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 | function getStreamsHistoryHashes(address sender) |
| 122 | internal |
| 123 | returns (bytes32[] memory) |
| 124 | { |
| 125 | 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 | function getSplitsReceivers(address sender) |
| 134 | internal |
| 135 | returns (SplitsReceiver[] memory) |
| 136 | { |
| 137 | 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 | function bubbleSortStreamReceivers(StreamReceiver[] memory unsorted) |
| 149 | internal |
| 150 | returns (StreamReceiver[] memory) |
| 151 | { |
| 152 | uint256 n = unsorted.length; |
| 153 | if (n <= 1) return unsorted; |
| 154 | |
| 155 | StreamReceiver[] memory sorted = unsorted; |
| 156 | for (uint256 i = 0; i < n - 1; i++) { |
| 157 | for (uint256 j = 0; j < n - i - 1; j++) { |
| 158 | if (bubbleSortStreamReceiverGT(sorted[j], sorted[j + 1])) { |
| 159 | StreamReceiver memory temp = sorted[j]; |
| 160 | sorted[j] = sorted[j + 1]; |
| 161 | sorted[j + 1] = temp; |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | return sorted; |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * @notice Sort the splits receivers. |
| 170 | * @param unsorted Unsorted splits receivers |
| 171 | * @return Sorted splits receivers |
| 172 | */ |
| 173 | function bubbleSortSplitsReceivers(SplitsReceiver[] memory unsorted) |
| 174 | internal |
| 175 | returns (SplitsReceiver[] memory) |
| 176 | { |
| 177 | uint256 n = unsorted.length; |
| 178 | if (n <= 1) return unsorted; |
| 179 | |
| 180 | SplitsReceiver[] memory sorted = unsorted; |
| 181 | for (uint256 i = 0; i < n - 1; i++) { |
| 182 | for (uint256 j = 0; j < n - i - 1; j++) { |
| 183 | if (bubbleSortSplitsReceiverGT(sorted[j], sorted[j + 1])) { |
| 184 | SplitsReceiver memory temp = sorted[j]; |
| 185 | sorted[j] = sorted[j + 1]; |
| 186 | sorted[j + 1] = temp; |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | 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 | function bubbleSortStreamReceiverGT( |
| 202 | StreamReceiver memory a, |
| 203 | StreamReceiver memory b |
| 204 | ) internal returns (bool) { |
| 205 | if (a.accountId != b.accountId) { |
| 206 | return a.accountId > b.accountId; |
| 207 | } |
| 208 | if (StreamConfig.unwrap(a.config) != StreamConfig.unwrap(b.config)) { |
| 209 | return |
| 210 | StreamConfig.unwrap(a.config) > StreamConfig.unwrap(b.config); |
| 211 | } |
| 212 | 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 | function bubbleSortSplitsReceiverGT( |
| 224 | SplitsReceiver memory a, |
| 225 | SplitsReceiver memory b |
| 226 | ) internal returns (bool) { |
| 227 | if (a.accountId != b.accountId) { |
| 228 | return a.accountId > b.accountId; |
| 229 | } |
| 230 | 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 | function getDripsAccountId(address account) internal returns (uint256) { |
| 240 | if (ADDRESS_TO_DRIPS_ACCOUNT_ID[account] == 0) { |
| 241 | ADDRESS_TO_DRIPS_ACCOUNT_ID[account] = driver.calcAccountId( |
| 242 | account |
| 243 | ); |
| 244 | } |
| 245 | return ADDRESS_TO_DRIPS_ACCOUNT_ID[account]; |
| 246 | } |
| 247 | } |
| 248 |
100.0%
src/echidna/tools/AddressDriverEchidna.sol
Lines covered: 3 / 3 (100.0%)
| 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 | contract AddressDriverEchidna is AddressDriver, ManagedEchidna { |
| 12 | constructor( |
| 13 | DripsEchidna drips_, |
| 14 | address forwarder, |
| 15 | uint32 driverId_ |
| 16 | ) AddressDriver(drips_, forwarder, driverId_) {} |
| 17 | } |
| 18 |
0.0%
src/echidna/tools/Debugger.sol
Lines covered: 0 / 1 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | |
| 3 | 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 | } |
| 94 |
100.0%
src/echidna/tools/DripsEchidna.sol
Lines covered: 10 / 10 (100.0%)
| 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 | contract DripsEchidna is Drips, ManagedEchidna { |
| 12 | 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 | 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 | bytes32 slot = _erc1967Slot("eip1967.streams.storage"); |
| 30 | assembly { |
| 31 | streamsStorage.slot := slot |
| 32 | } |
| 33 | |
| 34 | // Return the amtDelta for the given cycle |
| 35 | StreamsState storage state = streamsStorage.states[erc20][accountId]; |
| 36 | mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
| 37 | return (amtDeltas[cycle].thisCycle, amtDeltas[cycle].nextCycle); |
| 38 | } |
| 39 | } |
| 40 |
0.0%
src/echidna/tools/IHevm.sol
Lines covered: 0 / 0 (0.0%)
| 1 | // SPDX-License-Identifier: MIT |
| 2 | |
| 3 | // https://github.com/ethereum/hevm/blob/main/doc/src/controlling-the-unit-testing-environment.md#cheat-codes |
| 4 | |
| 5 | interface IHevm { |
| 6 | function warp(uint256 x) external; |
| 7 | |
| 8 | function roll(uint256 x) external; |
| 9 | |
| 10 | function store( |
| 11 | address c, |
| 12 | bytes32 loc, |
| 13 | bytes32 val |
| 14 | ) external; |
| 15 | |
| 16 | function load(address c, bytes32 loc) external returns (bytes32 val); |
| 17 | |
| 18 | function sign(uint256 sk, bytes32 digest) |
| 19 | external |
| 20 | returns ( |
| 21 | uint8 v, |
| 22 | bytes32 r, |
| 23 | bytes32 s |
| 24 | ); |
| 25 | |
| 26 | function addr(uint256 sk) external returns (address addr); |
| 27 | |
| 28 | function ffi(string[] calldata) external returns (bytes memory); |
| 29 | |
| 30 | function prank(address sender) external; |
| 31 | } |
| 32 |
60.0%
src/echidna/tools/ManagedEchidna.sol
Lines covered: 3 / 5 (60.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 | contract ManagedEchidna is Managed { |
| 9 | constructor() Managed() {} |
| 10 | |
| 11 | /** |
| 12 | * @notice Helper function to unpause the contract as any user |
| 13 | */ |
| 14 | function unpause_noModifiers() public { |
| 15 | _managedStorage().isPaused = false; |
| 16 | emit Unpaused(msg.sender); |
| 17 | } |
| 18 | } |
| 19 |
100.0%
test/recon/CryticTester.sol
Lines covered: 1 / 1 (100.0%)
| 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 | contract CryticTester is Properties {} |
| 8 |
88.0%
test/recon/Properties.sol
Lines covered: 8 / 9 (88.0%)
| 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 | emit AssertionFailed(reason); |
| 19 | assert(false); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | function invariant_canary() public returns (bool) { |
| 24 | t(false, INVARIANT_CANARY_GLOBAL_INVARIANT_FAILURE); |
| 25 | return true; |
| 26 | } |
| 27 | |
| 28 | function assert_canary_ASSERTION_CANARY(uint256 entropy) public { |
| 29 | t(entropy > 0, ASSERTION_CANARY); |
| 30 | } |
| 31 | } |
| 32 |