Files
47
Total Lines
7965
Coverage
87.7%
1552 / 1769 lines
Actions
0.0%
lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol
Lines covered: 0 / 0 (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 |
85.7%
lib/openzeppelin-contracts/contracts/metatx/ERC2771Context.sol
Lines covered: 6 / 7 (85.7%)
| 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 |
✓ 1
|
_trustedForwarder = trustedForwarder; |
|
| 18 |
} |
||
| 19 | |||
| 20 |
✓ 33.6M
|
function isTrustedForwarder(address forwarder) public view virtual returns (bool) {
|
|
| 21 |
✓ 33.6M
|
return forwarder == _trustedForwarder; |
|
| 22 |
} |
||
| 23 | |||
| 24 |
✓ 33.6M
|
function _msgSender() internal view virtual override returns (address sender) {
|
|
| 25 |
✓ 33.6M
|
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 |
✓ 33.6M
|
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 / 3 (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 / 24 (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 / 12 (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 |
10.0%
lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol
Lines covered: 1 / 10 (10.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 |
✓ 1
|
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 |
60.0%
lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol
Lines covered: 42 / 70 (60.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 |
✓ 1
|
_name = name_; |
|
| 56 |
✓ 1
|
_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 |
✓ 18.0M
|
function balanceOf(address account) public view virtual override returns (uint256) {
|
|
| 102 |
✓ 18.0M
|
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 |
✓ 2.9M
|
function transfer(address to, uint256 amount) public virtual override returns (bool) {
|
|
| 114 |
✓ 2.9M
|
address owner = _msgSender(); |
|
| 115 |
✓ 2.9M
|
_transfer(owner, to, amount); |
|
| 116 |
✓ 2.9M
|
return true; |
|
| 117 |
} |
||
| 118 | |||
| 119 |
/** |
||
| 120 |
* @dev See {IERC20-allowance}.
|
||
| 121 |
*/ |
||
| 122 |
✓ 4.7M
|
function allowance(address owner, address spender) public view virtual override returns (uint256) {
|
|
| 123 |
✓ 4.7M
|
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 |
✓ 4
|
function approve(address spender, uint256 amount) public virtual override returns (bool) {
|
|
| 137 |
✓ 4
|
address owner = _msgSender(); |
|
| 138 |
✓ 4
|
_approve(owner, spender, amount); |
|
| 139 |
✓ 4
|
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 |
✓ 4.7M
|
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
|
|
| 159 |
✓ 4.7M
|
address spender = _msgSender(); |
|
| 160 |
✓ 4.7M
|
_spendAllowance(from, spender, amount); |
|
| 161 |
✓ 4.7M
|
_transfer(from, to, amount); |
|
| 162 |
✓ 3.5M
|
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 |
✓ 7.6M
|
require(from != address(0), "ERC20: transfer from the zero address"); |
|
| 224 |
✓ 7.6M
|
require(to != address(0), "ERC20: transfer to the zero address"); |
|
| 225 | |||
| 226 |
✓ 7.6M
|
_beforeTokenTransfer(from, to, amount); |
|
| 227 | |||
| 228 |
✓ 7.6M
|
uint256 fromBalance = _balances[from]; |
|
| 229 |
✓ 7.6M
|
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); |
|
| 230 |
unchecked {
|
||
| 231 |
✓ 6.4M
|
_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 |
✓ 6.4M
|
_balances[to] += amount; |
|
| 235 |
} |
||
| 236 | |||
| 237 |
✓ 6.4M
|
emit Transfer(from, to, amount); |
|
| 238 | |||
| 239 |
✓ 6.4M
|
_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 |
✓ 1
|
require(account != address(0), "ERC20: mint to the zero address"); |
|
| 253 | |||
| 254 |
✓ 1
|
_beforeTokenTransfer(address(0), account, amount); |
|
| 255 | |||
| 256 |
✓ 1
|
_totalSupply += amount; |
|
| 257 |
unchecked {
|
||
| 258 |
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. |
||
| 259 |
✓ 1
|
_balances[account] += amount; |
|
| 260 |
} |
||
| 261 |
✓ 1
|
emit Transfer(address(0), account, amount); |
|
| 262 | |||
| 263 |
✓ 1
|
_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 |
✓ 4
|
require(owner != address(0), "ERC20: approve from the zero address"); |
|
| 310 |
✓ 4
|
require(spender != address(0), "ERC20: approve to the zero address"); |
|
| 311 | |||
| 312 |
✓ 4
|
_allowances[owner][spender] = amount; |
|
| 313 |
✓ 4
|
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 |
✓ 4.7M
|
uint256 currentAllowance = allowance(owner, spender); |
|
| 326 |
✓ 4.7M
|
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 |
✓ 7.6M
|
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 |
✓ 6.4M
|
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 / 3 (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 |
✓ 25.6M
|
contract ERC20PresetFixedSupply is ERC20Burnable {
|
|
| 22 |
/** |
||
| 23 |
* @dev Mints `initialSupply` amount of token and transfers them to `owner`. |
||
| 24 |
* |
||
| 25 |
* See {ERC20-constructor}.
|
||
| 26 |
*/ |
||
| 27 |
✓ 1
|
constructor(string memory name, string memory symbol, uint256 initialSupply, address owner) ERC20(name, symbol) {
|
|
| 28 |
✓ 1
|
_mint(owner, initialSupply); |
|
| 29 |
} |
||
| 30 |
} |
||
| 31 |
100.0%
lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
Lines covered: 5 / 5 (100.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 |
✓ 6
|
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 |
✓ 2.9M
|
_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 |
✓ 4.7M
|
_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 |
✓ 4.7M
|
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); |
|
| 123 |
✓ 3.5M
|
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.5%
lib/openzeppelin-contracts/contracts/utils/Address.sol
Lines covered: 15 / 24 (62.5%)
| 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 |
✓ 6
|
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 |
✓ 4.7M
|
) internal returns (bytes memory) {
|
|
| 104 |
✓ 4.7M
|
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 |
✓ 4.7M
|
) internal returns (bytes memory) {
|
|
| 134 |
✓ 4.7M
|
require(address(this).balance >= value, "Address: insufficient balance for call"); |
|
| 135 |
✓ 4.7M
|
(bool success, bytes memory returndata) = target.call{value: value}(data);
|
|
| 136 |
✓ 4.7M
|
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 |
✓ 4.7M
|
) internal view returns (bytes memory) {
|
|
| 201 |
✓ 4.7M
|
if (success) {
|
|
| 202 |
✓ 3.5M
|
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 |
✓ 3.5M
|
return returndata; |
|
| 208 |
} else {
|
||
| 209 |
✓ 1.2M
|
_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 |
✓ 1.2M
|
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 |
✓ 1.2M
|
let returndata_size := mload(returndata) |
|
| 238 |
✓ 1.2M
|
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 |
✓ 33.6M
|
function _msgSender() internal view virtual returns (address) {
|
|
| 18 |
✓ 33.6M
|
return msg.sender; |
|
| 19 |
} |
||
| 20 | |||
| 21 |
function _msgData() internal view virtual returns (bytes calldata) {
|
||
| 22 |
return msg.data; |
||
| 23 |
} |
||
| 24 |
} |
||
| 25 |
20.0%
lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol
Lines covered: 1 / 5 (20.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 |
✓ 6
|
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 |
2.9%
lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol
Lines covered: 1 / 35 (2.9%)
| 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 |
✓ 6
|
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 |
85.2%
src/AddressDriver.sol
Lines covered: 23 / 27 (85.2%)
| 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 |
✓ 1
|
constructor(Drips drips_, address forwarder, uint32 driverId_) DriverTransferUtils(forwarder) {
|
|
| 22 |
✓ 1
|
drips = drips_; |
|
| 23 |
✓ 1
|
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 |
✓ 31.1M
|
function calcAccountId(address addr) public view returns (uint256 accountId) {
|
|
| 32 |
// By assignment we get `accountId` value: |
||
| 33 |
// `zeros (224 bits) | driverId (32 bits)` |
||
| 34 |
✓ 31.1M
|
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 |
✓ 31.1M
|
accountId = (accountId << 224) | uint160(addr); |
|
| 40 |
} |
||
| 41 | |||
| 42 |
/// @notice Calculates the account ID for the message sender |
||
| 43 |
/// @return accountId The account ID |
||
| 44 |
✓ 28.9M
|
function _callerAccountId() internal view returns (uint256 accountId) {
|
|
| 45 |
✓ 28.9M
|
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 |
✓ 6.7M
|
function collect(IERC20 erc20, address transferTo) public whenNotPaused returns (uint128 amt) {
|
|
| 59 |
✓ 6.7M
|
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 |
✓ 1.3M
|
_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 |
✓ 14.1M
|
) public whenNotPaused returns (int128 realBalanceDelta) {
|
|
| 135 |
✓ 14.1M
|
return _setStreamsAndTransfer( |
|
| 136 |
✓ 14.1M
|
drips, |
|
| 137 |
✓ 14.1M
|
_callerAccountId(), |
|
| 138 |
✓ 14.1M
|
erc20, |
|
| 139 |
✓ 14.1M
|
currReceivers, |
|
| 140 |
✓ 14.1M
|
balanceDelta, |
|
| 141 |
✓ 14.1M
|
newReceivers, |
|
| 142 |
✓ 14.1M
|
maxEndHint1, |
|
| 143 |
✓ 14.1M
|
maxEndHint2, |
|
| 144 |
✓ 14.1M
|
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 |
✓ 6.8M
|
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: 98 / 114 (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 |
✓ 3.4M
|
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 |
✓ 13.3M
|
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 |
✓ 7.3M
|
uint160 public immutable minAmtPerSec; |
|
| 82 |
/// @notice The ERC-1967 storage slot holding a single `DripsStorage` structure. |
||
| 83 |
✓ 1
|
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 |
✓ 1
|
Streams(cycleSecs_, _erc1967Slot("eip1967.streams.storage"))
|
|
| 140 |
✓ 1
|
Splits(_erc1967Slot("eip1967.splits.storage"))
|
|
| 141 |
{
|
||
| 142 |
✓ 1
|
cycleSecs = Streams._cycleSecs; |
|
| 143 |
✓ 1
|
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 |
✓ 13.3M
|
uint32 driverId = uint32(accountId >> DRIVER_ID_OFFSET); |
|
| 156 |
✓ 13.3M
|
_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 |
✓ 27.7M
|
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 |
✓ 1
|
function registerDriver(address driverAddr) public whenNotPaused returns (uint32 driverId) {
|
|
| 178 |
✓ 1
|
require(driverAddr != address(0), "Driver registered for 0 address"); |
|
| 179 |
✓ 1
|
DripsStorage storage dripsStorage = _dripsStorage(); |
|
| 180 |
✓ 1
|
driverId = dripsStorage.nextDriverId++; |
|
| 181 |
✓ 1
|
dripsStorage.driverAddresses[driverId] = driverAddr; |
|
| 182 |
✓ 1
|
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 |
✓ 27.7M
|
function driverAddress(uint32 driverId) public view returns (address driverAddr) {
|
|
| 190 |
✓ 27.7M
|
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 |
✓ 1
|
_assertCallerIsDriver(driverId); |
|
| 200 |
✓ 1
|
_dripsStorage().driverAddresses[driverId] = newDriverAddr; |
|
| 201 |
✓ 1
|
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 |
✓ 7.0M
|
returns (uint128 streamsBalance, uint128 splitsBalance) |
|
| 229 |
{
|
||
| 230 |
✓ 7.0M
|
Balance storage balance = _dripsStorage().balances[erc20]; |
|
| 231 |
✓ 7.0M
|
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 |
✓ 2.7M
|
_verifyBalanceIncrease(erc20, amt); |
|
| 242 |
✓ 2.7M
|
_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 |
✓ 1.7M
|
_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 |
✓ 818.1K
|
_verifyBalanceIncrease(erc20, amt); |
|
| 262 |
✓ 818.1K
|
_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 |
✓ 1.2M
|
_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 |
✓ 772.9K
|
Balance storage balance = _dripsStorage().balances[erc20]; |
|
| 280 |
✓ 772.9K
|
balance.streams -= amt; |
|
| 281 |
✓ 772.9K
|
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 |
✓ 3.5M
|
(uint256 streamsBalance, uint128 splitsBalance) = balances(erc20); |
|
| 291 |
✓ 3.5M
|
uint256 newTotalBalance = streamsBalance + splitsBalance + amt; |
|
| 292 |
✓ 3.5M
|
require(newTotalBalance <= MAX_TOTAL_BALANCE, "Total balance too high"); |
|
| 293 |
✓ 3.5M
|
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 |
✓ 3.6M
|
(uint128 streamsBalance, uint128 splitsBalance) = balances(erc20); |
|
| 313 |
✓ 3.6M
|
uint256 withdrawable = _tokenBalance(erc20) - streamsBalance - splitsBalance; |
|
| 314 |
✓ 3.6M
|
require(amt <= withdrawable, "Withdrawal amount too high"); |
|
| 315 |
✓ 2.9M
|
emit Withdrawn(erc20, receiver, amt); |
|
| 316 |
✓ 2.9M
|
erc20.safeTransfer(receiver, amt); |
|
| 317 |
} |
||
| 318 | |||
| 319 |
✓ 7.0M
|
function _tokenBalance(IERC20 erc20) internal view returns (uint256) {
|
|
| 320 |
✓ 7.0M
|
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 |
✓ 507.4K
|
returns (uint32 cycles) |
|
| 338 |
{
|
||
| 339 |
✓ 507.4K
|
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 |
✓ 11.1M
|
returns (uint128 receivableAmt) |
|
| 358 |
{
|
||
| 359 |
✓ 11.1M
|
(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 |
✓ 6.5M
|
returns (uint128 receivedAmt) |
|
| 380 |
{
|
||
| 381 |
✓ 6.5M
|
receivedAmt = Streams._receiveStreams(accountId, erc20, maxCycles); |
|
| 382 |
✓ 6.5M
|
if (receivedAmt != 0) {
|
|
| 383 |
✓ 728.8K
|
_moveBalanceFromStreamsToSplits(erc20, receivedAmt); |
|
| 384 |
✓ 728.8K
|
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 |
✓ 22.6M
|
) public whenNotPaused returns (uint128 amt) {
|
|
| 415 |
✓ 22.6M
|
amt = Streams._squeezeStreams(accountId, erc20, senderId, historyHash, streamsHistory); |
|
| 416 |
✓ 22.6M
|
if (amt != 0) {
|
|
| 417 |
✓ 44.1K
|
_moveBalanceFromStreamsToSplits(erc20, amt); |
|
| 418 |
✓ 44.1K
|
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 |
✓ 13.2M
|
) public view returns (uint128 amt) {
|
|
| 442 |
(amt,,,,) = |
||
| 443 |
✓ 13.2M
|
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 |
✓ 6.3M
|
function splittable(uint256 accountId, IERC20 erc20) public view returns (uint128 amt) {
|
|
| 456 |
✓ 6.3M
|
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 |
✓ 488.4K
|
returns (uint128 collectableAmt, uint128 splitAmt) |
|
| 474 |
{
|
||
| 475 |
✓ 488.4K
|
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 |
✓ 8.2M
|
returns (uint128 collectableAmt, uint128 splitAmt) |
|
| 505 |
{
|
||
| 506 |
✓ 8.2M
|
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 |
✓ 4.3M
|
function collectable(uint256 accountId, IERC20 erc20) public view returns (uint128 amt) {
|
|
| 519 |
✓ 4.3M
|
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 |
✓ 6.7M
|
onlyDriver(accountId) |
|
| 537 |
✓ 6.7M
|
returns (uint128 amt) |
|
| 538 |
{
|
||
| 539 |
✓ 6.7M
|
amt = Splits._collect(accountId, erc20); |
|
| 540 |
✓ 6.7M
|
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 |
✓ 848.6K
|
onlyDriver(accountId) |
|
| 561 |
{
|
||
| 562 |
✓ 848.6K
|
if (amt != 0) _increaseSplitsBalance(erc20, amt); |
|
| 563 |
✓ 848.6K
|
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 |
✓ 14.6M
|
bytes32 streamsHash, |
|
| 584 |
✓ 14.6M
|
bytes32 streamsHistoryHash, |
|
| 585 |
✓ 14.6M
|
uint32 updateTime, |
|
| 586 |
✓ 14.6M
|
uint128 balance, |
|
| 587 |
✓ 14.6M
|
uint32 maxEnd |
|
| 588 |
) |
||
| 589 |
{
|
||
| 590 |
✓ 14.6M
|
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 |
✓ 4.5M
|
) public view returns (uint128 balance) {
|
|
| 614 |
✓ 4.5M
|
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 |
✓ 13.3M
|
) public whenNotPaused onlyDriver(accountId) returns (int128 realBalanceDelta) {
|
|
| 679 |
✓ 13.3M
|
if (balanceDelta > 0) _increaseStreamsBalance(erc20, uint128(balanceDelta)); |
|
| 680 |
✓ 13.3M
|
realBalanceDelta = Streams._setStreams( |
|
| 681 |
✓ 13.3M
|
accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 |
|
| 682 |
); |
||
| 683 |
✓ 13.0M
|
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 |
✓ 18.8M
|
returns (bytes32 streamsHash) |
|
| 697 |
{
|
||
| 698 |
✓ 18.8M
|
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 |
✓ 13.0M
|
) public pure returns (bytes32 streamsHistoryHash) {
|
|
| 716 |
✓ 13.0M
|
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 |
✓ 6.8M
|
onlyDriver(accountId) |
|
| 743 |
{
|
||
| 744 |
✓ 6.8M
|
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 |
✓ 41.9M
|
function _dripsStorage() internal view returns (DripsStorage storage storageRef) {
|
|
| 787 |
✓ 41.9M
|
bytes32 slot = _dripsStorageSlot; |
|
| 788 |
// slither-disable-next-line assembly |
||
| 789 |
assembly {
|
||
| 790 |
✓ 41.9M
|
storageRef.slot := slot |
|
| 791 |
} |
||
| 792 |
} |
||
| 793 |
} |
||
| 794 |
100.0%
src/DriverTransferUtils.sol
Lines covered: 12 / 12 (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 |
✓ 1
|
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 |
✓ 6.7M
|
returns (uint128 amt) |
|
| 30 |
{
|
||
| 31 |
✓ 6.7M
|
amt = drips.collect(accountId, erc20); |
|
| 32 |
✓ 6.7M
|
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 |
✓ 1.3M
|
if (amt > 0) _transferFromCaller(drips, erc20, amt); |
|
| 55 |
✓ 848.6K
|
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 |
✓ 14.1M
|
) internal returns (int128 realBalanceDelta) {
|
|
| 119 |
✓ 14.1M
|
if (balanceDelta > 0) _transferFromCaller(drips, erc20, uint128(balanceDelta)); |
|
| 120 |
✓ 13.3M
|
realBalanceDelta = drips.setStreams( |
|
| 121 |
✓ 13.3M
|
accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 |
|
| 122 |
); |
||
| 123 |
✓ 13.0M
|
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 |
✓ 4.7M
|
SafeERC20.safeTransferFrom(erc20, _msgSender(), address(drips), amt); |
|
| 132 |
} |
||
| 133 |
} |
||
| 134 |
25.0%
src/Managed.sol
Lines covered: 11 / 44 (25.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 |
✓ 1
|
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 |
✓ 22.6M
|
require(!isPaused(), "Contract paused"); |
|
| 67 |
✓ 13.0M
|
_; |
|
| 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 |
✓ 1
|
_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 |
✓ 65.0M
|
function isPaused() public view returns (bool) {
|
|
| 160 |
✓ 65.0M
|
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 |
✓ 76.8M
|
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 |
✓ 76.8M
|
return bytes32(uint256(keccak256(bytes(name))) - 1024); |
|
| 184 |
} |
||
| 185 | |||
| 186 |
/// @notice Returns the Managed storage. |
||
| 187 |
/// @return storageRef The storage. |
||
| 188 |
✓ 65.0M
|
function _managedStorage() internal view returns (ManagedStorage storage storageRef) {
|
|
| 189 |
✓ 65.0M
|
bytes32 slot = _managedStorageSlot; |
|
| 190 |
// slither-disable-next-line assembly |
||
| 191 |
assembly {
|
||
| 192 |
✓ 65.0M
|
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: 72 / 72 (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 |
✓ 3.5M
|
uint256 internal constant _MAX_SPLITS_RECEIVERS = 200; |
|
| 24 |
/// @notice The total splits weight of an account. |
||
| 25 |
✓ 3.4M
|
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 |
✓ 1
|
_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 |
✓ 1.9M
|
_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 |
✓ 6.3M
|
function _splittable(uint256 accountId, IERC20 erc20) internal view returns (uint128 amt) {
|
|
| 114 |
✓ 6.3M
|
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 |
✓ 488.4K
|
returns (uint128 collectableAmt, uint128 splitAmt) |
|
| 130 |
{
|
||
| 131 |
✓ 488.4K
|
_assertCurrSplits(accountId, currReceivers); |
|
| 132 |
✓ 488.4K
|
if (amount == 0) {
|
|
| 133 |
✓ 442.7K
|
return (0, 0); |
|
| 134 |
} |
||
| 135 |
unchecked {
|
||
| 136 |
✓ 45.7K
|
uint256 splitsWeight = 0; |
|
| 137 |
✓ 68.6K
|
for (uint256 i = currReceivers.length; i != 0;) {
|
|
| 138 |
✓ 22.9K
|
splitsWeight += currReceivers[--i].weight; |
|
| 139 |
} |
||
| 140 |
✓ 45.7K
|
splitAmt = uint128(amount * splitsWeight / _TOTAL_SPLITS_WEIGHT); |
|
| 141 |
✓ 45.7K
|
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 |
✓ 8.2M
|
returns (uint128 collectableAmt, uint128 splitAmt) |
|
| 159 |
{
|
||
| 160 |
✓ 8.2M
|
_assertCurrSplits(accountId, currReceivers); |
|
| 161 |
✓ 8.2M
|
SplitsBalance storage balance = _splitsStorage().splitsStates[accountId].balances[erc20]; |
|
| 162 | |||
| 163 |
✓ 8.2M
|
collectableAmt = balance.splittable; |
|
| 164 |
✓ 8.2M
|
if (collectableAmt == 0) {
|
|
| 165 |
✓ 6.8M
|
return (0, 0); |
|
| 166 |
} |
||
| 167 |
✓ 1.4M
|
balance.splittable = 0; |
|
| 168 | |||
| 169 |
unchecked {
|
||
| 170 |
✓ 1.4M
|
uint256 splitsWeight = 0; |
|
| 171 |
✓ 1.6M
|
for (uint256 i = 0; i < currReceivers.length; i++) {
|
|
| 172 |
✓ 251.0K
|
splitsWeight += currReceivers[i].weight; |
|
| 173 |
✓ 251.0K
|
uint128 currSplitAmt = splitAmt; |
|
| 174 |
✓ 251.0K
|
splitAmt = uint128(collectableAmt * splitsWeight / _TOTAL_SPLITS_WEIGHT); |
|
| 175 |
✓ 251.0K
|
currSplitAmt = splitAmt - currSplitAmt; |
|
| 176 |
✓ 251.0K
|
uint256 receiver = currReceivers[i].accountId; |
|
| 177 |
✓ 251.0K
|
_addSplittable(receiver, erc20, currSplitAmt); |
|
| 178 |
✓ 251.0K
|
emit Split(accountId, receiver, erc20, currSplitAmt); |
|
| 179 |
} |
||
| 180 |
✓ 1.4M
|
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 |
✓ 1.4M
|
balance.collectable += collectableAmt; |
|
| 184 |
} |
||
| 185 |
✓ 1.4M
|
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 |
✓ 4.3M
|
function _collectable(uint256 accountId, IERC20 erc20) internal view returns (uint128 amt) {
|
|
| 193 |
✓ 4.3M
|
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 |
✓ 6.7M
|
function _collect(uint256 accountId, IERC20 erc20) internal returns (uint128 amt) {
|
|
| 201 |
✓ 6.7M
|
SplitsBalance storage balance = _splitsStorage().splitsStates[accountId].balances[erc20]; |
|
| 202 |
✓ 6.7M
|
amt = balance.collectable; |
|
| 203 |
✓ 6.7M
|
balance.collectable = 0; |
|
| 204 |
✓ 6.7M
|
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 |
✓ 848.6K
|
_addSplittable(receiver, erc20, amt); |
|
| 215 |
✓ 848.6K
|
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 |
✓ 6.8M
|
SplitsState storage state = _splitsStorage().splitsStates[accountId]; |
|
| 238 |
✓ 6.8M
|
bytes32 newSplitsHash = _hashSplits(receivers); |
|
| 239 |
✓ 6.8M
|
if (newSplitsHash == state.splitsHash) return; |
|
| 240 |
✓ 3.5M
|
emit SplitsSet(accountId, newSplitsHash); |
|
| 241 |
✓ 3.5M
|
_assertSplitsValid(receivers, newSplitsHash); |
|
| 242 |
✓ 2.7M
|
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 |
✓ 3.5M
|
require(receivers.length <= _MAX_SPLITS_RECEIVERS, "Too many splits receivers"); |
|
| 252 |
✓ 3.5M
|
uint256 totalWeight = 0; |
|
| 253 |
✓ 3.5M
|
uint256 prevAccountId = 0; |
|
| 254 |
✓ 6.2M
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 255 |
✓ 2.8M
|
SplitsReceiver memory receiver = receivers[i]; |
|
| 256 |
✓ 2.8M
|
uint32 weight = receiver.weight; |
|
| 257 |
✓ 2.8M
|
require(weight != 0, "Splits receiver weight is zero"); |
|
| 258 |
✓ 2.8M
|
totalWeight += weight; |
|
| 259 |
✓ 2.8M
|
uint256 accountId = receiver.accountId; |
|
| 260 |
✓ 2.8M
|
if (accountId <= prevAccountId) require(i == 0, "Splits receivers not sorted"); |
|
| 261 |
✓ 2.8M
|
prevAccountId = accountId; |
|
| 262 |
✓ 2.8M
|
emit SplitsReceiverSeen(receiversHash, accountId, weight); |
|
| 263 |
} |
||
| 264 |
✓ 3.4M
|
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 |
✓ 8.6M
|
_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 |
✓ 8.6M
|
function _splitsHash(uint256 accountId) internal view returns (bytes32 currSplitsHash) {
|
|
| 285 |
✓ 8.6M
|
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 |
✓ 15.5M
|
returns (bytes32 receiversHash) |
|
| 296 |
{
|
||
| 297 |
✓ 15.5M
|
if (receivers.length == 0) {
|
|
| 298 |
✓ 11.7M
|
return bytes32(0); |
|
| 299 |
} |
||
| 300 |
✓ 3.8M
|
return keccak256(abi.encode(receivers)); |
|
| 301 |
} |
||
| 302 | |||
| 303 |
/// @notice Returns the Splits storage. |
||
| 304 |
/// @return splitsStorage The storage. |
||
| 305 |
✓ 42.8M
|
function _splitsStorage() private view returns (SplitsStorage storage splitsStorage) {
|
|
| 306 |
✓ 42.8M
|
bytes32 slot = _splitsStorageSlot; |
|
| 307 |
// slither-disable-next-line assembly |
||
| 308 |
assembly {
|
||
| 309 |
✓ 42.8M
|
splitsStorage.slot := slot |
|
| 310 |
} |
||
| 311 |
} |
||
| 312 |
} |
||
| 313 |
99.7%
src/Streams.sol
Lines covered: 367 / 368 (99.7%)
| 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 |
✓ 6
|
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 |
✓ 4.8M
|
returns (StreamConfig) |
|
| 66 |
{
|
||
| 67 |
// By assignment we get `config` value: |
||
| 68 |
// `zeros (224 bits) | streamId (32 bits)` |
||
| 69 |
✓ 4.8M
|
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 |
✓ 4.8M
|
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 |
✓ 4.8M
|
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 |
✓ 4.8M
|
config = (config << 32) | duration_; |
|
| 85 |
✓ 4.8M
|
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 |
✓ 73.6M
|
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 |
✓ 73.6M
|
return uint160(StreamConfig.unwrap(config) >> 64); |
|
| 108 |
} |
||
| 109 | |||
| 110 |
/// @notice Extracts start from a `StreamConfig` |
||
| 111 |
✓ 62.1M
|
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 |
✓ 62.1M
|
return uint32(StreamConfig.unwrap(config) >> 32); |
|
| 119 |
} |
||
| 120 | |||
| 121 |
/// @notice Extracts duration from a `StreamConfig` |
||
| 122 |
✓ 62.1M
|
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 |
✓ 62.1M
|
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 |
✓ 3.3M
|
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 |
✓ 3.3M
|
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 |
✓ 13.3M
|
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 |
✓ 49.5M
|
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 |
✓ 3.5M
|
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 |
✓ 1
|
require(cycleSecs > 1, "Cycle length too low"); |
|
| 277 |
✓ 1
|
_cycleSecs = cycleSecs; |
|
| 278 |
✓ 1
|
_minAmtPerSec = (_AMT_PER_SEC_MULTIPLIER + cycleSecs - 1) / cycleSecs; |
|
| 279 |
✓ 1
|
_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 |
✓ 6.5M
|
returns (uint128 receivedAmt) |
|
| 293 |
{
|
||
| 294 |
✓ 6.5M
|
uint32 receivableCycles; |
|
| 295 |
✓ 6.5M
|
uint32 fromCycle; |
|
| 296 |
✓ 6.5M
|
uint32 toCycle; |
|
| 297 |
✓ 6.5M
|
int128 finalAmtPerCycle; |
|
| 298 |
(receivedAmt, receivableCycles, fromCycle, toCycle, finalAmtPerCycle) = |
||
| 299 |
✓ 6.5M
|
_receiveStreamsResult(accountId, erc20, maxCycles); |
|
| 300 |
✓ 6.5M
|
if (fromCycle != toCycle) {
|
|
| 301 |
✓ 4.4M
|
StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
|
| 302 |
✓ 4.4M
|
state.nextReceivableCycle = toCycle; |
|
| 303 |
✓ 4.4M
|
mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
|
| 304 |
unchecked {
|
||
| 305 |
✓ 300.7M
|
for (uint32 cycle = fromCycle; cycle < toCycle; cycle++) {
|
|
| 306 |
✓ 296.3M
|
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 |
✓ 4.4M
|
if (finalAmtPerCycle != 0) {
|
|
| 311 |
✓ 89.5K
|
amtDeltas[toCycle].thisCycle += finalAmtPerCycle; |
|
| 312 |
} |
||
| 313 |
} |
||
| 314 |
} |
||
| 315 |
✓ 6.5M
|
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 |
✓ 17.7M
|
uint128 receivedAmt, |
|
| 334 |
✓ 17.7M
|
uint32 receivableCycles, |
|
| 335 |
✓ 17.7M
|
uint32 fromCycle, |
|
| 336 |
✓ 17.7M
|
uint32 toCycle, |
|
| 337 |
✓ 17.7M
|
int128 amtPerCycle |
|
| 338 |
) |
||
| 339 |
{
|
||
| 340 |
unchecked {
|
||
| 341 |
✓ 17.7M
|
(fromCycle, toCycle) = _receivableStreamsCyclesRange(accountId, erc20); |
|
| 342 |
✓ 17.7M
|
if (toCycle - fromCycle > maxCycles) {
|
|
| 343 |
✓ 204.8K
|
receivableCycles = toCycle - fromCycle - maxCycles; |
|
| 344 |
✓ 204.8K
|
toCycle -= receivableCycles; |
|
| 345 |
} |
||
| 346 |
✓ 17.7M
|
mapping(uint32 cycle => AmtDelta) storage amtDeltas = |
|
| 347 |
✓ 17.7M
|
_streamsStorage().states[erc20][accountId].amtDeltas; |
|
| 348 |
✓ 857.5M
|
for (uint32 cycle = fromCycle; cycle < toCycle; cycle++) {
|
|
| 349 |
✓ 839.9M
|
AmtDelta memory amtDelta = amtDeltas[cycle]; |
|
| 350 |
✓ 839.9M
|
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 |
✓ 839.9M
|
receivedAmt += uint128(amtPerCycle); |
|
| 354 |
✓ 839.9M
|
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 |
✓ 507.4K
|
returns (uint32 cycles) |
|
| 369 |
{
|
||
| 370 |
unchecked {
|
||
| 371 |
✓ 507.4K
|
(uint32 fromCycle, uint32 toCycle) = _receivableStreamsCyclesRange(accountId, erc20); |
|
| 372 |
✓ 507.4K
|
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 |
✓ 18.2M
|
returns (uint32 fromCycle, uint32 toCycle) |
|
| 385 |
{
|
||
| 386 |
✓ 18.2M
|
fromCycle = _streamsStorage().states[erc20][accountId].nextReceivableCycle; |
|
| 387 |
✓ 18.2M
|
toCycle = _cycleOf(_currTimestamp()); |
|
| 388 |
// slither-disable-next-line timestamp |
||
| 389 |
✓ 18.2M
|
if (fromCycle == 0 || toCycle < fromCycle) {
|
|
| 390 |
✓ 5.3M
|
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 |
✓ 22.6M
|
) internal returns (uint128 amt) {
|
|
| 417 |
unchecked {
|
||
| 418 |
✓ 22.6M
|
uint256 squeezedNum; |
|
| 419 |
✓ 22.6M
|
uint256[] memory squeezedRevIdxs; |
|
| 420 |
✓ 22.6M
|
bytes32[] memory historyHashes; |
|
| 421 |
✓ 22.6M
|
uint256 currCycleConfigs; |
|
| 422 |
(amt, squeezedNum, squeezedRevIdxs, historyHashes, currCycleConfigs) = |
||
| 423 |
✓ 22.6M
|
_squeezeStreamsResult(accountId, erc20, senderId, historyHash, streamsHistory); |
|
| 424 |
✓ 22.6M
|
bytes32[] memory squeezedHistoryHashes = new bytes32[](squeezedNum); |
|
| 425 |
✓ 22.6M
|
StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
|
| 426 |
✓ 22.6M
|
uint32[2 ** 32] storage nextSqueezed = state.nextSqueezed[senderId]; |
|
| 427 |
✓ 33.0M
|
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 |
✓ 10.4M
|
uint256 revIdx = squeezedRevIdxs[squeezedNum - i - 1]; |
|
| 431 |
✓ 10.4M
|
squeezedHistoryHashes[i] = historyHashes[historyHashes.length - revIdx]; |
|
| 432 |
✓ 10.4M
|
nextSqueezed[currCycleConfigs - revIdx] = _currTimestamp(); |
|
| 433 |
} |
||
| 434 |
✓ 22.6M
|
uint32 cycleStart = _currCycleStart(); |
|
| 435 |
✓ 22.6M
|
_addDeltaRange( |
|
| 436 |
✓ 22.6M
|
state, cycleStart, cycleStart + 1, -int160(amt * _AMT_PER_SEC_MULTIPLIER) |
|
| 437 |
); |
||
| 438 |
✓ 22.6M
|
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 |
✓ 35.8M
|
uint128 amt, |
|
| 476 |
✓ 35.8M
|
uint256 squeezedNum, |
|
| 477 |
✓ 35.8M
|
uint256[] memory squeezedRevIdxs, |
|
| 478 |
✓ 35.8M
|
bytes32[] memory historyHashes, |
|
| 479 |
✓ 35.8M
|
uint256 currCycleConfigs |
|
| 480 |
) |
||
| 481 |
{
|
||
| 482 |
{
|
||
| 483 |
✓ 35.8M
|
StreamsState storage sender = _streamsStorage().states[erc20][senderId]; |
|
| 484 |
historyHashes = |
||
| 485 |
✓ 35.8M
|
_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 |
✓ 35.8M
|
currCycleConfigs = 1; |
|
| 489 |
// slither-disable-next-line timestamp |
||
| 490 |
✓ 35.8M
|
if (sender.updateTime >= _currCycleStart()) {
|
|
| 491 |
✓ 1.6M
|
currCycleConfigs = sender.lastUpdatedCycleConfigs; |
|
| 492 |
} |
||
| 493 |
} |
||
| 494 |
✓ 35.8M
|
squeezedRevIdxs = new uint256[](streamsHistory.length); |
|
| 495 |
✓ 35.8M
|
uint32[2 ** 32] storage nextSqueezed = |
|
| 496 |
✓ 35.8M
|
_streamsStorage().states[erc20][accountId].nextSqueezed[senderId]; |
|
| 497 |
✓ 35.8M
|
uint32 squeezeEndCap = _currTimestamp(); |
|
| 498 |
unchecked {
|
||
| 499 |
✓ 70.0M
|
for (uint256 i = 1; i <= streamsHistory.length && i <= currCycleConfigs; i++) {
|
|
| 500 |
✓ 34.3M
|
StreamsHistory memory historyEntry = streamsHistory[streamsHistory.length - i]; |
|
| 501 |
✓ 34.3M
|
if (historyEntry.receivers.length != 0) {
|
|
| 502 |
✓ 19.8M
|
uint32 squeezeStartCap = nextSqueezed[currCycleConfigs - i]; |
|
| 503 |
✓ 19.8M
|
if (squeezeStartCap < _currCycleStart()) squeezeStartCap = _currCycleStart(); |
|
| 504 |
✓ 19.8M
|
if (squeezeStartCap < historyEntry.updateTime) {
|
|
| 505 |
✓ 1.1M
|
squeezeStartCap = historyEntry.updateTime; |
|
| 506 |
} |
||
| 507 |
✓ 19.8M
|
if (squeezeStartCap < squeezeEndCap) {
|
|
| 508 |
✓ 16.2M
|
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 |
✓ 16.2M
|
amt += _squeezedAmt(accountId, historyEntry, squeezeStartCap, squeezeEndCap); |
|
| 512 |
} |
||
| 513 |
} |
||
| 514 |
✓ 34.3M
|
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 |
✓ 35.8M
|
) private pure returns (bytes32[] memory historyHashes) {
|
|
| 533 |
✓ 35.8M
|
historyHashes = new bytes32[](streamsHistory.length); |
|
| 534 |
✓ 213.1M
|
for (uint256 i = 0; i < streamsHistory.length; i++) {
|
|
| 535 |
✓ 177.3M
|
StreamsHistory memory historyEntry = streamsHistory[i]; |
|
| 536 |
✓ 177.3M
|
bytes32 streamsHash = historyEntry.streamsHash; |
|
| 537 |
✓ 177.3M
|
if (historyEntry.receivers.length != 0) {
|
|
| 538 |
✓ 105.6M
|
require(streamsHash == 0, "Entry with hash and receivers"); |
|
| 539 |
✓ 105.6M
|
streamsHash = _hashStreams(historyEntry.receivers); |
|
| 540 |
} |
||
| 541 |
✓ 177.3M
|
historyHashes[i] = historyHash; |
|
| 542 |
✓ 177.3M
|
historyHash = _hashStreamsHistory( |
|
| 543 |
✓ 177.3M
|
historyHash, streamsHash, historyEntry.updateTime, historyEntry.maxEnd |
|
| 544 |
); |
||
| 545 |
} |
||
| 546 |
// slither-disable-next-line incorrect-equality,timestamp |
||
| 547 |
✓ 35.8M
|
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 |
✓ 16.2M
|
) private view returns (uint128 squeezedAmt) {
|
|
| 562 |
unchecked {
|
||
| 563 |
✓ 16.2M
|
StreamReceiver[] memory receivers = historyEntry.receivers; |
|
| 564 |
// Binary search for the `idx` of the first occurrence of `accountId` |
||
| 565 |
✓ 16.2M
|
uint256 idx = 0; |
|
| 566 |
✓ 36.8M
|
for (uint256 idxCap = receivers.length; idx < idxCap;) {
|
|
| 567 |
✓ 20.6M
|
uint256 idxMid = (idx + idxCap) / 2; |
|
| 568 |
✓ 20.6M
|
if (receivers[idxMid].accountId < accountId) {
|
|
| 569 |
✓ 7.2M
|
idx = idxMid + 1; |
|
| 570 |
} else {
|
||
| 571 |
✓ 13.4M
|
idxCap = idxMid; |
|
| 572 |
} |
||
| 573 |
} |
||
| 574 |
✓ 16.2M
|
uint32 updateTime = historyEntry.updateTime; |
|
| 575 |
✓ 16.2M
|
uint32 maxEnd = historyEntry.maxEnd; |
|
| 576 |
✓ 16.2M
|
uint256 amt = 0; |
|
| 577 |
✓ 22.1M
|
for (; idx < receivers.length; idx++) {
|
|
| 578 |
✓ 12.5M
|
StreamReceiver memory receiver = receivers[idx]; |
|
| 579 |
✓ 12.5M
|
if (receiver.accountId != accountId) break; |
|
| 580 |
✓ 5.9M
|
(uint32 start, uint32 end) = |
|
| 581 |
✓ 5.9M
|
_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 |
✓ 5.9M
|
amt += _streamedAmt(receiver.config.amtPerSec(), start, end); |
|
| 585 |
} |
||
| 586 |
✓ 16.2M
|
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 |
✓ 14.6M
|
bytes32 streamsHash, |
|
| 603 |
✓ 14.6M
|
bytes32 streamsHistoryHash, |
|
| 604 |
✓ 14.6M
|
uint32 updateTime, |
|
| 605 |
✓ 14.6M
|
uint128 balance, |
|
| 606 |
✓ 14.6M
|
uint32 maxEnd |
|
| 607 |
) |
||
| 608 |
{
|
||
| 609 |
✓ 14.6M
|
StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
|
| 610 |
return ( |
||
| 611 |
✓ 14.6M
|
state.streamsHash, |
|
| 612 |
✓ 14.6M
|
state.streamsHistoryHash, |
|
| 613 |
✓ 14.6M
|
state.updateTime, |
|
| 614 |
✓ 14.6M
|
state.balance, |
|
| 615 |
✓ 14.6M
|
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 |
✓ 4.5M
|
) internal view returns (uint128 balance) {
|
|
| 635 |
✓ 4.5M
|
StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
|
| 636 |
✓ 4.5M
|
require(timestamp >= state.updateTime, "Timestamp before the last update"); |
|
| 637 |
✓ 4.5M
|
_verifyStreamsReceivers(currReceivers, state); |
|
| 638 |
✓ 4.5M
|
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 |
✓ 17.8M
|
) private view returns (uint128 balance) {
|
|
| 658 |
unchecked {
|
||
| 659 |
✓ 17.8M
|
balance = lastBalance; |
|
| 660 |
✓ 35.2M
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 661 |
✓ 17.4M
|
StreamReceiver memory receiver = receivers[i]; |
|
| 662 |
✓ 17.4M
|
(uint32 start, uint32 end) = _streamRange({
|
|
| 663 |
✓ 17.4M
|
receiver: receiver, |
|
| 664 |
✓ 17.4M
|
updateTime: lastUpdate, |
|
| 665 |
✓ 17.4M
|
maxEnd: maxEnd, |
|
| 666 |
✓ 17.4M
|
startCap: lastUpdate, |
|
| 667 |
✓ 17.4M
|
endCap: timestamp |
|
| 668 |
}); |
||
| 669 |
✓ 17.4M
|
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 |
✓ 13.3M
|
) internal returns (int128 realBalanceDelta) {
|
|
| 725 |
unchecked {
|
||
| 726 |
✓ 13.3M
|
StreamsState storage state = _streamsStorage().states[erc20][accountId]; |
|
| 727 |
✓ 13.3M
|
_verifyStreamsReceivers(currReceivers, state); |
|
| 728 |
✓ 13.3M
|
uint32 lastUpdate = state.updateTime; |
|
| 729 |
✓ 13.3M
|
uint128 newBalance; |
|
| 730 |
✓ 13.3M
|
uint32 newMaxEnd; |
|
| 731 |
{
|
||
| 732 |
✓ 13.3M
|
uint32 currMaxEnd = state.maxEnd; |
|
| 733 |
✓ 13.3M
|
int128 currBalance = int128( |
|
| 734 |
✓ 13.3M
|
_calcBalance( |
|
| 735 |
✓ 13.3M
|
state.balance, lastUpdate, currMaxEnd, currReceivers, _currTimestamp() |
|
| 736 |
) |
||
| 737 |
); |
||
| 738 |
✓ 13.3M
|
realBalanceDelta = balanceDelta; |
|
| 739 |
// Cap `realBalanceDelta` at withdrawal of the entire `currBalance` |
||
| 740 |
✓ 13.3M
|
if (realBalanceDelta < -currBalance) {
|
|
| 741 |
✓ 9.7M
|
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 |
✓ 13.3M
|
newBalance = uint128(currBalance + realBalanceDelta); |
|
| 746 |
✓ 13.3M
|
newMaxEnd = _calcMaxEnd(newBalance, newReceivers, maxEndHint1, maxEndHint2); |
|
| 747 |
✓ 13.0M
|
_updateReceiverStates( |
|
| 748 |
✓ 13.0M
|
_streamsStorage().states[erc20], |
|
| 749 |
✓ 13.0M
|
currReceivers, |
|
| 750 |
✓ 13.0M
|
lastUpdate, |
|
| 751 |
✓ 13.0M
|
currMaxEnd, |
|
| 752 |
✓ 13.0M
|
newReceivers, |
|
| 753 |
✓ 13.0M
|
newMaxEnd |
|
| 754 |
); |
||
| 755 |
} |
||
| 756 |
✓ 13.0M
|
state.updateTime = _currTimestamp(); |
|
| 757 |
✓ 13.0M
|
state.maxEnd = newMaxEnd; |
|
| 758 |
✓ 13.0M
|
state.balance = newBalance; |
|
| 759 |
✓ 13.0M
|
bytes32 streamsHistory = state.streamsHistoryHash; |
|
| 760 |
// slither-disable-next-line timestamp |
||
| 761 |
✓ 13.0M
|
if (streamsHistory != 0 && _cycleOf(lastUpdate) != _cycleOf(_currTimestamp())) {
|
|
| 762 |
✓ 11.1M
|
state.lastUpdatedCycleConfigs = 2; |
|
| 763 |
} else {
|
||
| 764 |
✓ 1.9M
|
state.lastUpdatedCycleConfigs++; |
|
| 765 |
} |
||
| 766 |
✓ 13.0M
|
bytes32 newStreamsHash = _hashStreams(newReceivers); |
|
| 767 |
✓ 13.0M
|
state.streamsHistoryHash = |
|
| 768 |
✓ 13.0M
|
_hashStreamsHistory(streamsHistory, newStreamsHash, _currTimestamp(), newMaxEnd); |
|
| 769 |
✓ 13.0M
|
emit StreamsSet(accountId, erc20, newStreamsHash, streamsHistory, newBalance, newMaxEnd); |
|
| 770 |
// slither-disable-next-line timestamp |
||
| 771 |
✓ 13.0M
|
if (newStreamsHash != state.streamsHash) {
|
|
| 772 |
✓ 4.5M
|
state.streamsHash = newStreamsHash; |
|
| 773 |
✓ 11.3M
|
for (uint256 i = 0; i < newReceivers.length; i++) {
|
|
| 774 |
✓ 6.8M
|
StreamReceiver memory receiver = newReceivers[i]; |
|
| 775 |
✓ 6.8M
|
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 |
✓ 17.8M
|
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 |
✓ 13.3M
|
) private view returns (uint32 maxEnd) {
|
|
| 807 |
✓ 13.3M
|
(uint256[] memory configs, uint256 configsLen) = _buildConfigs(receivers); |
|
| 808 | |||
| 809 |
✓ 13.0M
|
uint256 enoughEnd = _currTimestamp(); |
|
| 810 |
// slither-disable-start incorrect-equality,timestamp |
||
| 811 |
✓ 13.0M
|
if (configsLen == 0 || balance == 0) {
|
|
| 812 |
✓ 10.8M
|
return uint32(enoughEnd); |
|
| 813 |
} |
||
| 814 | |||
| 815 |
✓ 2.2M
|
uint256 notEnoughEnd = type(uint32).max; |
|
| 816 |
✓ 2.2M
|
if (_isBalanceEnough(balance, configs, configsLen, notEnoughEnd)) {
|
|
| 817 |
✓ 470.7K
|
return uint32(notEnoughEnd); |
|
| 818 |
} |
||
| 819 | |||
| 820 |
✓ 1.7M
|
if (hint1 > enoughEnd && hint1 < notEnoughEnd) {
|
|
| 821 |
✓ 455.7K
|
if (_isBalanceEnough(balance, configs, configsLen, hint1)) {
|
|
| 822 |
✓ 58.3K
|
enoughEnd = hint1; |
|
| 823 |
} else {
|
||
| 824 |
✓ 397.5K
|
notEnoughEnd = hint1; |
|
| 825 |
} |
||
| 826 |
} |
||
| 827 | |||
| 828 |
✓ 1.7M
|
if (hint2 > enoughEnd && hint2 < notEnoughEnd) {
|
|
| 829 |
✓ 284.5K
|
if (_isBalanceEnough(balance, configs, configsLen, hint2)) {
|
|
| 830 |
✓ 47.4K
|
enoughEnd = hint2; |
|
| 831 |
} else {
|
||
| 832 |
✓ 237.1K
|
notEnoughEnd = hint2; |
|
| 833 |
} |
||
| 834 |
} |
||
| 835 | |||
| 836 |
✓ 55.6M
|
while (true) {
|
|
| 837 |
✓ 55.6M
|
uint256 end; |
|
| 838 |
unchecked {
|
||
| 839 |
✓ 55.6M
|
end = (enoughEnd + notEnoughEnd) / 2; |
|
| 840 |
} |
||
| 841 |
✓ 55.6M
|
if (end == enoughEnd) {
|
|
| 842 |
✓ 1.7M
|
return uint32(end); |
|
| 843 |
} |
||
| 844 |
✓ 53.8M
|
if (_isBalanceEnough(balance, configs, configsLen, end)) {
|
|
| 845 |
✓ 7.5M
|
enoughEnd = end; |
|
| 846 |
} else {
|
||
| 847 |
✓ 46.3M
|
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 |
✓ 56.8M
|
) private view returns (bool isEnough) {
|
|
| 865 |
unchecked {
|
||
| 866 |
✓ 56.8M
|
uint256 spent = 0; |
|
| 867 |
✓ 79.8M
|
for (uint256 i = 0; i < configsLen; i++) {
|
|
| 868 |
✓ 71.7M
|
(uint256 amtPerSec, uint256 start, uint256 end) = _getConfig(configs, i); |
|
| 869 |
// slither-disable-next-line timestamp |
||
| 870 |
✓ 71.7M
|
if (maxEnd <= start) {
|
|
| 871 |
✓ 7.3M
|
continue; |
|
| 872 |
} |
||
| 873 |
// slither-disable-next-line timestamp |
||
| 874 |
✓ 64.4M
|
if (end > maxEnd) {
|
|
| 875 |
✓ 42.3M
|
end = maxEnd; |
|
| 876 |
} |
||
| 877 |
✓ 64.4M
|
spent += _streamedAmt(amtPerSec, start, end); |
|
| 878 |
✓ 64.4M
|
if (spent > balance) {
|
|
| 879 |
✓ 48.7M
|
return false; |
|
| 880 |
} |
||
| 881 |
} |
||
| 882 |
✓ 8.1M
|
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 |
✓ 13.3M
|
returns (uint256[] memory configs, uint256 configsLen) |
|
| 896 |
{
|
||
| 897 |
unchecked {
|
||
| 898 |
✓ 13.3M
|
require(receivers.length <= _MAX_STREAMS_RECEIVERS, "Too many streams receivers"); |
|
| 899 |
✓ 13.3M
|
configs = new uint256[](receivers.length); |
|
| 900 |
✓ 26.9M
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 901 |
✓ 13.9M
|
StreamReceiver memory receiver = receivers[i]; |
|
| 902 |
✓ 13.9M
|
if (i > 0) {
|
|
| 903 |
✓ 4.8M
|
require(_isOrdered(receivers[i - 1], receiver), "Streams receivers not sorted"); |
|
| 904 |
} |
||
| 905 |
✓ 13.9M
|
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 |
✓ 13.9M
|
) private view returns (uint256 newConfigsLen) {
|
|
| 920 |
✓ 13.9M
|
uint160 amtPerSec = receiver.config.amtPerSec(); |
|
| 921 |
✓ 13.9M
|
require(amtPerSec >= _minAmtPerSec, "Stream receiver amtPerSec too low"); |
|
| 922 |
✓ 13.6M
|
(uint32 start, uint32 end) = |
|
| 923 |
✓ 13.6M
|
_streamRangeInFuture(receiver, _currTimestamp(), type(uint32).max); |
|
| 924 |
// slither-disable-next-line incorrect-equality,timestamp |
||
| 925 |
✓ 13.6M
|
if (start == end) {
|
|
| 926 |
✓ 5.2M
|
return configsLen; |
|
| 927 |
} |
||
| 928 |
// By assignment we get `config` value: |
||
| 929 |
// `zeros (96 bits) | amtPerSec (160 bits)` |
||
| 930 |
✓ 8.4M
|
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 |
✓ 8.4M
|
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 |
✓ 8.4M
|
config = (config << 32) | end; |
|
| 941 |
✓ 8.4M
|
configs[configsLen] = config; |
|
| 942 |
unchecked {
|
||
| 943 |
✓ 8.4M
|
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 |
✓ 71.7M
|
returns (uint256 amtPerSec, uint256 start, uint256 end) |
|
| 957 |
{
|
||
| 958 |
✓ 71.7M
|
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 |
✓ 71.7M
|
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 |
✓ 71.7M
|
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 |
✓ 71.7M
|
start = uint32(config >> 32); |
|
| 973 |
// By casting down we get value: |
||
| 974 |
// `end (32 bits)` |
||
| 975 |
✓ 71.7M
|
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 |
✓ 155.2M
|
returns (bytes32 streamsHash) |
|
| 987 |
{
|
||
| 988 |
✓ 155.2M
|
if (receivers.length == 0) {
|
|
| 989 |
✓ 17.6M
|
return bytes32(0); |
|
| 990 |
} |
||
| 991 |
✓ 137.6M
|
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 |
✓ 203.4M
|
) internal pure returns (bytes32 streamsHistoryHash) {
|
|
| 1009 |
✓ 203.4M
|
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 |
✓ 13.0M
|
uint256 currIdx = 0; |
|
| 1036 |
✓ 13.0M
|
uint256 newIdx = 0; |
|
| 1037 |
✓ 28.5M
|
while (true) {
|
|
| 1038 |
✓ 28.5M
|
bool pickCurr = currIdx < currReceivers.length; |
|
| 1039 |
// slither-disable-next-line uninitialized-local |
||
| 1040 |
✓ 28.5M
|
StreamReceiver memory currRecv; |
|
| 1041 |
✓ 28.5M
|
if (pickCurr) {
|
|
| 1042 |
✓ 13.1M
|
currRecv = currReceivers[currIdx]; |
|
| 1043 |
} |
||
| 1044 | |||
| 1045 |
✓ 28.5M
|
bool pickNew = newIdx < newReceivers.length; |
|
| 1046 |
// slither-disable-next-line uninitialized-local |
||
| 1047 |
✓ 28.5M
|
StreamReceiver memory newRecv; |
|
| 1048 |
✓ 28.5M
|
if (pickNew) {
|
|
| 1049 |
✓ 14.3M
|
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 |
✓ 28.5M
|
if (pickCurr && pickNew) {
|
|
| 1055 |
if ( |
||
| 1056 |
✓ 11.9M
|
currRecv.accountId != newRecv.accountId |
|
| 1057 |
✓ 10.4M
|
|| currRecv.config.amtPerSec() != newRecv.config.amtPerSec() |
|
| 1058 |
) {
|
||
| 1059 |
✓ 2.2M
|
pickCurr = _isOrdered(currRecv, newRecv); |
|
| 1060 |
✓ 2.2M
|
pickNew = !pickCurr; |
|
| 1061 |
} |
||
| 1062 |
} |
||
| 1063 | |||
| 1064 |
✓ 28.5M
|
if (pickCurr && pickNew) {
|
|
| 1065 |
// Shift the existing stream to fulfil the new configuration |
||
| 1066 |
✓ 9.7M
|
StreamsState storage state = states[currRecv.accountId]; |
|
| 1067 |
✓ 9.7M
|
(uint32 currStart, uint32 currEnd) = |
|
| 1068 |
✓ 9.7M
|
_streamRangeInFuture(currRecv, lastUpdate, currMaxEnd); |
|
| 1069 |
✓ 9.7M
|
(uint32 newStart, uint32 newEnd) = |
|
| 1070 |
✓ 9.7M
|
_streamRangeInFuture(newRecv, _currTimestamp(), newMaxEnd); |
|
| 1071 |
✓ 9.7M
|
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 |
✓ 9.7M
|
_addDeltaRange(state, currStart, newStart, -amtPerSec); |
|
| 1077 |
✓ 9.7M
|
_addDeltaRange(state, currEnd, newEnd, amtPerSec); |
|
| 1078 |
// Ensure that the account receives the updated cycles |
||
| 1079 |
✓ 9.7M
|
uint32 currStartCycle = _cycleOf(currStart); |
|
| 1080 |
✓ 9.7M
|
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 |
✓ 9.7M
|
if (currStartCycle > newStartCycle && state.nextReceivableCycle > newStartCycle) {
|
|
| 1086 |
✓ 17.1K
|
state.nextReceivableCycle = newStartCycle; |
|
| 1087 |
} |
||
| 1088 |
✓ 18.9M
|
} else if (pickCurr) {
|
|
| 1089 |
// Remove an existing stream |
||
| 1090 |
// slither-disable-next-line similar-names |
||
| 1091 |
✓ 1.9M
|
StreamsState storage state = states[currRecv.accountId]; |
|
| 1092 |
✓ 1.9M
|
(uint32 start, uint32 end) = _streamRangeInFuture(currRecv, lastUpdate, currMaxEnd); |
|
| 1093 |
// slither-disable-next-line similar-names |
||
| 1094 |
✓ 1.9M
|
int256 amtPerSec = int256(uint256(currRecv.config.amtPerSec())); |
|
| 1095 |
✓ 1.9M
|
_addDeltaRange(state, start, end, -amtPerSec); |
|
| 1096 |
✓ 17.0M
|
} else if (pickNew) {
|
|
| 1097 |
// Create a new stream |
||
| 1098 |
✓ 3.9M
|
StreamsState storage state = states[newRecv.accountId]; |
|
| 1099 |
// slither-disable-next-line uninitialized-local |
||
| 1100 |
✓ 3.9M
|
(uint32 start, uint32 end) = |
|
| 1101 |
✓ 3.9M
|
_streamRangeInFuture(newRecv, _currTimestamp(), newMaxEnd); |
|
| 1102 |
✓ 3.9M
|
int256 amtPerSec = int256(uint256(newRecv.config.amtPerSec())); |
|
| 1103 |
✓ 3.9M
|
_addDeltaRange(state, start, end, amtPerSec); |
|
| 1104 |
// Ensure that the account receives the updated cycles |
||
| 1105 |
✓ 3.9M
|
uint32 startCycle = _cycleOf(start); |
|
| 1106 |
// slither-disable-next-line timestamp |
||
| 1107 |
✓ 3.9M
|
uint32 nextReceivableCycle = state.nextReceivableCycle; |
|
| 1108 |
✓ 3.9M
|
if (nextReceivableCycle == 0 || nextReceivableCycle > startCycle) {
|
|
| 1109 |
✓ 1.5M
|
state.nextReceivableCycle = startCycle; |
|
| 1110 |
} |
||
| 1111 |
} else {
|
||
| 1112 |
✓ 13.0M
|
break; |
|
| 1113 |
} |
||
| 1114 | |||
| 1115 |
unchecked {
|
||
| 1116 |
✓ 15.5M
|
if (pickCurr) {
|
|
| 1117 |
✓ 11.6M
|
currIdx++; |
|
| 1118 |
} |
||
| 1119 |
✓ 15.5M
|
if (pickNew) {
|
|
| 1120 |
✓ 13.6M
|
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 |
✓ 38.8M
|
returns (uint32 start, uint32 end) |
|
| 1133 |
{
|
||
| 1134 |
✓ 38.8M
|
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 |
✓ 62.1M
|
) private pure returns (uint32 start, uint32 end_) {
|
|
| 1151 |
✓ 62.1M
|
start = receiver.config.start(); |
|
| 1152 |
// slither-disable-start timestamp |
||
| 1153 |
✓ 62.1M
|
if (start == 0) {
|
|
| 1154 |
✓ 16.5M
|
start = updateTime; |
|
| 1155 |
} |
||
| 1156 |
✓ 62.1M
|
uint40 end; |
|
| 1157 |
unchecked {
|
||
| 1158 |
✓ 62.1M
|
end = uint40(start) + receiver.config.duration(); |
|
| 1159 |
} |
||
| 1160 |
// slither-disable-next-line incorrect-equality |
||
| 1161 |
✓ 62.1M
|
if (end == start || end > maxEnd) {
|
|
| 1162 |
✓ 31.8M
|
end = maxEnd; |
|
| 1163 |
} |
||
| 1164 |
✓ 62.1M
|
if (start < startCap) {
|
|
| 1165 |
✓ 37.9M
|
start = startCap; |
|
| 1166 |
} |
||
| 1167 |
✓ 62.1M
|
if (end > endCap) {
|
|
| 1168 |
✓ 1.7M
|
end = endCap; |
|
| 1169 |
} |
||
| 1170 |
✓ 62.1M
|
if (end < start) {
|
|
| 1171 |
✓ 37.7M
|
end = start; |
|
| 1172 |
} |
||
| 1173 |
// slither-disable-end timestamp |
||
| 1174 |
✓ 62.1M
|
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 |
✓ 47.8M
|
if (start == end) {
|
|
| 1187 |
✓ 47.8M
|
return; |
|
| 1188 |
} |
||
| 1189 |
✓ 24.7M
|
mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
|
| 1190 |
✓ 24.7M
|
_addDelta(amtDeltas, start, amtPerSec); |
|
| 1191 |
✓ 24.7M
|
_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 |
✓ 49.5M
|
int256 amtPerSecMultiplier = int160(_AMT_PER_SEC_MULTIPLIER); |
|
| 1207 |
✓ 49.5M
|
int256 fullCycle = (int256(uint256(_cycleSecs)) * amtPerSec) / amtPerSecMultiplier; |
|
| 1208 |
// slither-disable-next-line weak-prng |
||
| 1209 |
✓ 49.5M
|
int256 nextCycle = (int256(timestamp % _cycleSecs) * amtPerSec) / amtPerSecMultiplier; |
|
| 1210 |
✓ 49.5M
|
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 |
✓ 49.5M
|
amtDelta.thisCycle += int128(fullCycle - nextCycle); |
|
| 1216 |
✓ 49.5M
|
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 |
✓ 7.0M
|
returns (bool) |
|
| 1227 |
{
|
||
| 1228 |
✓ 7.0M
|
if (prev.accountId != next.accountId) {
|
|
| 1229 |
✓ 3.7M
|
return prev.accountId < next.accountId; |
|
| 1230 |
} |
||
| 1231 |
✓ 3.3M
|
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 |
✓ 87.7M
|
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 |
✓ 87.7M
|
uint256 cycleSecs = _cycleSecs; |
|
| 1265 |
// slither-disable-next-line assembly |
||
| 1266 |
assembly {
|
||
| 1267 |
✓ 87.7M
|
let endedCycles := sub(div(end, cycleSecs), div(start, cycleSecs)) |
|
| 1268 |
// slither-disable-next-line divide-before-multiply |
||
| 1269 |
✓ 87.7M
|
let amtPerCycle := div(mul(cycleSecs, amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
|
| 1270 |
✓ 87.7M
|
amt := mul(endedCycles, amtPerCycle) |
|
| 1271 |
// slither-disable-next-line weak-prng |
||
| 1272 |
✓ 87.7M
|
let amtEnd := div(mul(mod(end, cycleSecs), amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
|
| 1273 |
✓ 87.7M
|
amt := add(amt, amtEnd) |
|
| 1274 |
// slither-disable-next-line weak-prng |
||
| 1275 |
✓ 87.7M
|
let amtStart := div(mul(mod(start, cycleSecs), amtPerSec), _AMT_PER_SEC_MULTIPLIER) |
|
| 1276 |
✓ 87.7M
|
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 |
✓ 114.2M
|
function _cycleOf(uint32 timestamp) private view returns (uint32 cycle) {
|
|
| 1284 |
unchecked {
|
||
| 1285 |
✓ 114.2M
|
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 |
✓ 291.7M
|
function _currTimestamp() private view returns (uint32 timestamp) {
|
|
| 1292 |
✓ 291.7M
|
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 |
✓ 97.3M
|
function _currCycleStart() private view returns (uint32 timestamp) {
|
|
| 1298 |
unchecked {
|
||
| 1299 |
✓ 97.3M
|
uint32 currTimestamp = _currTimestamp(); |
|
| 1300 |
// slither-disable-next-line weak-prng |
||
| 1301 |
✓ 97.3M
|
return currTimestamp - (currTimestamp % _cycleSecs); |
|
| 1302 |
} |
||
| 1303 |
} |
||
| 1304 | |||
| 1305 |
/// @notice Returns the Streams storage. |
||
| 1306 |
/// @return streamsStorage The storage. |
||
| 1307 |
✓ 179.8M
|
function _streamsStorage() private view returns (StreamsStorage storage streamsStorage) {
|
|
| 1308 |
✓ 179.8M
|
bytes32 slot = _streamsStorageSlot; |
|
| 1309 |
// slither-disable-next-line assembly |
||
| 1310 |
assembly {
|
||
| 1311 |
✓ 179.8M
|
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.5%
src/echidna/EchidnaBasicHelpers.sol
Lines covered: 39 / 40 (97.5%)
| 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 |
✓ 1.2M
|
address from = getAccount(fromAccId); |
|
| 22 |
✓ 1.2M
|
address to = getAccount(toAccId); |
|
| 23 | |||
| 24 |
✓ 1.2M
|
uint256 toDripsAccId = getDripsAccountId(to); |
|
| 25 | |||
| 26 |
✓ 1.2M
|
hevm.prank(from); |
|
| 27 |
✓ 1.2M
|
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 |
✓ 674.6K
|
address from = getAccount(fromAccId); |
|
| 42 | |||
| 43 |
✓ 674.6K
|
uint128 min = 1000; |
|
| 44 |
✓ 674.6K
|
uint128 max = uint128(token.balanceOf(from)); |
|
| 45 |
✓ 674.6K
|
uint128 clampedAmount = min + (amount % (max - min + 1)); |
|
| 46 | |||
| 47 |
✓ 664.1K
|
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 |
✓ 5.7M
|
returns (uint128) |
|
| 61 |
{
|
||
| 62 |
✓ 5.7M
|
address target = getAccount(targetAccId); |
|
| 63 |
✓ 5.7M
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 64 | |||
| 65 |
✓ 5.7M
|
uint128 receivedAmt = drips.receiveStreams( |
|
| 66 |
✓ 5.7M
|
targetDripsAccId, |
|
| 67 |
✓ 5.7M
|
token, |
|
| 68 |
✓ 5.7M
|
maxCycles |
|
| 69 |
); |
||
| 70 | |||
| 71 |
✓ 5.7M
|
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 |
✓ 4.8M
|
returns (uint128) |
|
| 83 |
{
|
||
| 84 |
✓ 4.8M
|
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 |
✓ 8.2M
|
function split(uint8 targetAccId) public returns (uint128, uint128) {
|
|
| 95 |
✓ 8.2M
|
address target = getAccount(targetAccId); |
|
| 96 |
✓ 8.2M
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 97 | |||
| 98 |
✓ 8.2M
|
(uint128 collectableAmt, uint128 splitAmt) = drips.split( |
|
| 99 |
✓ 8.2M
|
targetDripsAccId, |
|
| 100 |
✓ 8.2M
|
token, |
|
| 101 |
✓ 8.2M
|
getSplitsReceivers(target) |
|
| 102 |
); |
||
| 103 | |||
| 104 |
✓ 8.2M
|
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 |
✓ 6.3M
|
function collect(uint8 fromAccId, uint8 toAccId) public returns (uint128) {
|
|
| 116 |
✓ 6.3M
|
address from = getAccount(fromAccId); |
|
| 117 |
✓ 6.3M
|
address to = getAccount(toAccId); |
|
| 118 | |||
| 119 |
✓ 6.3M
|
hevm.prank(from); |
|
| 120 |
✓ 6.3M
|
uint128 collected = driver.collect(token, to); |
|
| 121 | |||
| 122 |
✓ 6.3M
|
return collected; |
|
| 123 |
} |
||
| 124 | |||
| 125 |
/** |
||
| 126 |
* @notice Collect funds to self |
||
| 127 |
* @param targetAccId Target account |
||
| 128 |
*/ |
||
| 129 |
function collectToSelf(uint8 targetAccId) public {
|
||
| 130 |
✓ 5.1M
|
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 |
✓ 4.7M
|
split(targetAccId); |
|
| 140 |
✓ 4.7M
|
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 |
✓ 4.3M
|
receiveStreamsAllCycles(targetAccId); |
|
| 150 |
✓ 4.3M
|
splitAndCollectToSelf(targetAccId); |
|
| 151 |
} |
||
| 152 |
} |
||
| 153 |
93.1%
src/echidna/EchidnaBasicTests.sol
Lines covered: 54 / 58 (93.1%)
| 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 |
✓ 382.7K
|
address from = getAccount(fromAccId); |
|
| 23 |
✓ 382.7K
|
address to = getAccount(toAccId); |
|
| 24 | |||
| 25 |
✓ 382.7K
|
uint256 toDripsAccId = getDripsAccountId(to); |
|
| 26 | |||
| 27 |
✓ 382.7K
|
require(amount <= token.balanceOf(from)); |
|
| 28 | |||
| 29 |
✓ 96.5K
|
hevm.prank(from); |
|
| 30 |
✓ 96.5K
|
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 |
✓ 395.9K
|
address target = getAccount(targetAccId); |
|
| 42 |
✓ 395.9K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 43 | |||
| 44 |
✓ 395.9K
|
uint128 splittableBefore = drips.splittable(targetDripsAccId, token); |
|
| 45 |
✓ 395.9K
|
uint128 receivedAmt = receiveStreams(targetAccId, maxCycles); |
|
| 46 |
✓ 395.9K
|
uint128 splittableAfter = drips.splittable(targetDripsAccId, token); |
|
| 47 | |||
| 48 |
✓ 395.9K
|
assert(splittableAfter == splittableBefore + receivedAmt); |
|
| 49 | |||
| 50 |
✓ 395.9K
|
if (receivedAmt > 0) {
|
|
| 51 |
✓ 44.3K
|
assert(splittableAfter > splittableBefore); |
|
| 52 |
} else {
|
||
| 53 |
✓ 351.6K
|
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 |
✓ 418.2K
|
address target = getAccount(targetAccId); |
|
| 63 |
✓ 418.2K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 64 | |||
| 65 |
try |
||
| 66 |
✓ 418.2K
|
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 |
✓ 535.8K
|
address target = getAccount(targetAccId); |
|
| 83 |
✓ 535.8K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 84 | |||
| 85 |
✓ 535.8K
|
require(maxCycles > 0); |
|
| 86 | |||
| 87 |
✓ 507.4K
|
uint128 receivable = drips.receiveStreamsResult( |
|
| 88 |
✓ 507.4K
|
targetDripsAccId, |
|
| 89 |
✓ 507.4K
|
token, |
|
| 90 |
✓ 507.4K
|
maxCycles |
|
| 91 |
); |
||
| 92 |
✓ 507.4K
|
uint32 receivableCycles = drips.receivableStreamsCycles( |
|
| 93 |
✓ 507.4K
|
targetDripsAccId, |
|
| 94 |
✓ 507.4K
|
token |
|
| 95 |
); |
||
| 96 | |||
| 97 |
✓ 507.4K
|
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 |
✓ 375.9K
|
address target = getAccount(targetAccId); |
|
| 112 |
✓ 375.9K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 113 | |||
| 114 |
✓ 375.9K
|
uint128 receivable = drips.receiveStreamsResult( |
|
| 115 |
✓ 375.9K
|
targetDripsAccId, |
|
| 116 |
✓ 375.9K
|
token, |
|
| 117 |
✓ 375.9K
|
maxCycles |
|
| 118 |
); |
||
| 119 | |||
| 120 |
✓ 375.9K
|
uint128 received = drips.receiveStreams( |
|
| 121 |
✓ 375.9K
|
targetDripsAccId, |
|
| 122 |
✓ 375.9K
|
token, |
|
| 123 |
✓ 375.9K
|
maxCycles |
|
| 124 |
); |
||
| 125 | |||
| 126 |
✓ 375.9K
|
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 |
✓ 650.3K
|
address from = getAccount(fromAccId); |
|
| 136 |
✓ 650.3K
|
address to = getAccount(toAccId); |
|
| 137 | |||
| 138 |
✓ 650.3K
|
uint256 fromDripsAccId = getDripsAccountId(from); |
|
| 139 | |||
| 140 |
✓ 650.3K
|
uint128 colBalBefore = drips.collectable(fromDripsAccId, token); |
|
| 141 |
✓ 650.3K
|
uint256 tokenBalBefore = token.balanceOf(to); |
|
| 142 | |||
| 143 |
✓ 650.3K
|
uint128 collected = collect(fromAccId, toAccId); |
|
| 144 | |||
| 145 |
✓ 650.3K
|
uint128 colBalAfter = drips.collectable(fromDripsAccId, token); |
|
| 146 |
✓ 650.3K
|
uint256 tokenBalAfter = token.balanceOf(to); |
|
| 147 | |||
| 148 |
✓ 650.3K
|
assert(colBalAfter == colBalBefore - collected); |
|
| 149 |
✓ 650.3K
|
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 |
✓ 378.2K
|
address from = getAccount(fromAccId); |
|
| 159 |
✓ 378.2K
|
address to = getAccount(toAccId); |
|
| 160 | |||
| 161 |
✓ 378.2K
|
hevm.prank(from); |
|
| 162 |
✓ 378.2K
|
try driver.collect(token, to) {} catch {
|
|
| 163 |
assert(false); |
||
| 164 |
} |
||
| 165 |
} |
||
| 166 |
} |
||
| 167 |
95.2%
src/echidna/EchidnaInvariantTests.sol
Lines covered: 60 / 63 (95.2%)
| 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 |
✓ 668.5K
|
require(amount > 0, "withdraw amount must be > 0"); |
|
| 24 | |||
| 25 |
✓ 656.5K
|
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 |
✓ 387.8K
|
address target = getAccount(targetAccId); |
|
| 39 | |||
| 40 |
✓ 387.8K
|
StreamReceiver[] memory receivers = getStreamReceivers(target); |
|
| 41 |
✓ 387.8K
|
require(receivers.length > 0, "no receivers"); |
|
| 42 | |||
| 43 |
✓ 231.7K
|
index = index % receivers.length; |
|
| 44 |
✓ 231.7K
|
uint160 amtPerSec = receivers[index].config.amtPerSec(); |
|
| 45 | |||
| 46 |
✓ 231.7K
|
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 |
✓ 394.1K
|
uint256 tokenBalance = token.balanceOf(address(drips)); |
|
| 55 |
✓ 394.1K
|
uint256 dripsBalancesTotal = getDripsBalancesTotalForAllUsers(); |
|
| 56 | |||
| 57 |
✓ 394.1K
|
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 |
✓ 882.6K
|
removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
|
| 68 |
✓ 882.6K
|
removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
|
| 69 |
✓ 882.6K
|
removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
|
| 70 |
✓ 882.6K
|
removeAllSplits(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
|
| 71 | |||
| 72 |
✓ 882.6K
|
squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
|
| 73 |
✓ 882.6K
|
squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
|
| 74 |
✓ 882.6K
|
squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
|
| 75 |
✓ 882.6K
|
squeezeAllSenders(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
|
| 76 | |||
| 77 |
✓ 882.6K
|
receiveStreamsSplitAndCollectToSelf( |
|
| 78 |
✓ 882.6K
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] |
|
| 79 |
); |
||
| 80 |
✓ 882.6K
|
receiveStreamsSplitAndCollectToSelf( |
|
| 81 |
✓ 882.6K
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] |
|
| 82 |
); |
||
| 83 |
✓ 882.6K
|
receiveStreamsSplitAndCollectToSelf( |
|
| 84 |
✓ 882.6K
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] |
|
| 85 |
); |
||
| 86 |
✓ 882.6K
|
receiveStreamsSplitAndCollectToSelf( |
|
| 87 |
✓ 882.6K
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3] |
|
| 88 |
); |
||
| 89 | |||
| 90 |
✓ 882.6K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
|
| 91 |
✓ 882.6K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
|
| 92 |
✓ 882.6K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
|
| 93 |
✓ 882.6K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
|
| 94 | |||
| 95 |
✓ 882.6K
|
uint256 dripsBalance = token.balanceOf(address(drips)); |
|
| 96 |
✓ 882.6K
|
uint256 user0Balance = token.balanceOf(ADDRESS_USER0); |
|
| 97 |
✓ 882.6K
|
uint256 user1Balance = token.balanceOf(ADDRESS_USER1); |
|
| 98 |
✓ 882.6K
|
uint256 user2Balance = token.balanceOf(ADDRESS_USER2); |
|
| 99 |
✓ 882.6K
|
uint256 user3Balance = token.balanceOf(ADDRESS_USER3); |
|
| 100 | |||
| 101 |
✓ 882.6K
|
uint256 totalUserBalance = user0Balance + |
|
| 102 |
✓ 882.6K
|
user1Balance + |
|
| 103 |
✓ 882.6K
|
user2Balance + |
|
| 104 |
✓ 882.6K
|
user3Balance; |
|
| 105 | |||
| 106 |
✓ 882.6K
|
assert(dripsBalance == 0); |
|
| 107 |
✓ 882.6K
|
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 |
✓ 387.1K
|
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 |
✓ 382.7K
|
address target = getAccount(targetAccId); |
|
| 127 |
✓ 382.7K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 128 | |||
| 129 |
✓ 382.7K
|
uint32 maxEnd = getMaxEndForAllUsers(); |
|
| 130 | |||
| 131 |
✓ 382.7K
|
uint32 firstCycle = getCycleFromTimestamp(STARTING_TIMESTAMP); |
|
| 132 |
✓ 382.7K
|
uint32 lastCycle = getCycleFromTimestamp(maxEnd); |
|
| 133 | |||
| 134 |
✓ 382.7K
|
require(maxEnd > 0, "no cycles"); |
|
| 135 |
✓ 367.7K
|
require(firstCycle != lastCycle, "only one cycle"); |
|
| 136 | |||
| 137 |
// limit amount of cycles for gas & memory savings |
||
| 138 |
✓ 365.5K
|
require(lastCycle - firstCycle < 1000, "too many cycles"); |
|
| 139 | |||
| 140 |
✓ 306.9K
|
int256 sumAmtDelta = 0; |
|
| 141 | |||
| 142 |
✓ 77.1M
|
for (uint32 cycle = firstCycle; cycle <= lastCycle; cycle++) {
|
|
| 143 |
✓ 76.8M
|
(int128 thisCycle, int128 nextCycle) = drips.getAmtDeltaForCycle( |
|
| 144 |
✓ 76.8M
|
targetDripsAccId, |
|
| 145 |
✓ 76.8M
|
token, |
|
| 146 |
✓ 76.8M
|
cycle |
|
| 147 |
); |
||
| 148 | |||
| 149 |
✓ 76.8M
|
sumAmtDelta += int256(thisCycle); |
|
| 150 |
✓ 76.8M
|
sumAmtDelta += int256(nextCycle); |
|
| 151 |
} |
||
| 152 | |||
| 153 |
✓ 306.9K
|
assert(sumAmtDelta == 0); |
|
| 154 |
} |
||
| 155 |
} |
||
| 156 |
97.7%
src/echidna/EchidnaSplitsHelpers.sol
Lines covered: 43 / 44 (97.7%)
| 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 |
✓ 6.9M
|
address sender = getAccount(senderAccId); |
|
| 21 | |||
| 22 |
✓ 6.9M
|
SplitsReceiver[] memory newReceivers = bubbleSortSplitsReceivers( |
|
| 23 |
✓ 6.9M
|
unsortedReceivers |
|
| 24 |
); |
||
| 25 | |||
| 26 |
✓ 6.8M
|
updateSplitsReceivers(sender, newReceivers); |
|
| 27 | |||
| 28 |
✓ 6.8M
|
hevm.prank(sender); |
|
| 29 |
✓ 6.8M
|
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 |
✓ 1.2M
|
address sender = getAccount(senderAccId); |
|
| 44 |
✓ 1.2M
|
address receiver = getAccount(receiverAccId); |
|
| 45 |
✓ 1.2M
|
uint256 receiverDripsAccId = getDripsAccountId(receiver); |
|
| 46 | |||
| 47 |
✓ 1.2M
|
SplitsReceiver[] memory receivers = new SplitsReceiver[](1); |
|
| 48 |
✓ 1.2M
|
receivers[0] = SplitsReceiver({
|
|
| 49 |
✓ 1.2M
|
accountId: receiverDripsAccId, |
|
| 50 |
✓ 1.2M
|
weight: weight |
|
| 51 |
}); |
||
| 52 |
✓ 1.2M
|
updateSplitsReceivers(sender, receivers); |
|
| 53 | |||
| 54 |
✓ 1.2M
|
_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 |
✓ 793.3K
|
weight = clampSplitWeight(weight, 0); // there are no existing weights |
|
| 71 |
✓ 793.3K
|
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 |
✓ 1.4M
|
address sender = getAccount(senderAccId); |
|
| 86 |
✓ 1.4M
|
address receiver = getAccount(receiverAccId); |
|
| 87 |
✓ 1.4M
|
uint256 senderDripsAccId = getDripsAccountId(sender); |
|
| 88 |
✓ 1.4M
|
uint256 receiverDripsAccId = getDripsAccountId(receiver); |
|
| 89 | |||
| 90 |
✓ 1.4M
|
SplitsReceiver[] memory oldReceivers = getSplitsReceivers(sender); |
|
| 91 | |||
| 92 |
✓ 1.4M
|
SplitsReceiver memory addedReceiver = SplitsReceiver({
|
|
| 93 |
✓ 1.4M
|
accountId: receiverDripsAccId, |
|
| 94 |
✓ 1.4M
|
weight: weight |
|
| 95 |
}); |
||
| 96 | |||
| 97 |
✓ 1.4M
|
SplitsReceiver[] memory newReceivers = new SplitsReceiver[]( |
|
| 98 |
✓ 1.4M
|
oldReceivers.length + 1 |
|
| 99 |
); |
||
| 100 |
✓ 1.8M
|
for (uint256 i = 0; i < oldReceivers.length; i++) {
|
|
| 101 |
✓ 371.0K
|
newReceivers[i] = oldReceivers[i]; |
|
| 102 |
} |
||
| 103 |
✓ 1.4M
|
newReceivers[newReceivers.length - 1] = addedReceiver; |
|
| 104 | |||
| 105 |
✓ 1.4M
|
_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 |
✓ 925.5K
|
address sender = getAccount(senderAccId); |
|
| 122 | |||
| 123 |
// sum all the existing weights |
||
| 124 |
✓ 925.5K
|
uint32 existingWeights; |
|
| 125 |
✓ 925.5K
|
SplitsReceiver[] memory receivers = getSplitsReceivers(sender); |
|
| 126 |
✓ 1.2M
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 127 |
✓ 251.5K
|
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 |
✓ 925.5K
|
if (existingWeights >= drips.TOTAL_SPLITS_WEIGHT()) return; |
|
| 133 | |||
| 134 |
✓ 904.8K
|
weight = clampSplitWeight(weight, existingWeights); |
|
| 135 | |||
| 136 |
✓ 904.8K
|
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 |
✓ 4.3M
|
SplitsReceiver[] memory receivers = new SplitsReceiver[](0); |
|
| 145 |
✓ 4.3M
|
_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 |
✓ 1.7M
|
returns (uint32) |
|
| 158 |
{
|
||
| 159 |
✓ 1.7M
|
return (weight % (drips.TOTAL_SPLITS_WEIGHT() - existingWeights)) + 1; |
|
| 160 |
} |
||
| 161 |
} |
||
| 162 |
95.5%
src/echidna/EchidnaSplitsTests.sol
Lines covered: 84 / 88 (95.5%)
| 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 |
✓ 635.9K
|
address target = getAccount(targetAccId); |
|
| 22 |
✓ 635.9K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 23 | |||
| 24 |
✓ 635.9K
|
uint128 splittableBefore = drips.splittable(targetDripsAccId, token); |
|
| 25 | |||
| 26 |
// check if we are splitting to ourselves |
||
| 27 |
✓ 635.9K
|
uint32 splitToSelfWeight; |
|
| 28 |
✓ 635.9K
|
SplitsReceiver[] memory receivers = getSplitsReceivers(target); |
|
| 29 |
✓ 870.5K
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 30 |
✓ 234.6K
|
if (receivers[i].accountId == targetDripsAccId) {
|
|
| 31 |
✓ 32.2K
|
splitToSelfWeight += receivers[i].weight; |
|
| 32 |
} |
||
| 33 |
} |
||
| 34 | |||
| 35 |
// calculate amount to split to self |
||
| 36 |
✓ 635.9K
|
uint128 splitToSelfAmount = uint128( |
|
| 37 |
✓ 635.9K
|
(splittableBefore * splitToSelfWeight) / drips.TOTAL_SPLITS_WEIGHT() |
|
| 38 |
); |
||
| 39 | |||
| 40 |
✓ 635.9K
|
(uint128 collectableAmt, uint128 splitAmt) = split(targetAccId); |
|
| 41 | |||
| 42 |
✓ 635.9K
|
uint128 splittableAfter = drips.splittable(targetDripsAccId, token); |
|
| 43 | |||
| 44 |
// sanity check |
||
| 45 |
✓ 635.9K
|
assert((splitAmt + collectableAmt) <= splittableBefore); |
|
| 46 | |||
| 47 |
✓ 635.9K
|
if (splitToSelfWeight == 0) {
|
|
| 48 |
// if we're not splitting to ourselves, things are simple |
||
| 49 |
assert( |
||
| 50 |
✓ 603.7K
|
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 |
✓ 32.2K
|
uint128 expectedSplittableAfter = splittableBefore - |
|
| 58 |
✓ 32.2K
|
splitAmt - |
|
| 59 |
✓ 32.2K
|
collectableAmt + |
|
| 60 |
✓ 32.2K
|
splitToSelfAmount; |
|
| 61 | |||
| 62 |
// calculate difference between expected and actual |
||
| 63 |
✓ 32.2K
|
int256 difference = int256(uint256(splittableAfter)) - |
|
| 64 |
✓ 32.2K
|
int256(uint256(expectedSplittableAfter)); |
|
| 65 | |||
| 66 |
// check if difference is within tolerance |
||
| 67 |
assert( |
||
| 68 |
✓ 32.2K
|
difference >= -int256(SPLIT_ROUNDING_TOLERANCE) && |
|
| 69 |
✓ 32.2K
|
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 |
✓ 712.6K
|
address target = getAccount(targetAccId); |
|
| 80 |
✓ 712.6K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 81 | |||
| 82 |
✓ 712.6K
|
uint128 colBalBefore = drips.collectable(targetDripsAccId, token); |
|
| 83 |
✓ 712.6K
|
(uint128 collectableAmt, ) = split(targetAccId); |
|
| 84 |
✓ 712.6K
|
uint128 colBalAfter = drips.collectable(targetDripsAccId, token); |
|
| 85 | |||
| 86 |
✓ 712.6K
|
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 |
✓ 369.1K
|
address target = getAccount(targetAccId); |
|
| 96 |
✓ 369.1K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 97 | |||
| 98 |
✓ 369.1K
|
uint128 amountToBeSplit = drips.splittable(targetDripsAccId, token); |
|
| 99 |
✓ 369.1K
|
SplitsReceiver[] memory receivers = getSplitsReceivers(target); |
|
| 100 | |||
| 101 |
// storage for all receivers |
||
| 102 |
✓ 369.1K
|
uint128[] memory splittableBefore = new uint128[](receivers.length); |
|
| 103 |
✓ 369.1K
|
uint128[] memory splittableAfter = new uint128[](receivers.length); |
|
| 104 |
✓ 369.1K
|
uint32[] memory weights = new uint32[](receivers.length); |
|
| 105 |
✓ 369.1K
|
uint128[] memory amounts = new uint128[](receivers.length); |
|
| 106 | |||
| 107 |
✓ 488.5K
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 108 |
// store splittable before |
||
| 109 |
✓ 119.4K
|
splittableBefore[i] = drips.splittable( |
|
| 110 |
✓ 119.4K
|
receivers[i].accountId, |
|
| 111 |
✓ 119.4K
|
token |
|
| 112 |
); |
||
| 113 | |||
| 114 |
// calculate amount the receiver should get |
||
| 115 |
✓ 119.4K
|
weights[i] = receivers[i].weight; |
|
| 116 |
✓ 119.4K
|
amounts[i] = uint128( |
|
| 117 |
✓ 119.4K
|
(amountToBeSplit * weights[i]) / drips.TOTAL_SPLITS_WEIGHT() |
|
| 118 |
); |
||
| 119 |
} |
||
| 120 | |||
| 121 |
// split |
||
| 122 |
✓ 369.1K
|
(uint128 collectableAmt, uint128 splitAmt) = split(targetAccId); |
|
| 123 | |||
| 124 |
// store splittable after |
||
| 125 |
✓ 488.5K
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 126 |
✓ 119.4K
|
splittableAfter[i] = drips.splittable( |
|
| 127 |
✓ 119.4K
|
receivers[i].accountId, |
|
| 128 |
✓ 119.4K
|
token |
|
| 129 |
); |
||
| 130 |
} |
||
| 131 | |||
| 132 |
✓ 488.5K
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 133 |
// calculate expected amount after the split |
||
| 134 |
✓ 119.4K
|
uint128 expectedAfter; |
|
| 135 |
✓ 119.4K
|
if (receivers[i].accountId != targetDripsAccId) {
|
|
| 136 |
// splitting so someone else is trivial |
||
| 137 |
✓ 88.5K
|
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 |
✓ 30.9K
|
splittableBefore[i] - |
|
| 145 |
✓ 30.9K
|
amountToBeSplit + |
|
| 146 |
✓ 30.9K
|
amounts[i]; |
|
| 147 |
} |
||
| 148 | |||
| 149 |
// calculate difference between expected and actual |
||
| 150 |
✓ 119.4K
|
int256 difference = int256(uint256(splittableAfter[i])) - |
|
| 151 |
✓ 119.4K
|
int256(uint256(expectedAfter)); |
|
| 152 | |||
| 153 |
// check if difference is within tolerance |
||
| 154 |
assert( |
||
| 155 |
✓ 119.4K
|
difference >= -int256(SPLIT_ROUNDING_TOLERANCE) && |
|
| 156 |
✓ 119.4K
|
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 |
✓ 488.4K
|
address target = getAccount(targetAccId); |
|
| 167 |
✓ 488.4K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 168 | |||
| 169 |
✓ 488.4K
|
uint128 splittable = drips.splittable(targetDripsAccId, token); |
|
| 170 | |||
| 171 |
✓ 488.4K
|
(uint128 collectableAmtView, uint128 splitAmtView) = drips.splitResult( |
|
| 172 |
✓ 488.4K
|
targetDripsAccId, |
|
| 173 |
✓ 488.4K
|
getSplitsReceivers(target), |
|
| 174 |
✓ 488.4K
|
splittable |
|
| 175 |
); |
||
| 176 | |||
| 177 |
✓ 488.4K
|
(uint128 collectableAmtActual, uint128 splitAmtActual) = split( |
|
| 178 |
✓ 488.4K
|
targetAccId |
|
| 179 |
); |
||
| 180 | |||
| 181 |
✓ 488.4K
|
assert(collectableAmtView == collectableAmtActual); |
|
| 182 |
✓ 488.4K
|
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 |
✓ 801.2K
|
address target = getAccount(targetAccId); |
|
| 191 |
✓ 801.2K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 192 | |||
| 193 |
✓ 801.2K
|
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 |
✓ 397.9K
|
EchidnaSplitsHelpers(address(this)).setSplitsWithClamping( |
|
| 211 |
✓ 397.9K
|
senderAccId, |
|
| 212 |
✓ 397.9K
|
receiverAccId, |
|
| 213 |
✓ 397.9K
|
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 |
✓ 518.6K
|
EchidnaSplitsHelpers(address(this)).addSplitsReceiverWithClamping( |
|
| 233 |
✓ 518.6K
|
senderAccId, |
|
| 234 |
✓ 518.6K
|
receiverAccId, |
|
| 235 |
✓ 518.6K
|
weight |
|
| 236 |
) |
||
| 237 |
{} catch (bytes memory reason) {
|
||
| 238 |
✓ 32.6K
|
bytes4 errorSelector = bytes4(reason); |
|
| 239 |
✓ 32.6K
|
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.5%
src/echidna/EchidnaSqueezeHelpers.sol
Lines covered: 66 / 67 (98.5%)
| 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 |
✓ 22.6M
|
) internal returns (uint128) {
|
|
| 25 |
✓ 22.6M
|
address receiver = getAccount(receiverAccId); |
|
| 26 |
✓ 22.6M
|
address sender = getAccount(senderAccId); |
|
| 27 |
✓ 22.6M
|
uint256 receiverDripsAccId = getDripsAccountId(receiver); |
|
| 28 |
✓ 22.6M
|
uint256 senderDripsAccId = getDripsAccountId(sender); |
|
| 29 | |||
| 30 |
✓ 22.6M
|
uint128 amount = drips.squeezeStreams( |
|
| 31 |
✓ 22.6M
|
receiverDripsAccId, |
|
| 32 |
✓ 22.6M
|
token, |
|
| 33 |
✓ 22.6M
|
senderDripsAccId, |
|
| 34 |
✓ 22.6M
|
historyHash, |
|
| 35 |
✓ 22.6M
|
history |
|
| 36 |
); |
||
| 37 | |||
| 38 |
✓ 22.6M
|
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 |
✓ 20.0M
|
returns (uint128) |
|
| 50 |
{
|
||
| 51 |
return |
||
| 52 |
✓ 20.0M
|
_squeeze( |
|
| 53 |
✓ 20.0M
|
receiverAccId, |
|
| 54 |
✓ 20.0M
|
senderAccId, |
|
| 55 |
✓ 20.0M
|
bytes32(0), |
|
| 56 |
✓ 20.0M
|
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 |
✓ 2.3M
|
) public returns (uint128) {
|
|
| 77 |
✓ 2.3M
|
address receiver = getAccount(receiverAccId); |
|
| 78 |
✓ 2.3M
|
address sender = getAccount(senderAccId); |
|
| 79 |
✓ 2.3M
|
uint256 receiverDripsAccId = getDripsAccountId(receiver); |
|
| 80 |
✓ 2.3M
|
uint256 senderDripsAccId = getDripsAccountId(sender); |
|
| 81 | |||
| 82 |
( |
||
| 83 |
✓ 2.3M
|
bytes32 historyHash, |
|
| 84 |
✓ 2.3M
|
StreamsHistory[] memory history |
|
| 85 |
✓ 2.3M
|
) = getFuzzedStreamsHistory( |
|
| 86 |
✓ 2.3M
|
senderAccId, |
|
| 87 |
✓ 2.3M
|
hashIndex, |
|
| 88 |
✓ 2.3M
|
receiversRandomSeed |
|
| 89 |
); |
||
| 90 | |||
| 91 |
✓ 2.1M
|
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 |
✓ 497.6K
|
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 |
✓ 4.4M
|
squeezeWithDefaultHistory( |
|
| 109 |
✓ 4.4M
|
targetAccId, |
|
| 110 |
✓ 4.4M
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] |
|
| 111 |
); |
||
| 112 |
✓ 4.4M
|
squeezeWithDefaultHistory( |
|
| 113 |
✓ 4.4M
|
targetAccId, |
|
| 114 |
✓ 4.4M
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] |
|
| 115 |
); |
||
| 116 |
✓ 4.4M
|
squeezeWithDefaultHistory( |
|
| 117 |
✓ 4.4M
|
targetAccId, |
|
| 118 |
✓ 4.4M
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] |
|
| 119 |
); |
||
| 120 |
✓ 4.4M
|
squeezeWithDefaultHistory( |
|
| 121 |
✓ 4.4M
|
targetAccId, |
|
| 122 |
✓ 4.4M
|
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 |
✓ 430.5K
|
squeezeAllSenders(targetAccId); |
|
| 135 |
✓ 430.5K
|
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 |
✓ 2.3M
|
) internal returns (bytes32, StreamsHistory[] memory) {
|
|
| 151 |
✓ 2.3M
|
address target = getAccount(targetAccId); |
|
| 152 | |||
| 153 |
// get the history structs and hashes |
||
| 154 |
✓ 2.3M
|
StreamsHistory[] memory historyStructs = getStreamsHistory(target); |
|
| 155 |
✓ 2.3M
|
bytes32[] memory historyHashes = getStreamsHistoryHashes(target); |
|
| 156 | |||
| 157 |
// having a hashed history requires at least 2 history entries |
||
| 158 |
✓ 2.3M
|
require(historyStructs.length >= 2, "need at least 2 history entries"); |
|
| 159 | |||
| 160 |
// hashIndex must be within bounds and cant be the last entry |
||
| 161 |
✓ 2.1M
|
hashIndex = hashIndex % (historyHashes.length - 1); |
|
| 162 | |||
| 163 |
// get the history hash at the index |
||
| 164 |
✓ 2.1M
|
bytes32 historyHash = historyHashes[hashIndex]; |
|
| 165 | |||
| 166 |
// create a history array with all entries after the hashIndex |
||
| 167 |
✓ 2.1M
|
StreamsHistory[] memory history = new StreamsHistory[]( |
|
| 168 |
✓ 2.1M
|
historyStructs.length - 1 - hashIndex |
|
| 169 |
); |
||
| 170 |
✓ 8.3M
|
for (uint256 i = hashIndex + 1; i < historyStructs.length; i++) {
|
|
| 171 |
✓ 6.3M
|
history[i - hashIndex - 1] = historyStructs[i]; |
|
| 172 |
} |
||
| 173 | |||
| 174 |
// hash receivers based on 'receiversRandomSeed' |
||
| 175 |
✓ 8.3M
|
for (uint256 i = 0; i < history.length; i++) {
|
|
| 176 |
✓ 6.3M
|
receiversRandomSeed = keccak256(bytes.concat(receiversRandomSeed)); |
|
| 177 |
✓ 6.3M
|
bool hashBool = (uint256(receiversRandomSeed) % 2) == 0 |
|
| 178 |
✓ 2.9M
|
? false |
|
| 179 |
✓ 3.4M
|
: true; |
|
| 180 | |||
| 181 |
✓ 6.3M
|
if (hashBool) {
|
|
| 182 |
✓ 3.4M
|
history[i].streamsHash = drips.hashStreams( |
|
| 183 |
✓ 3.4M
|
history[i].receivers |
|
| 184 |
); |
||
| 185 |
✓ 3.4M
|
history[i].receivers = new StreamReceiver[](0); |
|
| 186 |
} |
||
| 187 |
} |
||
| 188 | |||
| 189 |
✓ 2.1M
|
return (historyHash, history); |
|
| 190 |
} |
||
| 191 |
} |
||
| 192 |
96.8%
src/echidna/EchidnaSqueezeTests.sol
Lines covered: 92 / 95 (96.8%)
| 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 |
✓ 772.3K
|
address receiver = getAccount(receiverAccId); |
|
| 25 |
✓ 772.3K
|
address sender = getAccount(senderAccId); |
|
| 26 | |||
| 27 |
✓ 772.3K
|
uint256 receiverDripsAccId = getDripsAccountId(receiver); |
|
| 28 | |||
| 29 |
✓ 772.3K
|
uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
|
| 30 |
✓ 772.3K
|
uint128 splittableBefore = drips.splittable(receiverDripsAccId, token); |
|
| 31 | |||
| 32 |
✓ 772.3K
|
uint128 squeezedAmt = squeezeWithDefaultHistory( |
|
| 33 |
✓ 772.3K
|
receiverAccId, |
|
| 34 |
✓ 772.3K
|
senderAccId |
|
| 35 |
); |
||
| 36 | |||
| 37 |
✓ 772.3K
|
uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
|
| 38 |
✓ 772.3K
|
uint128 splittableAfter = drips.splittable(receiverDripsAccId, token); |
|
| 39 | |||
| 40 |
✓ 772.3K
|
assert(squeezableAfter == squeezableBefore - squeezedAmt); |
|
| 41 |
✓ 772.3K
|
assert(splittableAfter == splittableBefore + squeezedAmt); |
|
| 42 | |||
| 43 |
✓ 772.3K
|
if (squeezedAmt > 0) {
|
|
| 44 |
✓ 10.6K
|
assert(squeezableAfter < squeezableBefore); |
|
| 45 |
✓ 10.6K
|
assert(splittableAfter > splittableBefore); |
|
| 46 |
} else {
|
||
| 47 |
✓ 761.7K
|
assert(squeezableAfter == squeezableBefore); |
|
| 48 |
✓ 761.7K
|
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 |
✓ 365.3K
|
address receiver = getAccount(receiverAccId); |
|
| 61 |
✓ 365.3K
|
address sender = getAccount(senderAccId); |
|
| 62 | |||
| 63 |
✓ 365.3K
|
uint128 squeezable = getSqueezableAmount(sender, receiver); |
|
| 64 |
✓ 365.3K
|
uint128 squeezed = squeezeWithDefaultHistory( |
|
| 65 |
✓ 365.3K
|
receiverAccId, |
|
| 66 |
✓ 365.3K
|
senderAccId |
|
| 67 |
); |
||
| 68 | |||
| 69 |
✓ 365.3K
|
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 |
✓ 511.0K
|
address target = getAccount(targetAccId); |
|
| 78 | |||
| 79 |
// store the current squeezable and receivable amount |
||
| 80 |
✓ 511.0K
|
uint128 squeezable = getTotalSqueezableAmountForUser(target); |
|
| 81 |
✓ 511.0K
|
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 |
✓ 511.0K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0]); |
|
| 86 |
✓ 511.0K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1]); |
|
| 87 |
✓ 511.0K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2]); |
|
| 88 |
✓ 511.0K
|
setStreamBalanceWithdrawAll(ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER3]); |
|
| 89 | |||
| 90 |
// warp to the point in time where the streams are receivable |
||
| 91 |
✓ 511.0K
|
hevm.warp(getCurrentCycleEnd() + 1); |
|
| 92 | |||
| 93 |
✓ 511.0K
|
uint128 receivableAfter = getReceivableAmountForUser(target); |
|
| 94 | |||
| 95 |
// sanity check |
||
| 96 |
✓ 511.0K
|
assert(receivableAfter >= receivableBefore); |
|
| 97 | |||
| 98 |
✓ 511.0K
|
uint128 receiveableDelta = receivableAfter - receivableBefore; |
|
| 99 | |||
| 100 |
// squeezable before should match receivable now |
||
| 101 |
✓ 511.0K
|
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 |
✓ 562.0K
|
address receiver = getAccount(receiverAccId); |
|
| 114 |
✓ 562.0K
|
address sender = getAccount(senderAccId); |
|
| 115 | |||
| 116 |
✓ 562.0K
|
uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
|
| 117 | |||
| 118 |
✓ 562.0K
|
StreamsHistory[] memory history = getStreamsHistory(sender); |
|
| 119 |
✓ 2.9M
|
for (uint256 i = 0; i < history.length; i++) {
|
|
| 120 |
✓ 2.4M
|
history[i].streamsHash = drips.hashStreams(history[i].receivers); |
|
| 121 |
✓ 2.4M
|
history[i].receivers = new StreamReceiver[](0); |
|
| 122 |
} |
||
| 123 | |||
| 124 |
✓ 562.0K
|
uint128 squeezedAmt = _squeeze( |
|
| 125 |
✓ 562.0K
|
receiverAccId, |
|
| 126 |
✓ 562.0K
|
senderAccId, |
|
| 127 |
✓ 562.0K
|
bytes32(0), |
|
| 128 |
✓ 562.0K
|
history |
|
| 129 |
); |
||
| 130 | |||
| 131 |
✓ 562.0K
|
uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
|
| 132 | |||
| 133 |
✓ 562.0K
|
assert(squeezedAmt == 0); |
|
| 134 |
✓ 562.0K
|
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 |
✓ 784.6K
|
address receiver = getAccount(receiverAccId); |
|
| 152 |
✓ 784.6K
|
address sender = getAccount(senderAccId); |
|
| 153 | |||
| 154 |
✓ 784.6K
|
uint128 amount0 = squeezeWithFuzzedHistory( |
|
| 155 |
✓ 784.6K
|
receiverAccId, |
|
| 156 |
✓ 784.6K
|
senderAccId, |
|
| 157 |
✓ 784.6K
|
hashIndex, |
|
| 158 |
✓ 784.6K
|
receiversRandomSeed |
|
| 159 |
); |
||
| 160 | |||
| 161 |
✓ 585.6K
|
uint128 amount1 = squeezeWithFuzzedHistory( |
|
| 162 |
✓ 585.6K
|
receiverAccId, |
|
| 163 |
✓ 585.6K
|
senderAccId, |
|
| 164 |
✓ 585.6K
|
hashIndex, |
|
| 165 |
✓ 585.6K
|
receiversRandomSeed |
|
| 166 |
); |
||
| 167 | |||
| 168 |
✓ 585.6K
|
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 |
✓ 421.7K
|
address receiver = getAccount(receiverAccId); |
|
| 190 |
✓ 421.7K
|
address sender = getAccount(senderAccId); |
|
| 191 | |||
| 192 |
✓ 421.7K
|
uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
|
| 193 | |||
| 194 |
✓ 421.7K
|
setStreams( |
|
| 195 |
✓ 421.7K
|
receiverAccId, |
|
| 196 |
✓ 421.7K
|
senderAccId, |
|
| 197 |
✓ 421.7K
|
amountPerSec, |
|
| 198 |
✓ 421.7K
|
startTime, |
|
| 199 |
✓ 421.7K
|
duration, |
|
| 200 |
✓ 421.7K
|
balanceDelta |
|
| 201 |
); |
||
| 202 | |||
| 203 |
✓ 238.0K
|
uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
|
| 204 | |||
| 205 |
✓ 238.0K
|
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 |
✓ 570.7K
|
address receiver = getAccount(receiverAccId); |
|
| 219 |
✓ 570.7K
|
address sender = getAccount(senderAccId); |
|
| 220 | |||
| 221 |
✓ 570.7K
|
uint128 squeezableBefore = getSqueezableAmount(sender, receiver); |
|
| 222 | |||
| 223 |
✓ 570.7K
|
setStreamBalanceWithdrawAll(senderAccId); |
|
| 224 | |||
| 225 |
✓ 570.7K
|
uint128 squeezableAfter = getSqueezableAmount(sender, receiver); |
|
| 226 | |||
| 227 |
✓ 570.7K
|
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 |
✓ 366.3K
|
EchidnaSqueezeHelpers(address(this)).squeezeWithDefaultHistory( |
|
| 242 |
✓ 366.3K
|
receiverAccId, |
|
| 243 |
✓ 366.3K
|
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 |
✓ 641.3K
|
address sender = getAccount(senderAccId); |
|
| 265 |
require( |
||
| 266 |
✓ 641.3K
|
getStreamsHistory(sender).length >= 2, |
|
| 267 |
"need at least 2 history entries" |
||
| 268 |
); |
||
| 269 | |||
| 270 |
try |
||
| 271 |
✓ 451.4K
|
EchidnaSqueezeHelpers(address(this)).squeezeWithFuzzedHistory( |
|
| 272 |
✓ 451.4K
|
receiverAccId, |
|
| 273 |
✓ 451.4K
|
senderAccId, |
|
| 274 |
✓ 451.4K
|
hashIndex, |
|
| 275 |
✓ 451.4K
|
receiversRandomSeed |
|
| 276 |
) |
||
| 277 |
{} catch {
|
||
| 278 |
assert(false); |
||
| 279 |
} |
||
| 280 |
} |
||
| 281 | |||
| 282 |
} |
||
| 283 |
99.3%
src/echidna/EchidnaStreamsHelpers.sol
Lines covered: 143 / 144 (99.3%)
| 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 |
✓ 14.1M
|
) internal returns (int128) {
|
|
| 36 |
✓ 14.1M
|
StreamReceiver[] memory newReceivers = bubbleSortStreamReceivers( |
|
| 37 |
✓ 14.1M
|
unsortedNewReceivers |
|
| 38 |
); |
||
| 39 | |||
| 40 |
✓ 14.1M
|
hevm.prank(from); |
|
| 41 |
✓ 14.1M
|
int128 realBalanceDelta = driver.setStreams( |
|
| 42 |
✓ 14.1M
|
token, |
|
| 43 |
✓ 14.1M
|
currReceivers, |
|
| 44 |
✓ 14.1M
|
balanceDelta, |
|
| 45 |
✓ 14.1M
|
newReceivers, |
|
| 46 |
✓ 14.1M
|
maxEndHint1, |
|
| 47 |
✓ 14.1M
|
maxEndHint2, |
|
| 48 |
✓ 14.1M
|
from |
|
| 49 |
); |
||
| 50 | |||
| 51 |
✓ 13.0M
|
updateStreamReceivers(from, newReceivers); |
|
| 52 | |||
| 53 |
✓ 13.0M
|
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 |
✓ 2.2M
|
) public returns (int128) {
|
|
| 74 |
✓ 2.2M
|
address from = getAccount(fromAccId); |
|
| 75 |
✓ 2.2M
|
address to = getAccount(toAccId); |
|
| 76 | |||
| 77 |
✓ 2.2M
|
StreamReceiver[] memory receivers = new StreamReceiver[](1); |
|
| 78 |
✓ 2.2M
|
receivers[0] = StreamReceiver( |
|
| 79 |
✓ 2.2M
|
getDripsAccountId(to), |
|
| 80 |
✓ 2.2M
|
StreamConfigImpl.create( |
|
| 81 |
✓ 2.2M
|
0, // streamId is arbitrary and can be ignored |
|
| 82 |
✓ 2.2M
|
amountPerSec, |
|
| 83 |
✓ 2.2M
|
startTime, |
|
| 84 |
✓ 2.2M
|
duration |
|
| 85 |
) |
||
| 86 |
); |
||
| 87 | |||
| 88 |
✓ 2.2M
|
int128 realBalanceDelta = _setStreams( |
|
| 89 |
✓ 2.2M
|
from, |
|
| 90 |
✓ 2.2M
|
getStreamReceivers(from), |
|
| 91 |
✓ 2.2M
|
balanceDelta, |
|
| 92 |
✓ 2.2M
|
receivers |
|
| 93 |
); |
||
| 94 | |||
| 95 |
✓ 1.7M
|
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 |
✓ 1.3M
|
) public returns (int128) {
|
|
| 118 |
✓ 1.3M
|
address from = getAccount(fromAccId); |
|
| 119 |
✓ 1.3M
|
address to = getAccount(toAccId); |
|
| 120 | |||
| 121 |
✓ 1.3M
|
amountPerSec = clampAmountPerSec(amountPerSec); |
|
| 122 |
✓ 1.3M
|
startTime = clampStartTime(startTime); |
|
| 123 |
✓ 1.3M
|
duration = clampDuration(duration); |
|
| 124 |
✓ 1.3M
|
balanceDelta = clampBalanceDelta(balanceDelta, from); |
|
| 125 | |||
| 126 |
✓ 1.3M
|
setStreams( |
|
| 127 |
✓ 1.3M
|
fromAccId, |
|
| 128 |
✓ 1.3M
|
toAccId, |
|
| 129 |
✓ 1.3M
|
amountPerSec, |
|
| 130 |
✓ 1.3M
|
startTime, |
|
| 131 |
✓ 1.3M
|
duration, |
|
| 132 |
✓ 1.3M
|
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 |
✓ 2.5M
|
) public returns (int128) {
|
|
| 154 |
✓ 2.5M
|
address from = getAccount(fromAccId); |
|
| 155 |
✓ 2.5M
|
address to = getAccount(toAccId); |
|
| 156 | |||
| 157 |
✓ 2.5M
|
StreamReceiver[] memory oldReceivers = getStreamReceivers(from); |
|
| 158 | |||
| 159 |
✓ 2.5M
|
StreamReceiver memory addedReceiver = StreamReceiver( |
|
| 160 |
✓ 2.5M
|
getDripsAccountId(to), |
|
| 161 |
✓ 2.5M
|
StreamConfigImpl.create( |
|
| 162 |
✓ 2.5M
|
0, // streamId is arbitrary and can be ignored |
|
| 163 |
✓ 2.5M
|
amountPerSec, |
|
| 164 |
✓ 2.5M
|
startTime, |
|
| 165 |
✓ 2.5M
|
duration |
|
| 166 |
) |
||
| 167 |
); |
||
| 168 | |||
| 169 |
✓ 2.5M
|
StreamReceiver[] memory newReceivers = new StreamReceiver[]( |
|
| 170 |
✓ 2.5M
|
oldReceivers.length + 1 |
|
| 171 |
); |
||
| 172 |
✓ 5.4M
|
for (uint256 i = 0; i < oldReceivers.length; i++) {
|
|
| 173 |
✓ 2.9M
|
newReceivers[i] = oldReceivers[i]; |
|
| 174 |
} |
||
| 175 |
✓ 2.5M
|
newReceivers[newReceivers.length - 1] = addedReceiver; |
|
| 176 | |||
| 177 |
✓ 2.5M
|
int128 realBalanceDelta = _setStreams( |
|
| 178 |
✓ 2.5M
|
from, |
|
| 179 |
✓ 2.5M
|
oldReceivers, |
|
| 180 |
✓ 2.5M
|
balanceDelta, |
|
| 181 |
✓ 2.5M
|
newReceivers |
|
| 182 |
); |
||
| 183 | |||
| 184 |
✓ 2.2M
|
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 |
✓ 1.4M
|
) public returns (int128) {
|
|
| 206 |
✓ 1.4M
|
address from = getAccount(fromAccId); |
|
| 207 | |||
| 208 |
✓ 1.4M
|
amountPerSec = clampAmountPerSec(amountPerSec); |
|
| 209 |
✓ 1.4M
|
startTime = clampStartTime(startTime); |
|
| 210 |
✓ 1.4M
|
duration = clampDuration(duration); |
|
| 211 |
✓ 1.4M
|
balanceDelta = clampBalanceDelta(balanceDelta, from); |
|
| 212 | |||
| 213 |
return |
||
| 214 |
✓ 1.4M
|
addStream( |
|
| 215 |
✓ 1.4M
|
fromAccId, |
|
| 216 |
✓ 1.4M
|
toAccId, |
|
| 217 |
✓ 1.4M
|
amountPerSec, |
|
| 218 |
✓ 1.4M
|
startTime, |
|
| 219 |
✓ 1.4M
|
duration, |
|
| 220 |
✓ 1.4M
|
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 |
✓ 380.6K
|
address receiver = getAccount(toAccId); |
|
| 239 |
✓ 380.6K
|
address sender = getAccount(fromAccId); |
|
| 240 | |||
| 241 |
// calculate amount per second so there will be something to squeeze |
||
| 242 |
// this cycle |
||
| 243 |
✓ 380.6K
|
uint160 minAmtPerSec = drips.minAmtPerSec() * SECONDS_PER_CYCLE; |
|
| 244 |
amountPerSec = |
||
| 245 |
✓ 380.6K
|
minAmtPerSec + |
|
| 246 |
✓ 380.6K
|
(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 |
✓ 380.6K
|
int128 balanceDelta = (int128(uint128(amountPerSec)) * 100) / 1e9; |
|
| 251 |
✓ 380.6K
|
if (uint128(balanceDelta) > token.balanceOf(sender)) {
|
|
| 252 |
✓ 222.7K
|
balanceDelta = int128(uint128(token.balanceOf(sender))); |
|
| 253 |
} |
||
| 254 | |||
| 255 |
// add the stream |
||
| 256 |
✓ 380.6K
|
addStream(fromAccId, toAccId, amountPerSec, 0, 0, balanceDelta); |
|
| 257 | |||
| 258 |
// warp 1 second forward so there is something to squeeze |
||
| 259 |
✓ 380.6K
|
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 |
✓ 668.4K
|
address target = getAccount(targetAccId); |
|
| 269 | |||
| 270 |
✓ 668.4K
|
StreamReceiver[] memory oldReceivers = getStreamReceivers(target); |
|
| 271 | |||
| 272 |
✓ 668.4K
|
uint256 index = indexSeed % oldReceivers.length; |
|
| 273 | |||
| 274 |
✓ 530.1K
|
StreamReceiver[] memory newReceivers = new StreamReceiver[]( |
|
| 275 |
✓ 530.1K
|
oldReceivers.length - 1 |
|
| 276 |
); |
||
| 277 |
✓ 530.1K
|
uint256 j = 0; |
|
| 278 |
✓ 1.3M
|
for (uint256 i = 0; i < oldReceivers.length; i++) {
|
|
| 279 |
✓ 799.7K
|
if (i != index) {
|
|
| 280 |
✓ 269.6K
|
newReceivers[j] = oldReceivers[i]; |
|
| 281 |
✓ 269.6K
|
j++; |
|
| 282 |
} |
||
| 283 |
} |
||
| 284 | |||
| 285 |
✓ 530.1K
|
_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 |
✓ 1.7M
|
returns (int128) |
|
| 298 |
{
|
||
| 299 |
✓ 1.7M
|
address target = getAccount(targetAccId); |
|
| 300 | |||
| 301 |
✓ 1.7M
|
int128 realBalanceDelta = _setStreams( |
|
| 302 |
✓ 1.7M
|
target, |
|
| 303 |
✓ 1.7M
|
getStreamReceivers(target), |
|
| 304 |
✓ 1.7M
|
balanceDelta, |
|
| 305 |
✓ 1.7M
|
getStreamReceivers(target) |
|
| 306 |
); |
||
| 307 | |||
| 308 |
✓ 1.5M
|
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 |
✓ 810.9K
|
address target = getAccount(targetAccId); |
|
| 324 |
✓ 810.9K
|
balanceDelta = clampBalanceDelta(balanceDelta, target); |
|
| 325 |
✓ 810.9K
|
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 |
✓ 7.1M
|
returns (int128) |
|
| 337 |
{
|
||
| 338 |
✓ 7.1M
|
address target = getAccount(targetAccId); |
|
| 339 |
✓ 7.1M
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 340 | |||
| 341 |
✓ 7.1M
|
int128 realBalanceDelta = _setStreams( |
|
| 342 |
✓ 7.1M
|
target, |
|
| 343 |
✓ 7.1M
|
getStreamReceivers(target), |
|
| 344 |
✓ 7.1M
|
type(int128).min, |
|
| 345 |
✓ 7.1M
|
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 |
✓ 401.0K
|
require(TOGGLE_MAXENDHINTS_ENABLED); |
|
| 357 |
✓ 401.0K
|
maxEndHint1 = _maxEndHint1; |
|
| 358 |
✓ 401.0K
|
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 |
✓ 3.3M
|
returns (uint160) |
|
| 369 |
{
|
||
| 370 |
return |
||
| 371 |
✓ 3.3M
|
drips.minAmtPerSec() + |
|
| 372 |
✓ 3.3M
|
(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 |
✓ 2.6M
|
function clampStartTime(uint32 startTime) internal returns (uint32) {
|
|
| 381 |
✓ 2.6M
|
if (startTime == 0) return 0; |
|
| 382 | |||
| 383 |
// We want to make sure that the start time does not go below 1 |
||
| 384 |
✓ 2.3M
|
uint32 minStartTime; |
|
| 385 |
✓ 2.3M
|
if (CYCLE_FUZZING_BUFFER_SECONDS >= block.timestamp) {
|
|
| 386 |
✓ 99.1K
|
minStartTime = 1; |
|
| 387 |
} else {
|
||
| 388 |
minStartTime = |
||
| 389 |
✓ 2.2M
|
uint32(block.timestamp) - |
|
| 390 |
CYCLE_FUZZING_BUFFER_SECONDS; |
||
| 391 |
} |
||
| 392 | |||
| 393 |
✓ 2.3M
|
uint32 maxStartTime = uint32(block.timestamp) + |
|
| 394 |
CYCLE_FUZZING_BUFFER_SECONDS; |
||
| 395 | |||
| 396 |
✓ 2.3M
|
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 |
✓ 2.6M
|
function clampDuration(uint32 duration) internal returns (uint32) {
|
|
| 405 |
✓ 2.6M
|
if (duration == 0) return 0; |
|
| 406 | |||
| 407 |
✓ 2.5M
|
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 |
✓ 4.1M
|
returns (int128) |
|
| 419 |
{
|
||
| 420 |
✓ 4.1M
|
if (balanceDelta > 0) {
|
|
| 421 |
balanceDelta = |
||
| 422 |
✓ 2.3M
|
balanceDelta % |
|
| 423 |
✓ 2.3M
|
(int128(uint128(token.balanceOf(from))) + 1); |
|
| 424 |
} else {
|
||
| 425 |
✓ 1.9M
|
balanceDelta = balanceDelta % int128(uint128(STARTING_BALANCE)); |
|
| 426 |
} |
||
| 427 |
✓ 4.1M
|
return balanceDelta; |
|
| 428 |
} |
||
| 429 |
} |
||
| 430 |
94.0%
src/echidna/EchidnaStreamsTests.sol
Lines covered: 94 / 100 (94.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 |
✓ 546.6K
|
address target = getAccount(targetAccId); |
|
| 20 |
✓ 546.6K
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 21 | |||
| 22 |
✓ 546.6K
|
uint256 tokenBalanceBefore = token.balanceOf(target); |
|
| 23 |
✓ 546.6K
|
uint128 streamBalanceBefore = drips.balanceAt( |
|
| 24 |
✓ 546.6K
|
targetDripsAccId, |
|
| 25 |
✓ 546.6K
|
token, |
|
| 26 |
✓ 546.6K
|
getStreamReceivers(target), |
|
| 27 |
✓ 546.6K
|
uint32(block.timestamp) |
|
| 28 |
); |
||
| 29 | |||
| 30 |
✓ 546.6K
|
int128 realBalanceDelta = setStreamBalance(targetAccId, balanceDelta); |
|
| 31 | |||
| 32 |
✓ 420.3K
|
uint256 tokenBalanceAfter = token.balanceOf(target); |
|
| 33 |
✓ 420.3K
|
uint128 streamBalanceAfter = drips.balanceAt( |
|
| 34 |
✓ 420.3K
|
targetDripsAccId, |
|
| 35 |
✓ 420.3K
|
token, |
|
| 36 |
✓ 420.3K
|
getStreamReceivers(target), |
|
| 37 |
✓ 420.3K
|
uint32(block.timestamp) |
|
| 38 |
); |
||
| 39 | |||
| 40 |
✓ 420.3K
|
if (balanceDelta >= 0) {
|
|
| 41 |
✓ 98.2K
|
assert(realBalanceDelta == balanceDelta); |
|
| 42 |
} else {
|
||
| 43 |
✓ 322.2K
|
assert(realBalanceDelta <= 0); |
|
| 44 |
✓ 322.2K
|
assert(realBalanceDelta >= balanceDelta); |
|
| 45 |
} |
||
| 46 | |||
| 47 |
assert( |
||
| 48 |
✓ 420.3K
|
int256(tokenBalanceAfter) == |
|
| 49 |
✓ 420.3K
|
int256(tokenBalanceBefore) - realBalanceDelta |
|
| 50 |
); |
||
| 51 |
assert( |
||
| 52 |
✓ 420.3K
|
int128(streamBalanceAfter) == |
|
| 53 |
✓ 420.3K
|
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 |
✓ 686.7K
|
address from = getAccount(fromAccId); |
|
| 72 |
✓ 686.7K
|
address to = getAccount(toAccId); |
|
| 73 |
✓ 686.7K
|
uint256 fromDripsAccId = getDripsAccountId(from); |
|
| 74 |
✓ 686.7K
|
uint256 toDripsAccId = getDripsAccountId(to); |
|
| 75 | |||
| 76 |
✓ 686.7K
|
amtPerSecAdded = clampAmountPerSec(amtPerSecAdded); |
|
| 77 | |||
| 78 |
// the timestamps we are comparing |
||
| 79 |
✓ 686.7K
|
uint256 currentTimestamp = block.timestamp; |
|
| 80 |
✓ 686.7K
|
uint256 futureTimestamp = getCurrentCycleEnd() + 1; |
|
| 81 | |||
| 82 |
// retrieve initial balances |
||
| 83 |
✓ 686.7K
|
uint128 balanceInitial = getStreamBalanceForUser( |
|
| 84 |
✓ 686.7K
|
from, |
|
| 85 |
✓ 686.7K
|
uint32(block.timestamp) |
|
| 86 |
); |
||
| 87 |
✓ 686.7K
|
uint128 receivableInitial = getReceivableAmountForAllUsers(); |
|
| 88 | |||
| 89 |
// look at balances in the future if we wouldnt do anything |
||
| 90 |
✓ 686.7K
|
hevm.warp(futureTimestamp); |
|
| 91 |
✓ 686.7K
|
uint128 balanceBaseline = getStreamBalanceForUser( |
|
| 92 |
✓ 686.7K
|
from, |
|
| 93 |
✓ 686.7K
|
uint32(block.timestamp) |
|
| 94 |
); |
||
| 95 |
✓ 686.7K
|
uint128 receivableBaseline = getReceivableAmountForAllUsers(); |
|
| 96 |
✓ 686.7K
|
hevm.warp(currentTimestamp); |
|
| 97 | |||
| 98 |
// add a stream |
||
| 99 |
// make sure we add enough balance to complete the cycle |
||
| 100 |
✓ 686.7K
|
uint128 balanceAdded = uint128(amtPerSecAdded) * SECONDS_PER_CYCLE; |
|
| 101 |
✓ 686.7K
|
balanceAdded = uint128(clampBalanceDelta(int128(balanceAdded), from)); |
|
| 102 |
✓ 686.7K
|
require(balanceAdded / amtPerSecAdded >= SECONDS_PER_CYCLE); |
|
| 103 |
✓ 268.8K
|
addStream( |
|
| 104 |
✓ 268.8K
|
fromAccId, |
|
| 105 |
✓ 268.8K
|
toAccId, |
|
| 106 |
✓ 268.8K
|
amtPerSecAdded, |
|
| 107 |
✓ 268.8K
|
0, |
|
| 108 |
✓ 268.8K
|
0, |
|
| 109 |
✓ 268.8K
|
int128(balanceAdded) |
|
| 110 |
); |
||
| 111 | |||
| 112 |
// retrieve balances after adding stream |
||
| 113 |
✓ 268.7K
|
uint128 balanceBefore = getStreamBalanceForUser( |
|
| 114 |
✓ 268.7K
|
from, |
|
| 115 |
✓ 268.7K
|
uint32(block.timestamp) |
|
| 116 |
); |
||
| 117 |
✓ 268.7K
|
uint128 receivableBefore = getReceivableAmountForAllUsers(); |
|
| 118 | |||
| 119 |
// jump to future |
||
| 120 |
✓ 268.7K
|
hevm.warp(futureTimestamp); |
|
| 121 | |||
| 122 |
// retrieve balances in the future after adding the stream |
||
| 123 |
✓ 268.7K
|
uint128 balanceAfter = getStreamBalanceForUser( |
|
| 124 |
✓ 268.7K
|
from, |
|
| 125 |
✓ 268.7K
|
uint32(block.timestamp) |
|
| 126 |
); |
||
| 127 |
✓ 268.7K
|
uint128 receivableAfter = getReceivableAmountForAllUsers(); |
|
| 128 | |||
| 129 |
// sanity checks |
||
| 130 |
✓ 268.7K
|
assert(balanceInitial >= balanceBaseline); |
|
| 131 |
✓ 268.7K
|
assert(balanceBefore >= balanceAfter); |
|
| 132 |
✓ 268.7K
|
assert(receivableAfter >= receivableBaseline); |
|
| 133 | |||
| 134 |
// the amount that would have been streamed if we do nothing |
||
| 135 |
✓ 151.6K
|
uint128 baselineBalanceStreamed = balanceInitial - balanceBaseline; |
|
| 136 | |||
| 137 |
// calculate expected balance change including the effect of the |
||
| 138 |
// added stream |
||
| 139 |
✓ 151.6K
|
uint128 expectedBalanceChange = balanceBefore - |
|
| 140 |
✓ 151.6K
|
balanceAfter - |
|
| 141 |
✓ 151.6K
|
baselineBalanceStreamed; |
|
| 142 |
✓ 151.6K
|
uint128 expectedReceivedChange = receivableAfter - receivableBaseline; |
|
| 143 | |||
| 144 |
✓ 151.6K
|
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 |
✓ 706.1K
|
EchidnaStreamsHelpers(address(this)).setStreamsWithClamping( |
|
| 166 |
✓ 706.1K
|
fromAccId, |
|
| 167 |
✓ 706.1K
|
toAccId, |
|
| 168 |
✓ 706.1K
|
amountPerSec, |
|
| 169 |
✓ 706.1K
|
startTime, |
|
| 170 |
✓ 706.1K
|
duration, |
|
| 171 |
✓ 706.1K
|
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 |
✓ 553.2K
|
EchidnaStreamsHelpers(address(this)).addStreamWithClamping( |
|
| 197 |
✓ 553.2K
|
fromAccId, |
|
| 198 |
✓ 553.2K
|
toAccId, |
|
| 199 |
✓ 553.2K
|
amountPerSec, |
|
| 200 |
✓ 553.2K
|
startTime, |
|
| 201 |
✓ 553.2K
|
duration, |
|
| 202 |
✓ 553.2K
|
balanceDelta |
|
| 203 |
) |
||
| 204 |
{} catch (bytes memory reason) {
|
||
| 205 |
✓ 1
|
bytes4 errorSelector = bytes4(reason); |
|
| 206 |
✓ 1
|
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 |
✓ 430.4K
|
address target = getAccount(targetAccId); |
|
| 224 |
✓ 430.4K
|
require(getStreamReceivers(target).length > 0); |
|
| 225 | |||
| 226 |
try |
||
| 227 |
✓ 272.4K
|
EchidnaStreamsHelpers(address(this)).removeStream( |
|
| 228 |
✓ 272.4K
|
targetAccId, |
|
| 229 |
✓ 272.4K
|
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 |
✓ 387.1K
|
EchidnaStreamsHelpers(address(this)).setStreamBalanceWithClamping( |
|
| 247 |
✓ 387.1K
|
targetAccId, |
|
| 248 |
✓ 387.1K
|
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 |
✓ 556.7K
|
EchidnaStreamsHelpers(address(this)).setStreamBalanceWithdrawAll( |
|
| 264 |
✓ 556.7K
|
targetAccId |
|
| 265 |
) |
||
| 266 |
{} catch {
|
||
| 267 |
assert(false); |
||
| 268 |
} |
||
| 269 |
} |
||
| 270 |
} |
||
| 271 |
97.5%
src/echidna/base/EchidnaAccounting.sol
Lines covered: 78 / 80 (97.5%)
| 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 |
✓ 394.1K
|
function getDripsBalancesTotalForAllUsers() internal returns (uint256) {
|
|
| 18 |
✓ 394.1K
|
uint256 user0Total = getDripsBalancesTotalForUser(ADDRESS_USER0); |
|
| 19 |
✓ 394.1K
|
uint256 user1Total = getDripsBalancesTotalForUser(ADDRESS_USER1); |
|
| 20 |
✓ 394.1K
|
uint256 user2Total = getDripsBalancesTotalForUser(ADDRESS_USER2); |
|
| 21 |
✓ 394.1K
|
uint256 user3Total = getDripsBalancesTotalForUser(ADDRESS_USER3); |
|
| 22 | |||
| 23 |
✓ 394.1K
|
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 |
✓ 1.6M
|
returns (uint256) |
|
| 37 |
{
|
||
| 38 |
✓ 1.6M
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 39 | |||
| 40 |
✓ 1.6M
|
uint128 balance = getCurrentStreamBalanceForUser(target); |
|
| 41 |
✓ 1.6M
|
uint128 squeezable = getTotalSqueezableAmountForUser(target); |
|
| 42 |
✓ 1.6M
|
uint128 receivable = getReceivableAmountForUser(target); |
|
| 43 |
✓ 1.6M
|
uint128 collectable = drips.collectable(targetDripsAccId, token); |
|
| 44 |
✓ 1.6M
|
uint128 splittable = drips.splittable(targetDripsAccId, token); |
|
| 45 | |||
| 46 |
✓ 1.6M
|
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 |
✓ 3.5M
|
returns (uint128) |
|
| 60 |
{
|
||
| 61 |
✓ 3.5M
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 62 | |||
| 63 |
✓ 3.5M
|
uint128 balance; |
|
| 64 |
try |
||
| 65 |
✓ 3.5M
|
drips.balanceAt( |
|
| 66 |
✓ 3.5M
|
targetDripsAccId, |
|
| 67 |
✓ 3.5M
|
token, |
|
| 68 |
✓ 3.5M
|
getStreamReceivers(target), |
|
| 69 |
✓ 3.5M
|
timestamp |
|
| 70 |
) |
||
| 71 |
returns (uint128 _balance) {
|
||
| 72 |
✓ 3.5M
|
balance = _balance; |
|
| 73 |
} catch {
|
||
| 74 |
// this should not happen, so put an assert here to be sure |
||
| 75 |
assert(false); |
||
| 76 |
} |
||
| 77 | |||
| 78 |
✓ 3.5M
|
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 |
✓ 1.6M
|
returns (uint128) |
|
| 89 |
{
|
||
| 90 |
✓ 1.6M
|
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 |
✓ 1.9M
|
function getReceivableAmountForAllUsers() internal returns (uint128) {
|
|
| 100 |
✓ 1.9M
|
uint128 receivable; |
|
| 101 |
✓ 1.9M
|
receivable += getReceivableAmountForUser(ADDRESS_USER0); |
|
| 102 |
✓ 1.9M
|
receivable += getReceivableAmountForUser(ADDRESS_USER1); |
|
| 103 |
✓ 1.9M
|
receivable += getReceivableAmountForUser(ADDRESS_USER2); |
|
| 104 |
✓ 1.9M
|
receivable += getReceivableAmountForUser(ADDRESS_USER3); |
|
| 105 |
✓ 1.9M
|
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 |
✓ 10.2M
|
returns (uint128) |
|
| 118 |
{
|
||
| 119 |
✓ 10.2M
|
uint128 receivable = drips.receiveStreamsResult( |
|
| 120 |
✓ 10.2M
|
getDripsAccountId(target), |
|
| 121 |
✓ 10.2M
|
token, |
|
| 122 |
✓ 10.2M
|
type(uint32).max |
|
| 123 |
); |
||
| 124 |
✓ 10.2M
|
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 |
✓ 2.1M
|
returns (uint128) |
|
| 138 |
{
|
||
| 139 |
✓ 2.1M
|
uint128 amount = 0; |
|
| 140 |
✓ 2.1M
|
amount += getSqueezableAmount(ADDRESS_USER0, target); |
|
| 141 |
✓ 2.1M
|
amount += getSqueezableAmount(ADDRESS_USER1, target); |
|
| 142 |
✓ 2.1M
|
amount += getSqueezableAmount(ADDRESS_USER2, target); |
|
| 143 |
✓ 2.1M
|
amount += getSqueezableAmount(ADDRESS_USER3, target); |
|
| 144 | |||
| 145 |
✓ 2.1M
|
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 |
✓ 13.2M
|
returns (uint128) |
|
| 160 |
{
|
||
| 161 |
✓ 13.2M
|
uint256 senderDripsAccId = getDripsAccountId(sender); |
|
| 162 |
✓ 13.2M
|
uint256 receiverDripsAccId = getDripsAccountId(receiver); |
|
| 163 | |||
| 164 |
✓ 13.2M
|
uint128 amount = drips.squeezeStreamsResult( |
|
| 165 |
✓ 13.2M
|
receiverDripsAccId, |
|
| 166 |
✓ 13.2M
|
token, |
|
| 167 |
✓ 13.2M
|
senderDripsAccId, |
|
| 168 |
✓ 13.2M
|
bytes32(0), |
|
| 169 |
✓ 13.2M
|
getStreamsHistory(sender) |
|
| 170 |
); |
||
| 171 | |||
| 172 |
✓ 13.2M
|
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 |
✓ 382.7K
|
function getMaxEndForAllUsers() internal returns (uint32) {
|
|
| 182 |
✓ 382.7K
|
uint32 maxMaxEnd; |
|
| 183 | |||
| 184 |
✓ 382.7K
|
uint32 maxEndUser0 = getMaxEndForUser(ADDRESS_USER0); |
|
| 185 |
✓ 382.7K
|
if (maxEndUser0 > maxMaxEnd) maxMaxEnd = maxEndUser0; |
|
| 186 |
✓ 382.7K
|
uint32 maxEndUser1 = getMaxEndForUser(ADDRESS_USER1); |
|
| 187 |
✓ 382.7K
|
if (maxEndUser1 > maxMaxEnd) maxMaxEnd = maxEndUser1; |
|
| 188 |
✓ 382.7K
|
uint32 maxEndUser2 = getMaxEndForUser(ADDRESS_USER2); |
|
| 189 |
✓ 382.7K
|
if (maxEndUser2 > maxMaxEnd) maxMaxEnd = maxEndUser2; |
|
| 190 |
✓ 382.7K
|
uint32 maxEndUser3 = getMaxEndForUser(ADDRESS_USER3); |
|
| 191 |
✓ 382.7K
|
if (maxEndUser3 > maxMaxEnd) maxMaxEnd = maxEndUser3; |
|
| 192 | |||
| 193 |
✓ 382.7K
|
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 |
✓ 1.5M
|
function getMaxEndForUser(address target) internal returns (uint32) {
|
|
| 202 |
✓ 1.5M
|
uint256 targetDripsAccId = getDripsAccountId(target); |
|
| 203 |
✓ 1.5M
|
(, , , , uint32 maxEnd) = drips.streamsState(targetDripsAccId, token); |
|
| 204 |
✓ 1.5M
|
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 |
✓ 1.2M
|
function getCurrentCycleStart() internal returns (uint32) {
|
|
| 212 |
✓ 1.2M
|
uint32 currTimestamp = uint32(block.timestamp); |
|
| 213 |
✓ 1.2M
|
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 |
✓ 1.2M
|
function getCurrentCycleEnd() internal returns (uint32) {
|
|
| 221 |
✓ 1.2M
|
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 |
✓ 765.3K
|
returns (uint32) |
|
| 232 |
{
|
||
| 233 |
✓ 765.3K
|
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 |
92.0%
src/echidna/base/EchidnaConfig.sol
Lines covered: 23 / 25 (92.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 |
✓ 51.4M
|
address internal constant ADDRESS_USER0 = address(0x10000); |
|
| 20 |
✓ 33.9M
|
address internal constant ADDRESS_USER1 = address(0x20000); |
|
| 21 |
✓ 32.1M
|
address internal constant ADDRESS_USER2 = address(0x30000); |
|
| 22 |
✓ 40.8M
|
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 |
✓ 3.3M
|
uint256 internal constant STARTING_BALANCE = 1_000_000_000e18; |
|
| 33 | |||
| 34 |
// Amount of seconds in a Drips cycle |
||
| 35 |
✓ 3.3M
|
uint32 internal constant SECONDS_PER_CYCLE = 10; |
|
| 36 | |||
| 37 |
// Buffers to be used as fuzzing boundaries |
||
| 38 |
✓ 2.5M
|
uint32 internal constant CYCLE_FUZZING_BUFFER_CYCLES = 10; |
|
| 39 |
uint32 internal constant CYCLE_FUZZING_BUFFER_SECONDS = |
||
| 40 |
✓ 2.5M
|
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 |
✓ 3.3M
|
(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 |
✓ 119.4K
|
uint256 internal constant SPLIT_ROUNDING_TOLERANCE = 1; |
|
| 54 | |||
| 55 |
// Toggles for certain tests |
||
| 56 |
bool internal constant TOGGLE_EXPERIMENTAL_TESTS_ENABLED = true; |
||
| 57 |
✓ 882.6K
|
bool internal constant TOGGLE_HEAVY_TESTS_ENABLED = true; |
|
| 58 |
✓ 401.0K
|
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 |
✓ 882.6K
|
if (!TOGGLE_HEAVY_TESTS_ENABLED) return; |
|
| 69 |
_; |
||
| 70 |
} |
||
| 71 | |||
| 72 |
constructor() {
|
||
| 73 |
✓ 1
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER0] = 0; |
|
| 74 |
✓ 1
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER1] = 64; |
|
| 75 |
✓ 1
|
ADDRESS_TO_ACCOUNT_ID[ADDRESS_USER2] = 128; |
|
| 76 |
✓ 1
|
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 |
✓ 158.1M
|
function getAccount(uint8 rawId) internal pure returns (address) {
|
|
| 86 |
✓ 158.1M
|
uint256 id = uint256(rawId) / 64; |
|
| 87 | |||
| 88 |
✓ 158.1M
|
if (id == 0) return ADDRESS_USER0; |
|
| 89 |
✓ 106.7M
|
if (id == 1) return ADDRESS_USER1; |
|
| 90 |
✓ 72.8M
|
if (id == 2) return ADDRESS_USER2; |
|
| 91 |
✓ 40.8M
|
if (id == 3) return ADDRESS_USER3; |
|
| 92 | |||
| 93 |
require(false, "Unknown account ID"); |
||
| 94 |
} |
||
| 95 |
} |
||
| 96 |
96.3%
src/echidna/base/EchidnaSetup.sol
Lines covered: 26 / 27 (96.3%)
| 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 |
✓ 1
|
IHevm hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); |
|
| 12 | |||
| 13 |
ERC20PresetFixedSupply token; |
||
| 14 |
DripsEchidna drips; |
||
| 15 |
AddressDriverEchidna driver; |
||
| 16 | |||
| 17 |
constructor() EchidnaConfig() {
|
||
| 18 |
// Deploy ERC20 token |
||
| 19 |
✓ 1
|
token = new ERC20PresetFixedSupply( |
|
| 20 |
"Test Token", |
||
| 21 |
"TEST", |
||
| 22 |
✓ 1
|
STARTING_BALANCE * 4, |
|
| 23 |
✓ 1
|
address(this) |
|
| 24 |
); |
||
| 25 | |||
| 26 |
// Deploy Drips |
||
| 27 |
✓ 1
|
drips = new DripsEchidna(SECONDS_PER_CYCLE); |
|
| 28 |
✓ 1
|
drips.unpause_noModifiers(); |
|
| 29 | |||
| 30 |
// Deploy AddressDriver |
||
| 31 |
✓ 1
|
uint32 driverId = drips.registerDriver(address(this)); |
|
| 32 |
✓ 1
|
driver = new AddressDriverEchidna( |
|
| 33 |
✓ 1
|
drips, |
|
| 34 |
✓ 1
|
address(0), |
|
| 35 |
✓ 1
|
driverId |
|
| 36 |
); |
||
| 37 |
✓ 1
|
driver.unpause_noModifiers(); |
|
| 38 |
✓ 1
|
drips.updateDriverAddress(driverId, address(driver)); |
|
| 39 | |||
| 40 |
// Set up token balances |
||
| 41 |
✓ 1
|
token.transfer(ADDRESS_USER0, STARTING_BALANCE); |
|
| 42 |
✓ 1
|
hevm.prank(ADDRESS_USER0); |
|
| 43 |
✓ 1
|
token.approve(address(driver), type(uint256).max); |
|
| 44 |
✓ 1
|
token.transfer(ADDRESS_USER1, STARTING_BALANCE); |
|
| 45 |
✓ 1
|
hevm.prank(ADDRESS_USER1); |
|
| 46 |
✓ 1
|
token.approve(address(driver), type(uint256).max); |
|
| 47 |
✓ 1
|
token.transfer(ADDRESS_USER2, STARTING_BALANCE); |
|
| 48 |
✓ 1
|
hevm.prank(ADDRESS_USER2); |
|
| 49 |
✓ 1
|
token.approve(address(driver), type(uint256).max); |
|
| 50 |
✓ 1
|
token.transfer(ADDRESS_USER3, STARTING_BALANCE); |
|
| 51 |
✓ 1
|
hevm.prank(ADDRESS_USER3); |
|
| 52 |
✓ 1
|
token.approve(address(driver), type(uint256).max); |
|
| 53 | |||
| 54 |
// Store starting timestamp |
||
| 55 |
✓ 1
|
STARTING_TIMESTAMP = block.timestamp; |
|
| 56 |
} |
||
| 57 |
} |
||
| 58 |
98.6%
src/echidna/base/EchidnaStorage.sol
Lines covered: 73 / 74 (98.6%)
| 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 |
✓ 13.0M
|
bytes32 receiversHash = drips.hashStreams(receivers); |
|
| 36 | |||
| 37 |
// Update the stream receivers for 'sender' |
||
| 38 |
✓ 13.0M
|
delete userToStreamReceivers[sender]; |
|
| 39 |
✓ 26.6M
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 40 |
✓ 13.6M
|
userToStreamReceivers[sender].push(receivers[i]); |
|
| 41 |
} |
||
| 42 | |||
| 43 |
// Add a new entry to the streams history |
||
| 44 |
✓ 13.0M
|
uint256 nextIndex = userToStreamsHistory[sender].length; |
|
| 45 |
✓ 13.0M
|
userToStreamsHistory[sender].push(); |
|
| 46 |
✓ 13.0M
|
userToStreamsHistory[sender][nextIndex].streamsHash = bytes32(0); |
|
| 47 |
✓ 26.6M
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 48 |
✓ 13.6M
|
userToStreamsHistory[sender][nextIndex].receivers.push( |
|
| 49 |
✓ 13.6M
|
receivers[i] |
|
| 50 |
); |
||
| 51 |
} |
||
| 52 |
✓ 13.0M
|
(, , uint32 updateTime, , uint32 maxEnd) = drips.streamsState( |
|
| 53 |
✓ 13.0M
|
getDripsAccountId(sender), |
|
| 54 |
✓ 13.0M
|
token |
|
| 55 |
); |
||
| 56 |
✓ 13.0M
|
userToStreamsHistory[sender][nextIndex].updateTime = updateTime; |
|
| 57 |
✓ 13.0M
|
userToStreamsHistory[sender][nextIndex].maxEnd = maxEnd; |
|
| 58 | |||
| 59 |
// Generate starting hash. If there is no previous history, use 0 |
||
| 60 |
✓ 13.0M
|
bytes32 startingHash; |
|
| 61 |
✓ 13.0M
|
if (nextIndex == 0) {
|
|
| 62 |
✓ 1.4M
|
startingHash = bytes32(0); |
|
| 63 |
} else {
|
||
| 64 |
✓ 11.6M
|
startingHash = userToStreamsHistoryHashes[sender][nextIndex - 1]; |
|
| 65 |
} |
||
| 66 | |||
| 67 |
// Add new entry to the streams history hashes |
||
| 68 |
✓ 13.0M
|
bytes32 historyHash = drips.hashStreamsHistory( |
|
| 69 |
✓ 13.0M
|
startingHash, |
|
| 70 |
✓ 13.0M
|
receiversHash, |
|
| 71 |
✓ 13.0M
|
updateTime, |
|
| 72 |
✓ 13.0M
|
maxEnd |
|
| 73 |
); |
||
| 74 |
✓ 13.0M
|
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 |
✓ 8.1M
|
delete userToSplitsReceivers[sender]; |
|
| 87 |
✓ 12.1M
|
for (uint256 i = 0; i < receivers.length; i++) {
|
|
| 88 |
✓ 4.0M
|
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 |
✓ 28.3M
|
returns (StreamReceiver[] memory) |
|
| 100 |
{
|
||
| 101 |
✓ 28.3M
|
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 |
✓ 36.7M
|
returns (StreamsHistory[] memory) |
|
| 112 |
{
|
||
| 113 |
✓ 36.7M
|
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 |
✓ 2.3M
|
returns (bytes32[] memory) |
|
| 124 |
{
|
||
| 125 |
✓ 2.3M
|
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 |
✓ 12.0M
|
returns (SplitsReceiver[] memory) |
|
| 136 |
{
|
||
| 137 |
✓ 12.0M
|
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 |
✓ 14.1M
|
returns (StreamReceiver[] memory) |
|
| 151 |
{
|
||
| 152 |
✓ 14.1M
|
uint256 n = unsorted.length; |
|
| 153 |
✓ 14.1M
|
if (n <= 1) return unsorted; |
|
| 154 | |||
| 155 |
✓ 3.2M
|
StreamReceiver[] memory sorted = unsorted; |
|
| 156 |
✓ 8.3M
|
for (uint256 i = 0; i < n - 1; i++) {
|
|
| 157 |
✓ 13.1M
|
for (uint256 j = 0; j < n - i - 1; j++) {
|
|
| 158 |
✓ 7.9M
|
if (bubbleSortStreamReceiverGT(sorted[j], sorted[j + 1])) {
|
|
| 159 |
✓ 1.8M
|
StreamReceiver memory temp = sorted[j]; |
|
| 160 |
✓ 1.8M
|
sorted[j] = sorted[j + 1]; |
|
| 161 |
✓ 1.8M
|
sorted[j + 1] = temp; |
|
| 162 |
} |
||
| 163 |
} |
||
| 164 |
} |
||
| 165 |
✓ 3.2M
|
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 |
✓ 6.9M
|
returns (SplitsReceiver[] memory) |
|
| 176 |
{
|
||
| 177 |
✓ 6.9M
|
uint256 n = unsorted.length; |
|
| 178 |
✓ 6.9M
|
if (n <= 1) return unsorted; |
|
| 179 | |||
| 180 |
✓ 338.8K
|
SplitsReceiver[] memory sorted = unsorted; |
|
| 181 |
✓ 607.3K
|
for (uint256 i = 0; i < n - 1; i++) {
|
|
| 182 |
✓ 665.8K
|
for (uint256 j = 0; j < n - i - 1; j++) {
|
|
| 183 |
✓ 397.3K
|
if (bubbleSortSplitsReceiverGT(sorted[j], sorted[j + 1])) {
|
|
| 184 |
✓ 133.1K
|
SplitsReceiver memory temp = sorted[j]; |
|
| 185 |
✓ 133.1K
|
sorted[j] = sorted[j + 1]; |
|
| 186 |
✓ 133.1K
|
sorted[j + 1] = temp; |
|
| 187 |
} |
||
| 188 |
} |
||
| 189 |
} |
||
| 190 |
✓ 243.6K
|
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 |
✓ 7.9M
|
) internal returns (bool) {
|
|
| 205 |
✓ 7.9M
|
if (a.accountId != b.accountId) {
|
|
| 206 |
✓ 3.9M
|
return a.accountId > b.accountId; |
|
| 207 |
} |
||
| 208 |
✓ 4.0M
|
if (StreamConfig.unwrap(a.config) != StreamConfig.unwrap(b.config)) {
|
|
| 209 |
return |
||
| 210 |
✓ 4.0M
|
StreamConfig.unwrap(a.config) > StreamConfig.unwrap(b.config); |
|
| 211 |
} |
||
| 212 |
✓ 510
|
⟲ 510
|
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 |
✓ 397.3K
|
) internal returns (bool) {
|
|
| 227 |
✓ 397.3K
|
if (a.accountId != b.accountId) {
|
|
| 228 |
✓ 302.1K
|
return a.accountId > b.accountId; |
|
| 229 |
} |
||
| 230 |
✓ 95.2K
|
⟲ 95.2K
|
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 |
✓ 145.9M
|
function getDripsAccountId(address account) internal returns (uint256) {
|
|
| 240 |
✓ 145.9M
|
if (ADDRESS_TO_DRIPS_ACCOUNT_ID[account] == 0) {
|
|
| 241 |
✓ 2.3M
|
ADDRESS_TO_DRIPS_ACCOUNT_ID[account] = driver.calcAccountId( |
|
| 242 |
✓ 2.3M
|
account |
|
| 243 |
); |
||
| 244 |
} |
||
| 245 |
✓ 145.9M
|
return ADDRESS_TO_DRIPS_ACCOUNT_ID[account]; |
|
| 246 |
} |
||
| 247 |
} |
||
| 248 |
100.0%
src/echidna/tools/AddressDriverEchidna.sol
Lines covered: 2 / 2 (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 |
✓ 31.1M
|
contract AddressDriverEchidna is AddressDriver, ManagedEchidna {
|
|
| 12 |
constructor( |
||
| 13 |
DripsEchidna drips_, |
||
| 14 |
address forwarder, |
||
| 15 |
uint32 driverId_ |
||
| 16 |
✓ 1
|
) AddressDriver(drips_, forwarder, driverId_) {}
|
|
| 17 |
} |
||
| 18 |
100.0%
src/echidna/tools/Debugger.sol
Lines covered: 1 / 1 (100.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
✓ 6
|
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: 8 / 8 (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 |
✓ 1
|
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 |
✓ 76.8M
|
) public view returns (int128, int128) {
|
|
| 26 |
// Manually calculate the storage slot because it is a private variable |
||
| 27 |
// in Streams |
||
| 28 |
✓ 76.8M
|
StreamsStorage storage streamsStorage; |
|
| 29 |
✓ 76.8M
|
bytes32 slot = _erc1967Slot("eip1967.streams.storage");
|
|
| 30 |
assembly {
|
||
| 31 |
✓ 76.8M
|
streamsStorage.slot := slot |
|
| 32 |
} |
||
| 33 | |||
| 34 |
// Return the amtDelta for the given cycle |
||
| 35 |
✓ 76.8M
|
StreamsState storage state = streamsStorage.states[erc20][accountId]; |
|
| 36 |
✓ 76.8M
|
mapping(uint32 cycle => AmtDelta) storage amtDeltas = state.amtDeltas; |
|
| 37 |
✓ 76.8M
|
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 |
66.7%
src/echidna/tools/ManagedEchidna.sol
Lines covered: 2 / 3 (66.7%)
| 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 |
✓ 1
|
_managedStorage().isPaused = false; |
|
| 16 |
✓ 1
|
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 |
✓ 38.4M
|
contract CryticTester is Properties {}
|
|
| 8 |
57.1%
test/recon/Properties.sol
Lines covered: 4 / 7 (57.1%)
| 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 |
✓ 388.0K
|
if (!b) {
|
|
| 18 |
✓ 21.4K
|
emit AssertionFailed(reason); |
|
| 19 |
✓ 21.4K
|
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 |
✓ 388.0K
|
t(entropy > 0, ASSERTION_CANARY); |
|
| 30 |
} |
||
| 31 |
} |
||
| 32 |