upsideos_evm_rwa_artifacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +95 -0
  3. data/data/recallable-payment/abi/AccessControl.json +1 -0
  4. data/data/recallable-payment/abi/RecallablePayment.json +1 -0
  5. data/data/recallable-payment/manifest.json +1 -0
  6. data/data/recallable-payment/verification-source-codes.json +1 -0
  7. data/data/v5/abi/AccessControl.json +1 -0
  8. data/data/v5/abi/ERC2771CustomForwarder.json +1 -0
  9. data/data/v5/abi/IdentityRegistry.json +1 -0
  10. data/data/v5/abi/InterestPayment.json +1 -0
  11. data/data/v5/abi/PurchaseContract.json +1 -0
  12. data/data/v5/abi/RestrictedLockupToken.json +1 -0
  13. data/data/v5/abi/RestrictedLockupTokenExtension.json +1 -0
  14. data/data/v5/abi/RestrictedLockupTokenManagementExtension.json +1 -0
  15. data/data/v5/abi/RestrictedSwap.json +1 -0
  16. data/data/v5/abi/SnapshotPeriods.json +1 -0
  17. data/data/v5/abi/TransferRules.json +1 -0
  18. data/data/v5/abi/merged/RestrictedLockupToken.json +1 -0
  19. data/data/v5/manifest.json +1 -0
  20. data/data/v5/verification-source-codes.json +1 -0
  21. data/data/v5.1/abi/AccessControl.json +1 -0
  22. data/data/v5.1/abi/ERC2771CustomForwarder.json +1 -0
  23. data/data/v5.1/abi/IdentityRegistry.json +1 -0
  24. data/data/v5.1/abi/InterestPayment.json +1 -0
  25. data/data/v5.1/abi/PurchaseContract.json +1 -0
  26. data/data/v5.1/abi/RestrictedLockupToken.json +1 -0
  27. data/data/v5.1/abi/RestrictedLockupTokenExtension.json +1 -0
  28. data/data/v5.1/abi/RestrictedLockupTokenManagementExtension.json +1 -0
  29. data/data/v5.1/abi/RestrictedLockupTokenStandardsExtension.json +1 -0
  30. data/data/v5.1/abi/RestrictedSwap.json +1 -0
  31. data/data/v5.1/abi/SnapshotPeriods.json +1 -0
  32. data/data/v5.1/abi/TransferRules.json +1 -0
  33. data/data/v5.1/abi/merged/RestrictedLockupToken.json +1 -0
  34. data/data/v5.1/manifest.json +1 -0
  35. data/data/v5.1/verification-source-codes.json +1 -0
  36. data/lib/upsideos_evm_rwa_artifacts/version.rb +5 -0
  37. data/lib/upsideos_evm_rwa_artifacts.rb +118 -0
  38. metadata +81 -0
@@ -0,0 +1 @@
1
+ {"_format":"hh-sol-build-info-1","id":"1110a83cd06ec2b0ceabed3bafb0b48b","input":{"language":"Solidity","settings":{"evmVersion":"paris","metadata":{"useLiteralContent":true},"optimizer":{"enabled":true,"runs":200},"outputSelection":{"*":{"":["ast"],"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata","storageLayout","devdoc","userdoc","evm.gasEstimates"]}},"viaIR":true},"sources":{"@openzeppelin/contracts/access/Ownable.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)\n\npragma solidity ^0.8.20;\n\nimport {Context} from \"../utils/Context.sol\";\n\n/**\n * @dev Contract module which provides a basic access control mechanism, where\n * there is an account (an owner) that can be granted exclusive access to\n * specific functions.\n *\n * The initial owner is set to the address provided by the deployer. This can\n * later be changed with {transferOwnership}.\n *\n * This module is used through inheritance. It will make available the modifier\n * `onlyOwner`, which can be applied to your functions to restrict their use to\n * the owner.\n */\nabstract contract Ownable is Context {\n address private _owner;\n\n /**\n * @dev The caller account is not authorized to perform an operation.\n */\n error OwnableUnauthorizedAccount(address account);\n\n /**\n * @dev The owner is not a valid owner account. (eg. `address(0)`)\n */\n error OwnableInvalidOwner(address owner);\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /**\n * @dev Initializes the contract setting the address provided by the deployer as the initial owner.\n */\n constructor(address initialOwner) {\n if (initialOwner == address(0)) {\n revert OwnableInvalidOwner(address(0));\n }\n _transferOwnership(initialOwner);\n }\n\n /**\n * @dev Throws if called by any account other than the owner.\n */\n modifier onlyOwner() {\n _checkOwner();\n _;\n }\n\n /**\n * @dev Returns the address of the current owner.\n */\n function owner() public view virtual returns (address) {\n return _owner;\n }\n\n /**\n * @dev Throws if the sender is not the owner.\n */\n function _checkOwner() internal view virtual {\n if (owner() != _msgSender()) {\n revert OwnableUnauthorizedAccount(_msgSender());\n }\n }\n\n /**\n * @dev Leaves the contract without owner. It will not be possible to call\n * `onlyOwner` functions. Can only be called by the current owner.\n *\n * NOTE: Renouncing ownership will leave the contract without an owner,\n * thereby disabling any functionality that is only available to the owner.\n */\n function renounceOwnership() public virtual onlyOwner {\n _transferOwnership(address(0));\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Can only be called by the current owner.\n */\n function transferOwnership(address newOwner) public virtual onlyOwner {\n if (newOwner == address(0)) {\n revert OwnableInvalidOwner(address(0));\n }\n _transferOwnership(newOwner);\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Internal function without access restriction.\n */\n function _transferOwnership(address newOwner) internal virtual {\n address oldOwner = _owner;\n _owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n}\n"},"@openzeppelin/contracts/interfaces/IERC1363.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC165} from \"./IERC165.sol\";\n\n/**\n * @title IERC1363\n * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].\n *\n * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract\n * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.\n */\ninterface IERC1363 is IERC20, IERC165 {\n /*\n * Note: the ERC-165 identifier for this interface is 0xb0202a11.\n * 0xb0202a11 ===\n * bytes4(keccak256('transferAndCall(address,uint256)')) ^\n * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256,bytes)'))\n */\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @param data Additional data with no specified format, sent in call to `spender`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);\n}\n"},"@openzeppelin/contracts/interfaces/IERC165.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC165} from \"../utils/introspection/IERC165.sol\";\n"},"@openzeppelin/contracts/interfaces/IERC20.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../token/ERC20/IERC20.sol\";\n"},"@openzeppelin/contracts/interfaces/IERC5267.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5267.sol)\n\npragma solidity ^0.8.20;\n\ninterface IERC5267 {\n /**\n * @dev MAY be emitted to signal that the domain could have changed.\n */\n event EIP712DomainChanged();\n\n /**\n * @dev returns the fields and values that describe the domain separator used by this contract for EIP-712\n * signature.\n */\n function eip712Domain()\n external\n view\n returns (\n bytes1 fields,\n string memory name,\n string memory version,\n uint256 chainId,\n address verifyingContract,\n bytes32 salt,\n uint256[] memory extensions\n );\n}\n"},"@openzeppelin/contracts/interfaces/draft-IERC6093.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC6093.sol)\npragma solidity ^0.8.20;\n\n/**\n * @dev Standard ERC-20 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens.\n */\ninterface IERC20Errors {\n /**\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param balance Current balance for the interacting account.\n * @param needed Minimum amount required to perform a transfer.\n */\n error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC20InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC20InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers.\n * @param spender Address that may be allowed to operate on tokens without being their owner.\n * @param allowance Amount of tokens a `spender` is allowed to operate with.\n * @param needed Minimum amount required to perform a transfer.\n */\n error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC20InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `spender` to be approved. Used in approvals.\n * @param spender Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC20InvalidSpender(address spender);\n}\n\n/**\n * @dev Standard ERC-721 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens.\n */\ninterface IERC721Errors {\n /**\n * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20.\n * Used in balance queries.\n * @param owner Address of the current owner of a token.\n */\n error ERC721InvalidOwner(address owner);\n\n /**\n * @dev Indicates a `tokenId` whose `owner` is the zero address.\n * @param tokenId Identifier number of a token.\n */\n error ERC721NonexistentToken(uint256 tokenId);\n\n /**\n * @dev Indicates an error related to the ownership over a particular token. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param tokenId Identifier number of a token.\n * @param owner Address of the current owner of a token.\n */\n error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC721InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC721InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `operator`’s approval. Used in transfers.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n * @param tokenId Identifier number of a token.\n */\n error ERC721InsufficientApproval(address operator, uint256 tokenId);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC721InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC721InvalidOperator(address operator);\n}\n\n/**\n * @dev Standard ERC-1155 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens.\n */\ninterface IERC1155Errors {\n /**\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param balance Current balance for the interacting account.\n * @param needed Minimum amount required to perform a transfer.\n * @param tokenId Identifier number of a token.\n */\n error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC1155InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC1155InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `operator`’s approval. Used in transfers.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n * @param owner Address of the current owner of a token.\n */\n error ERC1155MissingApprovalForAll(address operator, address owner);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC1155InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC1155InvalidOperator(address operator);\n\n /**\n * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation.\n * Used in batch transfers.\n * @param idsLength Length of the array of token identifiers\n * @param valuesLength Length of the array of token amounts\n */\n error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength);\n}\n"},"@openzeppelin/contracts/metatx/ERC2771Context.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (metatx/ERC2771Context.sol)\n\npragma solidity ^0.8.20;\n\nimport {Context} from \"../utils/Context.sol\";\n\n/**\n * @dev Context variant with ERC-2771 support.\n *\n * WARNING: Avoid using this pattern in contracts that rely in a specific calldata length as they'll\n * be affected by any forwarder whose `msg.data` is suffixed with the `from` address according to the ERC-2771\n * specification adding the address size in bytes (20) to the calldata size. An example of an unexpected\n * behavior could be an unintended fallback (or another function) invocation while trying to invoke the `receive`\n * function only accessible if `msg.data.length == 0`.\n *\n * WARNING: The usage of `delegatecall` in this contract is dangerous and may result in context corruption.\n * Any forwarded request to this contract triggering a `delegatecall` to itself will result in an invalid {_msgSender}\n * recovery.\n */\nabstract contract ERC2771Context is Context {\n /// @custom:oz-upgrades-unsafe-allow state-variable-immutable\n address private immutable _trustedForwarder;\n\n /**\n * @dev Initializes the contract with a trusted forwarder, which will be able to\n * invoke functions on this contract on behalf of other accounts.\n *\n * NOTE: The trusted forwarder can be replaced by overriding {trustedForwarder}.\n */\n /// @custom:oz-upgrades-unsafe-allow constructor\n constructor(address trustedForwarder_) {\n _trustedForwarder = trustedForwarder_;\n }\n\n /**\n * @dev Returns the address of the trusted forwarder.\n */\n function trustedForwarder() public view virtual returns (address) {\n return _trustedForwarder;\n }\n\n /**\n * @dev Indicates whether any particular address is the trusted forwarder.\n */\n function isTrustedForwarder(address forwarder) public view virtual returns (bool) {\n return forwarder == trustedForwarder();\n }\n\n /**\n * @dev Override for `msg.sender`. Defaults to the original `msg.sender` whenever\n * a call is not performed by the trusted forwarder or the calldata length is less than\n * 20 bytes (an address length).\n */\n function _msgSender() internal view virtual override returns (address) {\n uint256 calldataLength = msg.data.length;\n uint256 contextSuffixLength = _contextSuffixLength();\n if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {\n return address(bytes20(msg.data[calldataLength - contextSuffixLength:]));\n } else {\n return super._msgSender();\n }\n }\n\n /**\n * @dev Override for `msg.data`. Defaults to the original `msg.data` whenever\n * a call is not performed by the trusted forwarder or the calldata length is less than\n * 20 bytes (an address length).\n */\n function _msgData() internal view virtual override returns (bytes calldata) {\n uint256 calldataLength = msg.data.length;\n uint256 contextSuffixLength = _contextSuffixLength();\n if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {\n return msg.data[:calldataLength - contextSuffixLength];\n } else {\n return super._msgData();\n }\n }\n\n /**\n * @dev ERC-2771 specifies the context as being a single address (20 bytes).\n */\n function _contextSuffixLength() internal view virtual override returns (uint256) {\n return 20;\n }\n}\n"},"@openzeppelin/contracts/metatx/ERC2771Forwarder.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (metatx/ERC2771Forwarder.sol)\n\npragma solidity ^0.8.20;\n\nimport {ERC2771Context} from \"./ERC2771Context.sol\";\nimport {ECDSA} from \"../utils/cryptography/ECDSA.sol\";\nimport {EIP712} from \"../utils/cryptography/EIP712.sol\";\nimport {Nonces} from \"../utils/Nonces.sol\";\nimport {Address} from \"../utils/Address.sol\";\nimport {Errors} from \"../utils/Errors.sol\";\n\n/**\n * @dev A forwarder compatible with ERC-2771 contracts. See {ERC2771Context}.\n *\n * This forwarder operates on forward requests that include:\n *\n * * `from`: An address to operate on behalf of. It is required to be equal to the request signer.\n * * `to`: The address that should be called.\n * * `value`: The amount of native token to attach with the requested call.\n * * `gas`: The amount of gas limit that will be forwarded with the requested call.\n * * `nonce`: A unique transaction ordering identifier to avoid replayability and request invalidation.\n * * `deadline`: A timestamp after which the request is not executable anymore.\n * * `data`: Encoded `msg.data` to send with the requested call.\n *\n * Relayers are able to submit batches if they are processing a high volume of requests. With high\n * throughput, relayers may run into limitations of the chain such as limits on the number of\n * transactions in the mempool. In these cases the recommendation is to distribute the load among\n * multiple accounts.\n *\n * NOTE: Batching requests includes an optional refund for unused `msg.value` that is achieved by\n * performing a call with empty calldata. While this is within the bounds of ERC-2771 compliance,\n * if the refund receiver happens to consider the forwarder a trusted forwarder, it MUST properly\n * handle `msg.data.length == 0`. `ERC2771Context` in OpenZeppelin Contracts versions prior to 4.9.3\n * do not handle this properly.\n *\n * ==== Security Considerations\n *\n * If a relayer submits a forward request, it should be willing to pay up to 100% of the gas amount\n * specified in the request. This contract does not implement any kind of retribution for this gas,\n * and it is assumed that there is an out of band incentive for relayers to pay for execution on\n * behalf of signers. Often, the relayer is operated by a project that will consider it a user\n * acquisition cost.\n *\n * By offering to pay for gas, relayers are at risk of having that gas used by an attacker toward\n * some other purpose that is not aligned with the expected out of band incentives. If you operate a\n * relayer, consider whitelisting target contracts and function selectors. When relaying ERC-721 or\n * ERC-1155 transfers specifically, consider rejecting the use of the `data` field, since it can be\n * used to execute arbitrary code.\n */\ncontract ERC2771Forwarder is EIP712, Nonces {\n using ECDSA for bytes32;\n\n struct ForwardRequestData {\n address from;\n address to;\n uint256 value;\n uint256 gas;\n uint48 deadline;\n bytes data;\n bytes signature;\n }\n\n bytes32 internal constant _FORWARD_REQUEST_TYPEHASH =\n keccak256(\n \"ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint48 deadline,bytes data)\"\n );\n\n /**\n * @dev Emitted when a `ForwardRequest` is executed.\n *\n * NOTE: An unsuccessful forward request could be due to an invalid signature, an expired deadline,\n * or simply a revert in the requested call. The contract guarantees that the relayer is not able to force\n * the requested call to run out of gas.\n */\n event ExecutedForwardRequest(address indexed signer, uint256 nonce, bool success);\n\n /**\n * @dev The request `from` doesn't match with the recovered `signer`.\n */\n error ERC2771ForwarderInvalidSigner(address signer, address from);\n\n /**\n * @dev The `requestedValue` doesn't match with the available `msgValue`.\n */\n error ERC2771ForwarderMismatchedValue(uint256 requestedValue, uint256 msgValue);\n\n /**\n * @dev The request `deadline` has expired.\n */\n error ERC2771ForwarderExpiredRequest(uint48 deadline);\n\n /**\n * @dev The request target doesn't trust the `forwarder`.\n */\n error ERC2771UntrustfulTarget(address target, address forwarder);\n\n /**\n * @dev See {EIP712-constructor}.\n */\n constructor(string memory name) EIP712(name, \"1\") {}\n\n /**\n * @dev Returns `true` if a request is valid for a provided `signature` at the current block timestamp.\n *\n * A transaction is considered valid when the target trusts this forwarder, the request hasn't expired\n * (deadline is not met), and the signer matches the `from` parameter of the signed request.\n *\n * NOTE: A request may return false here but it won't cause {executeBatch} to revert if a refund\n * receiver is provided.\n */\n function verify(ForwardRequestData calldata request) public view virtual returns (bool) {\n (bool isTrustedForwarder, bool active, bool signerMatch, ) = _validate(request);\n return isTrustedForwarder && active && signerMatch;\n }\n\n /**\n * @dev Executes a `request` on behalf of `signature`'s signer using the ERC-2771 protocol. The gas\n * provided to the requested call may not be exactly the amount requested, but the call will not run\n * out of gas. Will revert if the request is invalid or the call reverts, in this case the nonce is not consumed.\n *\n * Requirements:\n *\n * - The request value should be equal to the provided `msg.value`.\n * - The request should be valid according to {verify}.\n */\n function execute(ForwardRequestData calldata request) public payable virtual {\n // We make sure that msg.value and request.value match exactly.\n // If the request is invalid or the call reverts, this whole function\n // will revert, ensuring value isn't stuck.\n if (msg.value != request.value) {\n revert ERC2771ForwarderMismatchedValue(request.value, msg.value);\n }\n\n if (!_execute(request, true)) {\n revert Errors.FailedCall();\n }\n }\n\n /**\n * @dev Batch version of {execute} with optional refunding and atomic execution.\n *\n * In case a batch contains at least one invalid request (see {verify}), the\n * request will be skipped and the `refundReceiver` parameter will receive back the\n * unused requested value at the end of the execution. This is done to prevent reverting\n * the entire batch when a request is invalid or has already been submitted.\n *\n * If the `refundReceiver` is the `address(0)`, this function will revert when at least\n * one of the requests was not valid instead of skipping it. This could be useful if\n * a batch is required to get executed atomically (at least at the top-level). For example,\n * refunding (and thus atomicity) can be opt-out if the relayer is using a service that avoids\n * including reverted transactions.\n *\n * Requirements:\n *\n * - The sum of the requests' values should be equal to the provided `msg.value`.\n * - All of the requests should be valid (see {verify}) when `refundReceiver` is the zero address.\n *\n * NOTE: Setting a zero `refundReceiver` guarantees an all-or-nothing requests execution only for\n * the first-level forwarded calls. In case a forwarded request calls to a contract with another\n * subcall, the second-level call may revert without the top-level call reverting.\n */\n function executeBatch(\n ForwardRequestData[] calldata requests,\n address payable refundReceiver\n ) public payable virtual {\n bool atomic = refundReceiver == address(0);\n\n uint256 requestsValue;\n uint256 refundValue;\n\n for (uint256 i; i < requests.length; ++i) {\n requestsValue += requests[i].value;\n bool success = _execute(requests[i], atomic);\n if (!success) {\n refundValue += requests[i].value;\n }\n }\n\n // The batch should revert if there's a mismatched msg.value provided\n // to avoid request value tampering\n if (requestsValue != msg.value) {\n revert ERC2771ForwarderMismatchedValue(requestsValue, msg.value);\n }\n\n // Some requests with value were invalid (possibly due to frontrunning).\n // To avoid leaving ETH in the contract this value is refunded.\n if (refundValue != 0) {\n // We know refundReceiver != address(0) && requestsValue == msg.value\n // meaning we can ensure refundValue is not taken from the original contract's balance\n // and refundReceiver is a known account.\n Address.sendValue(refundReceiver, refundValue);\n }\n }\n\n /**\n * @dev Validates if the provided request can be executed at current block timestamp with\n * the given `request.signature` on behalf of `request.signer`.\n */\n function _validate(\n ForwardRequestData calldata request\n ) internal view virtual returns (bool isTrustedForwarder, bool active, bool signerMatch, address signer) {\n (bool isValid, address recovered) = _recoverForwardRequestSigner(request);\n\n return (\n _isTrustedByTarget(request.to),\n request.deadline >= block.timestamp,\n isValid && recovered == request.from,\n recovered\n );\n }\n\n /**\n * @dev Returns a tuple with the recovered the signer of an EIP712 forward request message hash\n * and a boolean indicating if the signature is valid.\n *\n * NOTE: The signature is considered valid if {ECDSA-tryRecover} indicates no recover error for it.\n */\n function _recoverForwardRequestSigner(\n ForwardRequestData calldata request\n ) internal view virtual returns (bool isValid, address signer) {\n (address recovered, ECDSA.RecoverError err, ) = _hashTypedDataV4(\n keccak256(\n abi.encode(\n _FORWARD_REQUEST_TYPEHASH,\n request.from,\n request.to,\n request.value,\n request.gas,\n nonces(request.from),\n request.deadline,\n keccak256(request.data)\n )\n )\n ).tryRecover(request.signature);\n\n return (err == ECDSA.RecoverError.NoError, recovered);\n }\n\n /**\n * @dev Validates and executes a signed request returning the request call `success` value.\n *\n * Internal function without msg.value validation.\n *\n * Requirements:\n *\n * - The caller must have provided enough gas to forward with the call.\n * - The request must be valid (see {verify}) if the `requireValidRequest` is true.\n *\n * Emits an {ExecutedForwardRequest} event.\n *\n * IMPORTANT: Using this function doesn't check that all the `msg.value` was sent, potentially\n * leaving value stuck in the contract.\n */\n function _execute(\n ForwardRequestData calldata request,\n bool requireValidRequest\n ) internal virtual returns (bool success) {\n (bool isTrustedForwarder, bool active, bool signerMatch, address signer) = _validate(request);\n\n // Need to explicitly specify if a revert is required since non-reverting is default for\n // batches and reversion is opt-in since it could be useful in some scenarios\n if (requireValidRequest) {\n if (!isTrustedForwarder) {\n revert ERC2771UntrustfulTarget(request.to, address(this));\n }\n\n if (!active) {\n revert ERC2771ForwarderExpiredRequest(request.deadline);\n }\n\n if (!signerMatch) {\n revert ERC2771ForwarderInvalidSigner(signer, request.from);\n }\n }\n\n // Ignore an invalid request because requireValidRequest = false\n if (isTrustedForwarder && signerMatch && active) {\n // Nonce should be used before the call to prevent reusing by reentrancy\n uint256 currentNonce = _useNonce(signer);\n\n uint256 reqGas = request.gas;\n address to = request.to;\n uint256 value = request.value;\n bytes memory data = abi.encodePacked(request.data, request.from);\n\n uint256 gasLeft;\n\n assembly (\"memory-safe\") {\n success := call(reqGas, to, value, add(data, 0x20), mload(data), 0, 0)\n gasLeft := gas()\n }\n\n _checkForwardedGas(gasLeft, request);\n\n emit ExecutedForwardRequest(signer, currentNonce, success);\n }\n }\n\n /**\n * @dev Returns whether the target trusts this forwarder.\n *\n * This function performs a static call to the target contract calling the\n * {ERC2771Context-isTrustedForwarder} function.\n *\n * NOTE: Consider the execution of this forwarder is permissionless. Without this check, anyone may transfer assets\n * that are owned by, or are approved to this forwarder.\n */\n function _isTrustedByTarget(address target) internal view virtual returns (bool) {\n bytes memory encodedParams = abi.encodeCall(ERC2771Context.isTrustedForwarder, (address(this)));\n\n bool success;\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n // Perform the staticcall and save the result in the scratch space.\n // | Location | Content | Content (Hex) |\n // |-----------|----------|--------------------------------------------------------------------|\n // | | | result ↓ |\n // | 0x00:0x1F | selector | 0x0000000000000000000000000000000000000000000000000000000000000001 |\n success := staticcall(gas(), target, add(encodedParams, 0x20), mload(encodedParams), 0, 0x20)\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n\n return success && returnSize >= 0x20 && returnValue > 0;\n }\n\n /**\n * @dev Checks if the requested gas was correctly forwarded to the callee.\n *\n * As a consequence of https://eips.ethereum.org/EIPS/eip-150[EIP-150]:\n * - At most `gasleft() - floor(gasleft() / 64)` is forwarded to the callee.\n * - At least `floor(gasleft() / 64)` is kept in the caller.\n *\n * It reverts consuming all the available gas if the forwarded gas is not the requested gas.\n *\n * IMPORTANT: The `gasLeft` parameter should be measured exactly at the end of the forwarded call.\n * Any gas consumed in between will make room for bypassing this check.\n */\n function _checkForwardedGas(uint256 gasLeft, ForwardRequestData calldata request) private pure {\n // To avoid insufficient gas griefing attacks, as referenced in https://ronan.eth.limo/blog/ethereum-gas-dangers/\n //\n // A malicious relayer can attempt to shrink the gas forwarded so that the underlying call reverts out-of-gas\n // but the forwarding itself still succeeds. In order to make sure that the subcall received sufficient gas,\n // we will inspect gasleft() after the forwarding.\n //\n // Let X be the gas available before the subcall, such that the subcall gets at most X * 63 / 64.\n // We can't know X after CALL dynamic costs, but we want it to be such that X * 63 / 64 >= req.gas.\n // Let Y be the gas used in the subcall. gasleft() measured immediately after the subcall will be gasleft() = X - Y.\n // If the subcall ran out of gas, then Y = X * 63 / 64 and gasleft() = X - Y = X / 64.\n // Under this assumption req.gas / 63 > gasleft() is true if and only if\n // req.gas / 63 > X / 64, or equivalently req.gas > X * 63 / 64.\n // This means that if the subcall runs out of gas we are able to detect that insufficient gas was passed.\n //\n // We will now also see that req.gas / 63 > gasleft() implies that req.gas >= X * 63 / 64.\n // The contract guarantees Y <= req.gas, thus gasleft() = X - Y >= X - req.gas.\n // - req.gas / 63 > gasleft()\n // - req.gas / 63 >= X - req.gas\n // - req.gas >= X * 63 / 64\n // In other words if req.gas < X * 63 / 64 then req.gas / 63 <= gasleft(), thus if the relayer behaves honestly\n // the forwarding does not revert.\n if (gasLeft < request.gas / 63) {\n // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since\n // neither revert or assert consume all gas since Solidity 0.8.20\n // https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require\n assembly (\"memory-safe\") {\n invalid()\n }\n }\n }\n}\n"},"@openzeppelin/contracts/token/ERC20/ERC20.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/ERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC20Metadata} from \"./extensions/IERC20Metadata.sol\";\nimport {Context} from \"../../utils/Context.sol\";\nimport {IERC20Errors} from \"../../interfaces/draft-IERC6093.sol\";\n\n/**\n * @dev Implementation of the {IERC20} interface.\n *\n * This implementation is agnostic to the way tokens are created. This means\n * that a supply mechanism has to be added in a derived contract using {_mint}.\n *\n * TIP: For a detailed writeup see our guide\n * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How\n * to implement supply mechanisms].\n *\n * The default value of {decimals} is 18. To change this, you should override\n * this function so it returns a different value.\n *\n * We have followed general OpenZeppelin Contracts guidelines: functions revert\n * instead returning `false` on failure. This behavior is nonetheless\n * conventional and does not conflict with the expectations of ERC-20\n * applications.\n */\nabstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {\n mapping(address account => uint256) private _balances;\n\n mapping(address account => mapping(address spender => uint256)) private _allowances;\n\n uint256 private _totalSupply;\n\n string private _name;\n string private _symbol;\n\n /**\n * @dev Sets the values for {name} and {symbol}.\n *\n * Both values are immutable: they can only be set once during construction.\n */\n constructor(string memory name_, string memory symbol_) {\n _name = name_;\n _symbol = symbol_;\n }\n\n /**\n * @dev Returns the name of the token.\n */\n function name() public view virtual returns (string memory) {\n return _name;\n }\n\n /**\n * @dev Returns the symbol of the token, usually a shorter version of the\n * name.\n */\n function symbol() public view virtual returns (string memory) {\n return _symbol;\n }\n\n /**\n * @dev Returns the number of decimals used to get its user representation.\n * For example, if `decimals` equals `2`, a balance of `505` tokens should\n * be displayed to a user as `5.05` (`505 / 10 ** 2`).\n *\n * Tokens usually opt for a value of 18, imitating the relationship between\n * Ether and Wei. This is the default value returned by this function, unless\n * it's overridden.\n *\n * NOTE: This information is only used for _display_ purposes: it in\n * no way affects any of the arithmetic of the contract, including\n * {IERC20-balanceOf} and {IERC20-transfer}.\n */\n function decimals() public view virtual returns (uint8) {\n return 18;\n }\n\n /**\n * @dev See {IERC20-totalSupply}.\n */\n function totalSupply() public view virtual returns (uint256) {\n return _totalSupply;\n }\n\n /**\n * @dev See {IERC20-balanceOf}.\n */\n function balanceOf(address account) public view virtual returns (uint256) {\n return _balances[account];\n }\n\n /**\n * @dev See {IERC20-transfer}.\n *\n * Requirements:\n *\n * - `to` cannot be the zero address.\n * - the caller must have a balance of at least `value`.\n */\n function transfer(address to, uint256 value) public virtual returns (bool) {\n address owner = _msgSender();\n _transfer(owner, to, value);\n return true;\n }\n\n /**\n * @dev See {IERC20-allowance}.\n */\n function allowance(address owner, address spender) public view virtual returns (uint256) {\n return _allowances[owner][spender];\n }\n\n /**\n * @dev See {IERC20-approve}.\n *\n * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on\n * `transferFrom`. This is semantically equivalent to an infinite approval.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n */\n function approve(address spender, uint256 value) public virtual returns (bool) {\n address owner = _msgSender();\n _approve(owner, spender, value);\n return true;\n }\n\n /**\n * @dev See {IERC20-transferFrom}.\n *\n * Skips emitting an {Approval} event indicating an allowance update. This is not\n * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].\n *\n * NOTE: Does not update the allowance if the current allowance\n * is the maximum `uint256`.\n *\n * Requirements:\n *\n * - `from` and `to` cannot be the zero address.\n * - `from` must have a balance of at least `value`.\n * - the caller must have allowance for ``from``'s tokens of at least\n * `value`.\n */\n function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {\n address spender = _msgSender();\n _spendAllowance(from, spender, value);\n _transfer(from, to, value);\n return true;\n }\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to`.\n *\n * This internal function is equivalent to {transfer}, and can be used to\n * e.g. implement automatic token fees, slashing mechanisms, etc.\n *\n * Emits a {Transfer} event.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead.\n */\n function _transfer(address from, address to, uint256 value) internal {\n if (from == address(0)) {\n revert ERC20InvalidSender(address(0));\n }\n if (to == address(0)) {\n revert ERC20InvalidReceiver(address(0));\n }\n _update(from, to, value);\n }\n\n /**\n * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`\n * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding\n * this function.\n *\n * Emits a {Transfer} event.\n */\n function _update(address from, address to, uint256 value) internal virtual {\n if (from == address(0)) {\n // Overflow check required: The rest of the code assumes that totalSupply never overflows\n _totalSupply += value;\n } else {\n uint256 fromBalance = _balances[from];\n if (fromBalance < value) {\n revert ERC20InsufficientBalance(from, fromBalance, value);\n }\n unchecked {\n // Overflow not possible: value <= fromBalance <= totalSupply.\n _balances[from] = fromBalance - value;\n }\n }\n\n if (to == address(0)) {\n unchecked {\n // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.\n _totalSupply -= value;\n }\n } else {\n unchecked {\n // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.\n _balances[to] += value;\n }\n }\n\n emit Transfer(from, to, value);\n }\n\n /**\n * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).\n * Relies on the `_update` mechanism\n *\n * Emits a {Transfer} event with `from` set to the zero address.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead.\n */\n function _mint(address account, uint256 value) internal {\n if (account == address(0)) {\n revert ERC20InvalidReceiver(address(0));\n }\n _update(address(0), account, value);\n }\n\n /**\n * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.\n * Relies on the `_update` mechanism.\n *\n * Emits a {Transfer} event with `to` set to the zero address.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead\n */\n function _burn(address account, uint256 value) internal {\n if (account == address(0)) {\n revert ERC20InvalidSender(address(0));\n }\n _update(account, address(0), value);\n }\n\n /**\n * @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens.\n *\n * This internal function is equivalent to `approve`, and can be used to\n * e.g. set automatic allowances for certain subsystems, etc.\n *\n * Emits an {Approval} event.\n *\n * Requirements:\n *\n * - `owner` cannot be the zero address.\n * - `spender` cannot be the zero address.\n *\n * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.\n */\n function _approve(address owner, address spender, uint256 value) internal {\n _approve(owner, spender, value, true);\n }\n\n /**\n * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.\n *\n * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by\n * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any\n * `Approval` event during `transferFrom` operations.\n *\n * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to\n * true using the following override:\n *\n * ```solidity\n * function _approve(address owner, address spender, uint256 value, bool) internal virtual override {\n * super._approve(owner, spender, value, true);\n * }\n * ```\n *\n * Requirements are the same as {_approve}.\n */\n function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {\n if (owner == address(0)) {\n revert ERC20InvalidApprover(address(0));\n }\n if (spender == address(0)) {\n revert ERC20InvalidSpender(address(0));\n }\n _allowances[owner][spender] = value;\n if (emitEvent) {\n emit Approval(owner, spender, value);\n }\n }\n\n /**\n * @dev Updates `owner`'s allowance for `spender` based on spent `value`.\n *\n * Does not update the allowance value in case of infinite allowance.\n * Revert if not enough allowance is available.\n *\n * Does not emit an {Approval} event.\n */\n function _spendAllowance(address owner, address spender, uint256 value) internal virtual {\n uint256 currentAllowance = allowance(owner, spender);\n if (currentAllowance < type(uint256).max) {\n if (currentAllowance < value) {\n revert ERC20InsufficientAllowance(spender, currentAllowance, value);\n }\n unchecked {\n _approve(owner, spender, currentAllowance - value, false);\n }\n }\n }\n}\n"},"@openzeppelin/contracts/token/ERC20/IERC20.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n"},"@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Permit.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20Permit} from \"./IERC20Permit.sol\";\nimport {ERC20} from \"../ERC20.sol\";\nimport {ECDSA} from \"../../../utils/cryptography/ECDSA.sol\";\nimport {EIP712} from \"../../../utils/cryptography/EIP712.sol\";\nimport {Nonces} from \"../../../utils/Nonces.sol\";\n\n/**\n * @dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in\n * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].\n *\n * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by\n * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't\n * need to send a transaction, and thus is not required to hold Ether at all.\n */\nabstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces {\n bytes32 private constant PERMIT_TYPEHASH =\n keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\");\n\n /**\n * @dev Permit deadline has expired.\n */\n error ERC2612ExpiredSignature(uint256 deadline);\n\n /**\n * @dev Mismatched signature.\n */\n error ERC2612InvalidSigner(address signer, address owner);\n\n /**\n * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `\"1\"`.\n *\n * It's a good idea to use the same `name` that is defined as the ERC-20 token name.\n */\n constructor(string memory name) EIP712(name, \"1\") {}\n\n /**\n * @inheritdoc IERC20Permit\n */\n function permit(\n address owner,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public virtual {\n if (block.timestamp > deadline) {\n revert ERC2612ExpiredSignature(deadline);\n }\n\n bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));\n\n bytes32 hash = _hashTypedDataV4(structHash);\n\n address signer = ECDSA.recover(hash, v, r, s);\n if (signer != owner) {\n revert ERC2612InvalidSigner(signer, owner);\n }\n\n _approve(owner, spender, value);\n }\n\n /**\n * @inheritdoc IERC20Permit\n */\n function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) {\n return super.nonces(owner);\n }\n\n /**\n * @inheritdoc IERC20Permit\n */\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {\n return _domainSeparatorV4();\n }\n}\n"},"@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\n\n/**\n * @dev Interface for the optional metadata functions from the ERC-20 standard.\n */\ninterface IERC20Metadata is IERC20 {\n /**\n * @dev Returns the name of the token.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the symbol of the token.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the decimals places of the token.\n */\n function decimals() external view returns (uint8);\n}\n"},"@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Permit.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in\n * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].\n *\n * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by\n * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't\n * need to send a transaction, and thus is not required to hold Ether at all.\n *\n * ==== Security Considerations\n *\n * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature\n * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be\n * considered as an intention to spend the allowance in any specific way. The second is that because permits have\n * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should\n * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be\n * generally recommended is:\n *\n * ```solidity\n * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public {\n * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {}\n * doThing(..., value);\n * }\n *\n * function doThing(..., uint256 value) public {\n * token.safeTransferFrom(msg.sender, address(this), value);\n * ...\n * }\n * ```\n *\n * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of\n * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also\n * {SafeERC20-safeTransferFrom}).\n *\n * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so\n * contracts should have entry points that don't rely on permit.\n */\ninterface IERC20Permit {\n /**\n * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,\n * given ``owner``'s signed approval.\n *\n * IMPORTANT: The same issues {IERC20-approve} has related to transaction\n * ordering also apply here.\n *\n * Emits an {Approval} event.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n * - `deadline` must be a timestamp in the future.\n * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`\n * over the EIP712-formatted function arguments.\n * - the signature must use ``owner``'s current nonce (see {nonces}).\n *\n * For more information on the signature format, see the\n * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP\n * section].\n *\n * CAUTION: See Security Considerations above.\n */\n function permit(\n address owner,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev Returns the current nonce for `owner`. This value must be\n * included whenever a signature is generated for {permit}.\n *\n * Every successful call to {permit} increases ``owner``'s nonce by one. This\n * prevents a signature from being used multiple times.\n */\n function nonces(address owner) external view returns (uint256);\n\n /**\n * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.\n */\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32);\n}\n"},"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\nimport {IERC1363} from \"../../../interfaces/IERC1363.sol\";\n\n/**\n * @title SafeERC20\n * @dev Wrappers around ERC-20 operations that throw on failure (when the token\n * contract returns false). Tokens that return no value (and instead revert or\n * throw on failure) are also supported, non-reverting calls are assumed to be\n * successful.\n * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,\n * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.\n */\nlibrary SafeERC20 {\n /**\n * @dev An operation with an ERC-20 token failed.\n */\n error SafeERC20FailedOperation(address token);\n\n /**\n * @dev Indicates a failed `decreaseAllowance` request.\n */\n error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);\n\n /**\n * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n */\n function safeTransfer(IERC20 token, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the\n * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.\n */\n function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {\n uint256 oldAllowance = token.allowance(address(this), spender);\n forceApprove(token, spender, oldAllowance + value);\n }\n\n /**\n * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no\n * value, non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {\n unchecked {\n uint256 currentAllowance = token.allowance(address(this), spender);\n if (currentAllowance < requestedDecrease) {\n revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);\n }\n forceApprove(token, spender, currentAllowance - requestedDecrease);\n }\n }\n\n /**\n * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval\n * to be set to zero before setting it to a non-zero value, such as USDT.\n *\n * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function\n * only sets the \"standard\" allowance. Any temporary allowance will remain active, in addition to the value being\n * set here.\n */\n function forceApprove(IERC20 token, address spender, uint256 value) internal {\n bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));\n\n if (!_callOptionalReturnBool(token, approvalCall)) {\n _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));\n _callOptionalReturn(token, approvalCall);\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n safeTransfer(token, to, value);\n } else if (!token.transferAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target\n * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferFromAndCallRelaxed(\n IERC1363 token,\n address from,\n address to,\n uint256 value,\n bytes memory data\n ) internal {\n if (to.code.length == 0) {\n safeTransferFrom(token, from, to, value);\n } else if (!token.transferFromAndCall(from, to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.\n * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}\n * once without retrying, and relies on the returned value to be true.\n *\n * Reverts if the returned value is other than `true`.\n */\n function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n forceApprove(token, to, value);\n } else if (!token.approveAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.\n */\n function _callOptionalReturn(IERC20 token, bytes memory data) private {\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n // bubble errors\n if iszero(success) {\n let ptr := mload(0x40)\n returndatacopy(ptr, 0, returndatasize())\n revert(ptr, returndatasize())\n }\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n\n if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.\n */\n function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {\n bool success;\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);\n }\n}\n"},"@openzeppelin/contracts/utils/Address.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.2.0) (utils/Address.sol)\n\npragma solidity ^0.8.20;\n\nimport {Errors} from \"./Errors.sol\";\n\n/**\n * @dev Collection of functions related to the address type\n */\nlibrary Address {\n /**\n * @dev There's no code at `target` (it is not a contract).\n */\n error AddressEmptyCode(address target);\n\n /**\n * @dev Replacement for Solidity's `transfer`: sends `amount` wei to\n * `recipient`, forwarding all available gas and reverting on errors.\n *\n * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost\n * of certain opcodes, possibly making contracts go over the 2300 gas limit\n * imposed by `transfer`, making them unable to receive funds via\n * `transfer`. {sendValue} removes this limitation.\n *\n * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].\n *\n * IMPORTANT: because control is transferred to `recipient`, care must be\n * taken to not create reentrancy vulnerabilities. Consider using\n * {ReentrancyGuard} or the\n * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].\n */\n function sendValue(address payable recipient, uint256 amount) internal {\n if (address(this).balance < amount) {\n revert Errors.InsufficientBalance(address(this).balance, amount);\n }\n\n (bool success, bytes memory returndata) = recipient.call{value: amount}(\"\");\n if (!success) {\n _revert(returndata);\n }\n }\n\n /**\n * @dev Performs a Solidity function call using a low level `call`. A\n * plain `call` is an unsafe replacement for a function call: use this\n * function instead.\n *\n * If `target` reverts with a revert reason or custom error, it is bubbled\n * up by this function (like regular Solidity function calls). However, if\n * the call reverted with no returned reason, this function reverts with a\n * {Errors.FailedCall} error.\n *\n * Returns the raw returned data. To convert to the expected return value,\n * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].\n *\n * Requirements:\n *\n * - `target` must be a contract.\n * - calling `target` with `data` must not revert.\n */\n function functionCall(address target, bytes memory data) internal returns (bytes memory) {\n return functionCallWithValue(target, data, 0);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but also transferring `value` wei to `target`.\n *\n * Requirements:\n *\n * - the calling contract must have an ETH balance of at least `value`.\n * - the called Solidity function must be `payable`.\n */\n function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {\n if (address(this).balance < value) {\n revert Errors.InsufficientBalance(address(this).balance, value);\n }\n (bool success, bytes memory returndata) = target.call{value: value}(data);\n return verifyCallResultFromTarget(target, success, returndata);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but performing a static call.\n */\n function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {\n (bool success, bytes memory returndata) = target.staticcall(data);\n return verifyCallResultFromTarget(target, success, returndata);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but performing a delegate call.\n */\n function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {\n (bool success, bytes memory returndata) = target.delegatecall(data);\n return verifyCallResultFromTarget(target, success, returndata);\n }\n\n /**\n * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target\n * was not a contract or bubbling up the revert reason (falling back to {Errors.FailedCall}) in case\n * of an unsuccessful call.\n */\n function verifyCallResultFromTarget(\n address target,\n bool success,\n bytes memory returndata\n ) internal view returns (bytes memory) {\n if (!success) {\n _revert(returndata);\n } else {\n // only check if target is a contract if the call was successful and the return data is empty\n // otherwise we already know that it was a contract\n if (returndata.length == 0 && target.code.length == 0) {\n revert AddressEmptyCode(target);\n }\n return returndata;\n }\n }\n\n /**\n * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the\n * revert reason or with a default {Errors.FailedCall} error.\n */\n function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {\n if (!success) {\n _revert(returndata);\n } else {\n return returndata;\n }\n }\n\n /**\n * @dev Reverts with returndata if present. Otherwise reverts with {Errors.FailedCall}.\n */\n function _revert(bytes memory returndata) private pure {\n // Look for revert reason and bubble it up if present\n if (returndata.length > 0) {\n // The easiest way to bubble the revert reason is using memory via assembly\n assembly (\"memory-safe\") {\n let returndata_size := mload(returndata)\n revert(add(32, returndata), returndata_size)\n }\n } else {\n revert Errors.FailedCall();\n }\n }\n}\n"},"@openzeppelin/contracts/utils/Context.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract Context {\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n\n function _msgData() internal view virtual returns (bytes calldata) {\n return msg.data;\n }\n\n function _contextSuffixLength() internal view virtual returns (uint256) {\n return 0;\n }\n}\n"},"@openzeppelin/contracts/utils/Errors.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Errors.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Collection of common custom errors used in multiple contracts\n *\n * IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.\n * It is recommended to avoid relying on the error API for critical functionality.\n *\n * _Available since v5.1._\n */\nlibrary Errors {\n /**\n * @dev The ETH balance of the account is not enough to perform the operation.\n */\n error InsufficientBalance(uint256 balance, uint256 needed);\n\n /**\n * @dev A call to an address target failed. The target may have reverted.\n */\n error FailedCall();\n\n /**\n * @dev The deployment failed.\n */\n error FailedDeployment();\n\n /**\n * @dev A necessary precompile is missing.\n */\n error MissingPrecompile(address);\n}\n"},"@openzeppelin/contracts/utils/Nonces.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (utils/Nonces.sol)\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides tracking nonces for addresses. Nonces will only increment.\n */\nabstract contract Nonces {\n /**\n * @dev The nonce used for an `account` is not the expected current nonce.\n */\n error InvalidAccountNonce(address account, uint256 currentNonce);\n\n mapping(address account => uint256) private _nonces;\n\n /**\n * @dev Returns the next unused nonce for an address.\n */\n function nonces(address owner) public view virtual returns (uint256) {\n return _nonces[owner];\n }\n\n /**\n * @dev Consumes a nonce.\n *\n * Returns the current value and increments nonce.\n */\n function _useNonce(address owner) internal virtual returns (uint256) {\n // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be\n // decremented or reset. This guarantees that the nonce never overflows.\n unchecked {\n // It is important to do x++ and not ++x here.\n return _nonces[owner]++;\n }\n }\n\n /**\n * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`.\n */\n function _useCheckedNonce(address owner, uint256 nonce) internal virtual {\n uint256 current = _useNonce(owner);\n if (nonce != current) {\n revert InvalidAccountNonce(owner, current);\n }\n }\n}\n"},"@openzeppelin/contracts/utils/Panic.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Helper library for emitting standardized panic codes.\n *\n * ```solidity\n * contract Example {\n * using Panic for uint256;\n *\n * // Use any of the declared internal constants\n * function foo() { Panic.GENERIC.panic(); }\n *\n * // Alternatively\n * function foo() { Panic.panic(Panic.GENERIC); }\n * }\n * ```\n *\n * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil].\n *\n * _Available since v5.1._\n */\n// slither-disable-next-line unused-state\nlibrary Panic {\n /// @dev generic / unspecified error\n uint256 internal constant GENERIC = 0x00;\n /// @dev used by the assert() builtin\n uint256 internal constant ASSERT = 0x01;\n /// @dev arithmetic underflow or overflow\n uint256 internal constant UNDER_OVERFLOW = 0x11;\n /// @dev division or modulo by zero\n uint256 internal constant DIVISION_BY_ZERO = 0x12;\n /// @dev enum conversion error\n uint256 internal constant ENUM_CONVERSION_ERROR = 0x21;\n /// @dev invalid encoding in storage\n uint256 internal constant STORAGE_ENCODING_ERROR = 0x22;\n /// @dev empty array pop\n uint256 internal constant EMPTY_ARRAY_POP = 0x31;\n /// @dev array out of bounds access\n uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32;\n /// @dev resource error (too large allocation or too large array)\n uint256 internal constant RESOURCE_ERROR = 0x41;\n /// @dev calling invalid internal function\n uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51;\n\n /// @dev Reverts with a panic code. Recommended to use with\n /// the internal constants with predefined codes.\n function panic(uint256 code) internal pure {\n assembly (\"memory-safe\") {\n mstore(0x00, 0x4e487b71)\n mstore(0x20, code)\n revert(0x1c, 0x24)\n }\n }\n}\n"},"@openzeppelin/contracts/utils/Pausable.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/Pausable.sol)\n\npragma solidity ^0.8.20;\n\nimport {Context} from \"../utils/Context.sol\";\n\n/**\n * @dev Contract module which allows children to implement an emergency stop\n * mechanism that can be triggered by an authorized account.\n *\n * This module is used through inheritance. It will make available the\n * modifiers `whenNotPaused` and `whenPaused`, which can be applied to\n * the functions of your contract. Note that they will not be pausable by\n * simply including this module, only once the modifiers are put in place.\n */\nabstract contract Pausable is Context {\n bool private _paused;\n\n /**\n * @dev Emitted when the pause is triggered by `account`.\n */\n event Paused(address account);\n\n /**\n * @dev Emitted when the pause is lifted by `account`.\n */\n event Unpaused(address account);\n\n /**\n * @dev The operation failed because the contract is paused.\n */\n error EnforcedPause();\n\n /**\n * @dev The operation failed because the contract is not paused.\n */\n error ExpectedPause();\n\n /**\n * @dev Modifier to make a function callable only when the contract is not paused.\n *\n * Requirements:\n *\n * - The contract must not be paused.\n */\n modifier whenNotPaused() {\n _requireNotPaused();\n _;\n }\n\n /**\n * @dev Modifier to make a function callable only when the contract is paused.\n *\n * Requirements:\n *\n * - The contract must be paused.\n */\n modifier whenPaused() {\n _requirePaused();\n _;\n }\n\n /**\n * @dev Returns true if the contract is paused, and false otherwise.\n */\n function paused() public view virtual returns (bool) {\n return _paused;\n }\n\n /**\n * @dev Throws if the contract is paused.\n */\n function _requireNotPaused() internal view virtual {\n if (paused()) {\n revert EnforcedPause();\n }\n }\n\n /**\n * @dev Throws if the contract is not paused.\n */\n function _requirePaused() internal view virtual {\n if (!paused()) {\n revert ExpectedPause();\n }\n }\n\n /**\n * @dev Triggers stopped state.\n *\n * Requirements:\n *\n * - The contract must not be paused.\n */\n function _pause() internal virtual whenNotPaused {\n _paused = true;\n emit Paused(_msgSender());\n }\n\n /**\n * @dev Returns to normal state.\n *\n * Requirements:\n *\n * - The contract must be paused.\n */\n function _unpause() internal virtual whenPaused {\n _paused = false;\n emit Unpaused(_msgSender());\n }\n}\n"},"@openzeppelin/contracts/utils/ReentrancyGuard.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Contract module that helps prevent reentrant calls to a function.\n *\n * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier\n * available, which can be applied to functions to make sure there are no nested\n * (reentrant) calls to them.\n *\n * Note that because there is a single `nonReentrant` guard, functions marked as\n * `nonReentrant` may not call one another. This can be worked around by making\n * those functions `private`, and then adding `external` `nonReentrant` entry\n * points to them.\n *\n * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,\n * consider using {ReentrancyGuardTransient} instead.\n *\n * TIP: If you would like to learn more about reentrancy and alternative ways\n * to protect against it, check out our blog post\n * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].\n */\nabstract contract ReentrancyGuard {\n // Booleans are more expensive than uint256 or any type that takes up a full\n // word because each write operation emits an extra SLOAD to first read the\n // slot's contents, replace the bits taken up by the boolean, and then write\n // back. This is the compiler's defense against contract upgrades and\n // pointer aliasing, and it cannot be disabled.\n\n // The values being non-zero value makes deployment a bit more expensive,\n // but in exchange the refund on every call to nonReentrant will be lower in\n // amount. Since refunds are capped to a percentage of the total\n // transaction's gas, it is best to keep them low in cases like this one, to\n // increase the likelihood of the full refund coming into effect.\n uint256 private constant NOT_ENTERED = 1;\n uint256 private constant ENTERED = 2;\n\n uint256 private _status;\n\n /**\n * @dev Unauthorized reentrant call.\n */\n error ReentrancyGuardReentrantCall();\n\n constructor() {\n _status = NOT_ENTERED;\n }\n\n /**\n * @dev Prevents a contract from calling itself, directly or indirectly.\n * Calling a `nonReentrant` function from another `nonReentrant`\n * function is not supported. It is possible to prevent this from happening\n * by making the `nonReentrant` function external, and making it call a\n * `private` function that does the actual work.\n */\n modifier nonReentrant() {\n _nonReentrantBefore();\n _;\n _nonReentrantAfter();\n }\n\n function _nonReentrantBefore() private {\n // On the first call to nonReentrant, _status will be NOT_ENTERED\n if (_status == ENTERED) {\n revert ReentrancyGuardReentrantCall();\n }\n\n // Any calls to nonReentrant after this point will fail\n _status = ENTERED;\n }\n\n function _nonReentrantAfter() private {\n // By storing the original value once again, a refund is triggered (see\n // https://eips.ethereum.org/EIPS/eip-2200)\n _status = NOT_ENTERED;\n }\n\n /**\n * @dev Returns true if the reentrancy guard is currently set to \"entered\", which indicates there is a\n * `nonReentrant` function in the call stack.\n */\n function _reentrancyGuardEntered() internal view returns (bool) {\n return _status == ENTERED;\n }\n}\n"},"@openzeppelin/contracts/utils/ShortStrings.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/ShortStrings.sol)\n\npragma solidity ^0.8.20;\n\nimport {StorageSlot} from \"./StorageSlot.sol\";\n\n// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |\n// | length | 0x BB |\ntype ShortString is bytes32;\n\n/**\n * @dev This library provides functions to convert short memory strings\n * into a `ShortString` type that can be used as an immutable variable.\n *\n * Strings of arbitrary length can be optimized using this library if\n * they are short enough (up to 31 bytes) by packing them with their\n * length (1 byte) in a single EVM word (32 bytes). Additionally, a\n * fallback mechanism can be used for every other case.\n *\n * Usage example:\n *\n * ```solidity\n * contract Named {\n * using ShortStrings for *;\n *\n * ShortString private immutable _name;\n * string private _nameFallback;\n *\n * constructor(string memory contractName) {\n * _name = contractName.toShortStringWithFallback(_nameFallback);\n * }\n *\n * function name() external view returns (string memory) {\n * return _name.toStringWithFallback(_nameFallback);\n * }\n * }\n * ```\n */\nlibrary ShortStrings {\n // Used as an identifier for strings longer than 31 bytes.\n bytes32 private constant FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF;\n\n error StringTooLong(string str);\n error InvalidShortString();\n\n /**\n * @dev Encode a string of at most 31 chars into a `ShortString`.\n *\n * This will trigger a `StringTooLong` error is the input string is too long.\n */\n function toShortString(string memory str) internal pure returns (ShortString) {\n bytes memory bstr = bytes(str);\n if (bstr.length > 31) {\n revert StringTooLong(str);\n }\n return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length));\n }\n\n /**\n * @dev Decode a `ShortString` back to a \"normal\" string.\n */\n function toString(ShortString sstr) internal pure returns (string memory) {\n uint256 len = byteLength(sstr);\n // using `new string(len)` would work locally but is not memory safe.\n string memory str = new string(32);\n assembly (\"memory-safe\") {\n mstore(str, len)\n mstore(add(str, 0x20), sstr)\n }\n return str;\n }\n\n /**\n * @dev Return the length of a `ShortString`.\n */\n function byteLength(ShortString sstr) internal pure returns (uint256) {\n uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF;\n if (result > 31) {\n revert InvalidShortString();\n }\n return result;\n }\n\n /**\n * @dev Encode a string into a `ShortString`, or write it to storage if it is too long.\n */\n function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) {\n if (bytes(value).length < 32) {\n return toShortString(value);\n } else {\n StorageSlot.getStringSlot(store).value = value;\n return ShortString.wrap(FALLBACK_SENTINEL);\n }\n }\n\n /**\n * @dev Decode a string that was encoded to `ShortString` or written to storage using {toShortStringWithFallback}.\n */\n function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) {\n if (ShortString.unwrap(value) != FALLBACK_SENTINEL) {\n return toString(value);\n } else {\n return store;\n }\n }\n\n /**\n * @dev Return the length of a string that was encoded to `ShortString` or written to storage using\n * {toShortStringWithFallback}.\n *\n * WARNING: This will return the \"byte length\" of the string. This may not reflect the actual length in terms of\n * actual characters as the UTF-8 encoding of a single character can span over multiple bytes.\n */\n function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) {\n if (ShortString.unwrap(value) != FALLBACK_SENTINEL) {\n return byteLength(value);\n } else {\n return bytes(store).length;\n }\n }\n}\n"},"@openzeppelin/contracts/utils/StorageSlot.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)\n// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for reading and writing primitive types to specific storage slots.\n *\n * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.\n * This library helps with reading and writing to such slots without the need for inline assembly.\n *\n * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.\n *\n * Example usage to set ERC-1967 implementation slot:\n * ```solidity\n * contract ERC1967 {\n * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.\n * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\n *\n * function _getImplementation() internal view returns (address) {\n * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;\n * }\n *\n * function _setImplementation(address newImplementation) internal {\n * require(newImplementation.code.length > 0);\n * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {SlotDerivation}.\n */\nlibrary StorageSlot {\n struct AddressSlot {\n address value;\n }\n\n struct BooleanSlot {\n bool value;\n }\n\n struct Bytes32Slot {\n bytes32 value;\n }\n\n struct Uint256Slot {\n uint256 value;\n }\n\n struct Int256Slot {\n int256 value;\n }\n\n struct StringSlot {\n string value;\n }\n\n struct BytesSlot {\n bytes value;\n }\n\n /**\n * @dev Returns an `AddressSlot` with member `value` located at `slot`.\n */\n function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `BooleanSlot` with member `value` located at `slot`.\n */\n function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.\n */\n function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Uint256Slot` with member `value` located at `slot`.\n */\n function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Int256Slot` with member `value` located at `slot`.\n */\n function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `StringSlot` with member `value` located at `slot`.\n */\n function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `StringSlot` representation of the string storage pointer `store`.\n */\n function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n\n /**\n * @dev Returns a `BytesSlot` with member `value` located at `slot`.\n */\n function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.\n */\n function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n}\n"},"@openzeppelin/contracts/utils/Strings.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/Strings.sol)\n\npragma solidity ^0.8.20;\n\nimport {Math} from \"./math/Math.sol\";\nimport {SafeCast} from \"./math/SafeCast.sol\";\nimport {SignedMath} from \"./math/SignedMath.sol\";\n\n/**\n * @dev String operations.\n */\nlibrary Strings {\n using SafeCast for *;\n\n bytes16 private constant HEX_DIGITS = \"0123456789abcdef\";\n uint8 private constant ADDRESS_LENGTH = 20;\n uint256 private constant SPECIAL_CHARS_LOOKUP =\n (1 << 0x08) | // backspace\n (1 << 0x09) | // tab\n (1 << 0x0a) | // newline\n (1 << 0x0c) | // form feed\n (1 << 0x0d) | // carriage return\n (1 << 0x22) | // double quote\n (1 << 0x5c); // backslash\n\n /**\n * @dev The `value` string doesn't fit in the specified `length`.\n */\n error StringsInsufficientHexLength(uint256 value, uint256 length);\n\n /**\n * @dev The string being parsed contains characters that are not in scope of the given base.\n */\n error StringsInvalidChar();\n\n /**\n * @dev The string being parsed is not a properly formatted address.\n */\n error StringsInvalidAddressFormat();\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` decimal representation.\n */\n function toString(uint256 value) internal pure returns (string memory) {\n unchecked {\n uint256 length = Math.log10(value) + 1;\n string memory buffer = new string(length);\n uint256 ptr;\n assembly (\"memory-safe\") {\n ptr := add(buffer, add(32, length))\n }\n while (true) {\n ptr--;\n assembly (\"memory-safe\") {\n mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))\n }\n value /= 10;\n if (value == 0) break;\n }\n return buffer;\n }\n }\n\n /**\n * @dev Converts a `int256` to its ASCII `string` decimal representation.\n */\n function toStringSigned(int256 value) internal pure returns (string memory) {\n return string.concat(value < 0 ? \"-\" : \"\", toString(SignedMath.abs(value)));\n }\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.\n */\n function toHexString(uint256 value) internal pure returns (string memory) {\n unchecked {\n return toHexString(value, Math.log256(value) + 1);\n }\n }\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.\n */\n function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {\n uint256 localValue = value;\n bytes memory buffer = new bytes(2 * length + 2);\n buffer[0] = \"0\";\n buffer[1] = \"x\";\n for (uint256 i = 2 * length + 1; i > 1; --i) {\n buffer[i] = HEX_DIGITS[localValue & 0xf];\n localValue >>= 4;\n }\n if (localValue != 0) {\n revert StringsInsufficientHexLength(value, length);\n }\n return string(buffer);\n }\n\n /**\n * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal\n * representation.\n */\n function toHexString(address addr) internal pure returns (string memory) {\n return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);\n }\n\n /**\n * @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal\n * representation, according to EIP-55.\n */\n function toChecksumHexString(address addr) internal pure returns (string memory) {\n bytes memory buffer = bytes(toHexString(addr));\n\n // hash the hex part of buffer (skip length + 2 bytes, length 40)\n uint256 hashValue;\n assembly (\"memory-safe\") {\n hashValue := shr(96, keccak256(add(buffer, 0x22), 40))\n }\n\n for (uint256 i = 41; i > 1; --i) {\n // possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f)\n if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) {\n // case shift by xoring with 0x20\n buffer[i] ^= 0x20;\n }\n hashValue >>= 4;\n }\n return string(buffer);\n }\n\n /**\n * @dev Returns true if the two strings are equal.\n */\n function equal(string memory a, string memory b) internal pure returns (bool) {\n return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));\n }\n\n /**\n * @dev Parse a decimal string and returns the value as a `uint256`.\n *\n * Requirements:\n * - The string must be formatted as `[0-9]*`\n * - The result must fit into an `uint256` type\n */\n function parseUint(string memory input) internal pure returns (uint256) {\n return parseUint(input, 0, bytes(input).length);\n }\n\n /**\n * @dev Variant of {parseUint-string} that parses a substring of `input` located between position `begin` (included) and\n * `end` (excluded).\n *\n * Requirements:\n * - The substring must be formatted as `[0-9]*`\n * - The result must fit into an `uint256` type\n */\n function parseUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) {\n (bool success, uint256 value) = tryParseUint(input, begin, end);\n if (!success) revert StringsInvalidChar();\n return value;\n }\n\n /**\n * @dev Variant of {parseUint-string} that returns false if the parsing fails because of an invalid character.\n *\n * NOTE: This function will revert if the result does not fit in a `uint256`.\n */\n function tryParseUint(string memory input) internal pure returns (bool success, uint256 value) {\n return _tryParseUintUncheckedBounds(input, 0, bytes(input).length);\n }\n\n /**\n * @dev Variant of {parseUint-string-uint256-uint256} that returns false if the parsing fails because of an invalid\n * character.\n *\n * NOTE: This function will revert if the result does not fit in a `uint256`.\n */\n function tryParseUint(\n string memory input,\n uint256 begin,\n uint256 end\n ) internal pure returns (bool success, uint256 value) {\n if (end > bytes(input).length || begin > end) return (false, 0);\n return _tryParseUintUncheckedBounds(input, begin, end);\n }\n\n /**\n * @dev Implementation of {tryParseUint-string-uint256-uint256} that does not check bounds. Caller should make sure that\n * `begin <= end <= input.length`. Other inputs would result in undefined behavior.\n */\n function _tryParseUintUncheckedBounds(\n string memory input,\n uint256 begin,\n uint256 end\n ) private pure returns (bool success, uint256 value) {\n bytes memory buffer = bytes(input);\n\n uint256 result = 0;\n for (uint256 i = begin; i < end; ++i) {\n uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i)));\n if (chr > 9) return (false, 0);\n result *= 10;\n result += chr;\n }\n return (true, result);\n }\n\n /**\n * @dev Parse a decimal string and returns the value as a `int256`.\n *\n * Requirements:\n * - The string must be formatted as `[-+]?[0-9]*`\n * - The result must fit in an `int256` type.\n */\n function parseInt(string memory input) internal pure returns (int256) {\n return parseInt(input, 0, bytes(input).length);\n }\n\n /**\n * @dev Variant of {parseInt-string} that parses a substring of `input` located between position `begin` (included) and\n * `end` (excluded).\n *\n * Requirements:\n * - The substring must be formatted as `[-+]?[0-9]*`\n * - The result must fit in an `int256` type.\n */\n function parseInt(string memory input, uint256 begin, uint256 end) internal pure returns (int256) {\n (bool success, int256 value) = tryParseInt(input, begin, end);\n if (!success) revert StringsInvalidChar();\n return value;\n }\n\n /**\n * @dev Variant of {parseInt-string} that returns false if the parsing fails because of an invalid character or if\n * the result does not fit in a `int256`.\n *\n * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`.\n */\n function tryParseInt(string memory input) internal pure returns (bool success, int256 value) {\n return _tryParseIntUncheckedBounds(input, 0, bytes(input).length);\n }\n\n uint256 private constant ABS_MIN_INT256 = 2 ** 255;\n\n /**\n * @dev Variant of {parseInt-string-uint256-uint256} that returns false if the parsing fails because of an invalid\n * character or if the result does not fit in a `int256`.\n *\n * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`.\n */\n function tryParseInt(\n string memory input,\n uint256 begin,\n uint256 end\n ) internal pure returns (bool success, int256 value) {\n if (end > bytes(input).length || begin > end) return (false, 0);\n return _tryParseIntUncheckedBounds(input, begin, end);\n }\n\n /**\n * @dev Implementation of {tryParseInt-string-uint256-uint256} that does not check bounds. Caller should make sure that\n * `begin <= end <= input.length`. Other inputs would result in undefined behavior.\n */\n function _tryParseIntUncheckedBounds(\n string memory input,\n uint256 begin,\n uint256 end\n ) private pure returns (bool success, int256 value) {\n bytes memory buffer = bytes(input);\n\n // Check presence of a negative sign.\n bytes1 sign = begin == end ? bytes1(0) : bytes1(_unsafeReadBytesOffset(buffer, begin)); // don't do out-of-bound (possibly unsafe) read if sub-string is empty\n bool positiveSign = sign == bytes1(\"+\");\n bool negativeSign = sign == bytes1(\"-\");\n uint256 offset = (positiveSign || negativeSign).toUint();\n\n (bool absSuccess, uint256 absValue) = tryParseUint(input, begin + offset, end);\n\n if (absSuccess && absValue < ABS_MIN_INT256) {\n return (true, negativeSign ? -int256(absValue) : int256(absValue));\n } else if (absSuccess && negativeSign && absValue == ABS_MIN_INT256) {\n return (true, type(int256).min);\n } else return (false, 0);\n }\n\n /**\n * @dev Parse a hexadecimal string (with or without \"0x\" prefix), and returns the value as a `uint256`.\n *\n * Requirements:\n * - The string must be formatted as `(0x)?[0-9a-fA-F]*`\n * - The result must fit in an `uint256` type.\n */\n function parseHexUint(string memory input) internal pure returns (uint256) {\n return parseHexUint(input, 0, bytes(input).length);\n }\n\n /**\n * @dev Variant of {parseHexUint-string} that parses a substring of `input` located between position `begin` (included) and\n * `end` (excluded).\n *\n * Requirements:\n * - The substring must be formatted as `(0x)?[0-9a-fA-F]*`\n * - The result must fit in an `uint256` type.\n */\n function parseHexUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) {\n (bool success, uint256 value) = tryParseHexUint(input, begin, end);\n if (!success) revert StringsInvalidChar();\n return value;\n }\n\n /**\n * @dev Variant of {parseHexUint-string} that returns false if the parsing fails because of an invalid character.\n *\n * NOTE: This function will revert if the result does not fit in a `uint256`.\n */\n function tryParseHexUint(string memory input) internal pure returns (bool success, uint256 value) {\n return _tryParseHexUintUncheckedBounds(input, 0, bytes(input).length);\n }\n\n /**\n * @dev Variant of {parseHexUint-string-uint256-uint256} that returns false if the parsing fails because of an\n * invalid character.\n *\n * NOTE: This function will revert if the result does not fit in a `uint256`.\n */\n function tryParseHexUint(\n string memory input,\n uint256 begin,\n uint256 end\n ) internal pure returns (bool success, uint256 value) {\n if (end > bytes(input).length || begin > end) return (false, 0);\n return _tryParseHexUintUncheckedBounds(input, begin, end);\n }\n\n /**\n * @dev Implementation of {tryParseHexUint-string-uint256-uint256} that does not check bounds. Caller should make sure that\n * `begin <= end <= input.length`. Other inputs would result in undefined behavior.\n */\n function _tryParseHexUintUncheckedBounds(\n string memory input,\n uint256 begin,\n uint256 end\n ) private pure returns (bool success, uint256 value) {\n bytes memory buffer = bytes(input);\n\n // skip 0x prefix if present\n bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2(\"0x\"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty\n uint256 offset = hasPrefix.toUint() * 2;\n\n uint256 result = 0;\n for (uint256 i = begin + offset; i < end; ++i) {\n uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i)));\n if (chr > 15) return (false, 0);\n result *= 16;\n unchecked {\n // Multiplying by 16 is equivalent to a shift of 4 bits (with additional overflow check).\n // This guarantees that adding a value < 16 will not cause an overflow, hence the unchecked.\n result += chr;\n }\n }\n return (true, result);\n }\n\n /**\n * @dev Parse a hexadecimal string (with or without \"0x\" prefix), and returns the value as an `address`.\n *\n * Requirements:\n * - The string must be formatted as `(0x)?[0-9a-fA-F]{40}`\n */\n function parseAddress(string memory input) internal pure returns (address) {\n return parseAddress(input, 0, bytes(input).length);\n }\n\n /**\n * @dev Variant of {parseAddress-string} that parses a substring of `input` located between position `begin` (included) and\n * `end` (excluded).\n *\n * Requirements:\n * - The substring must be formatted as `(0x)?[0-9a-fA-F]{40}`\n */\n function parseAddress(string memory input, uint256 begin, uint256 end) internal pure returns (address) {\n (bool success, address value) = tryParseAddress(input, begin, end);\n if (!success) revert StringsInvalidAddressFormat();\n return value;\n }\n\n /**\n * @dev Variant of {parseAddress-string} that returns false if the parsing fails because the input is not a properly\n * formatted address. See {parseAddress-string} requirements.\n */\n function tryParseAddress(string memory input) internal pure returns (bool success, address value) {\n return tryParseAddress(input, 0, bytes(input).length);\n }\n\n /**\n * @dev Variant of {parseAddress-string-uint256-uint256} that returns false if the parsing fails because input is not a properly\n * formatted address. See {parseAddress-string-uint256-uint256} requirements.\n */\n function tryParseAddress(\n string memory input,\n uint256 begin,\n uint256 end\n ) internal pure returns (bool success, address value) {\n if (end > bytes(input).length || begin > end) return (false, address(0));\n\n bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2(\"0x\"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty\n uint256 expectedLength = 40 + hasPrefix.toUint() * 2;\n\n // check that input is the correct length\n if (end - begin == expectedLength) {\n // length guarantees that this does not overflow, and value is at most type(uint160).max\n (bool s, uint256 v) = _tryParseHexUintUncheckedBounds(input, begin, end);\n return (s, address(uint160(v)));\n } else {\n return (false, address(0));\n }\n }\n\n function _tryParseChr(bytes1 chr) private pure returns (uint8) {\n uint8 value = uint8(chr);\n\n // Try to parse `chr`:\n // - Case 1: [0-9]\n // - Case 2: [a-f]\n // - Case 3: [A-F]\n // - otherwise not supported\n unchecked {\n if (value > 47 && value < 58) value -= 48;\n else if (value > 96 && value < 103) value -= 87;\n else if (value > 64 && value < 71) value -= 55;\n else return type(uint8).max;\n }\n\n return value;\n }\n\n /**\n * @dev Escape special characters in JSON strings. This can be useful to prevent JSON injection in NFT metadata.\n *\n * WARNING: This function should only be used in double quoted JSON strings. Single quotes are not escaped.\n *\n * NOTE: This function escapes all unicode characters, and not just the ones in ranges defined in section 2.5 of\n * RFC-4627 (U+0000 to U+001F, U+0022 and U+005C). ECMAScript's `JSON.parse` does recover escaped unicode\n * characters that are not in this range, but other tooling may provide different results.\n */\n function escapeJSON(string memory input) internal pure returns (string memory) {\n bytes memory buffer = bytes(input);\n bytes memory output = new bytes(2 * buffer.length); // worst case scenario\n uint256 outputLength = 0;\n\n for (uint256 i; i < buffer.length; ++i) {\n bytes1 char = bytes1(_unsafeReadBytesOffset(buffer, i));\n if (((SPECIAL_CHARS_LOOKUP & (1 << uint8(char))) != 0)) {\n output[outputLength++] = \"\\\\\";\n if (char == 0x08) output[outputLength++] = \"b\";\n else if (char == 0x09) output[outputLength++] = \"t\";\n else if (char == 0x0a) output[outputLength++] = \"n\";\n else if (char == 0x0c) output[outputLength++] = \"f\";\n else if (char == 0x0d) output[outputLength++] = \"r\";\n else if (char == 0x5c) output[outputLength++] = \"\\\\\";\n else if (char == 0x22) {\n // solhint-disable-next-line quotes\n output[outputLength++] = '\"';\n }\n } else {\n output[outputLength++] = char;\n }\n }\n // write the actual length and deallocate unused memory\n assembly (\"memory-safe\") {\n mstore(output, outputLength)\n mstore(0x40, add(output, shl(5, shr(5, add(outputLength, 63)))))\n }\n\n return string(output);\n }\n\n /**\n * @dev Reads a bytes32 from a bytes array without bounds checking.\n *\n * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the\n * assembly block as such would prevent some optimizations.\n */\n function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) {\n // This is not memory safe in the general case, but all calls to this private function are within bounds.\n assembly (\"memory-safe\") {\n value := mload(add(buffer, add(0x20, offset)))\n }\n }\n}\n"},"@openzeppelin/contracts/utils/cryptography/ECDSA.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/ECDSA.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.\n *\n * These functions can be used to verify that a message was signed by the holder\n * of the private keys of a given address.\n */\nlibrary ECDSA {\n enum RecoverError {\n NoError,\n InvalidSignature,\n InvalidSignatureLength,\n InvalidSignatureS\n }\n\n /**\n * @dev The signature derives the `address(0)`.\n */\n error ECDSAInvalidSignature();\n\n /**\n * @dev The signature has an invalid length.\n */\n error ECDSAInvalidSignatureLength(uint256 length);\n\n /**\n * @dev The signature has an S value that is in the upper half order.\n */\n error ECDSAInvalidSignatureS(bytes32 s);\n\n /**\n * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not\n * return address(0) without also returning an error description. Errors are documented using an enum (error type)\n * and a bytes32 providing additional information about the error.\n *\n * If no error is returned, then the address can be used for verification purposes.\n *\n * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:\n * this function rejects them by requiring the `s` value to be in the lower\n * half order, and the `v` value to be either 27 or 28.\n *\n * IMPORTANT: `hash` _must_ be the result of a hash operation for the\n * verification to be secure: it is possible to craft signatures that\n * recover to arbitrary addresses for non-hashed data. A safe way to ensure\n * this is by receiving a hash of the original message (which may otherwise\n * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.\n *\n * Documentation for signature generation:\n * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]\n * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]\n */\n function tryRecover(\n bytes32 hash,\n bytes memory signature\n ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {\n if (signature.length == 65) {\n bytes32 r;\n bytes32 s;\n uint8 v;\n // ecrecover takes the signature parameters, and the only way to get them\n // currently is to use assembly.\n assembly (\"memory-safe\") {\n r := mload(add(signature, 0x20))\n s := mload(add(signature, 0x40))\n v := byte(0, mload(add(signature, 0x60)))\n }\n return tryRecover(hash, v, r, s);\n } else {\n return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));\n }\n }\n\n /**\n * @dev Returns the address that signed a hashed message (`hash`) with\n * `signature`. This address can then be used for verification purposes.\n *\n * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:\n * this function rejects them by requiring the `s` value to be in the lower\n * half order, and the `v` value to be either 27 or 28.\n *\n * IMPORTANT: `hash` _must_ be the result of a hash operation for the\n * verification to be secure: it is possible to craft signatures that\n * recover to arbitrary addresses for non-hashed data. A safe way to ensure\n * this is by receiving a hash of the original message (which may otherwise\n * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.\n */\n function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {\n (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature);\n _throwError(error, errorArg);\n return recovered;\n }\n\n /**\n * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.\n *\n * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures]\n */\n function tryRecover(\n bytes32 hash,\n bytes32 r,\n bytes32 vs\n ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {\n unchecked {\n bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);\n // We do not check for an overflow here since the shift operation results in 0 or 1.\n uint8 v = uint8((uint256(vs) >> 255) + 27);\n return tryRecover(hash, v, r, s);\n }\n }\n\n /**\n * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.\n */\n function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {\n (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs);\n _throwError(error, errorArg);\n return recovered;\n }\n\n /**\n * @dev Overload of {ECDSA-tryRecover} that receives the `v`,\n * `r` and `s` signature fields separately.\n */\n function tryRecover(\n bytes32 hash,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {\n // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature\n // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines\n // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most\n // signatures from current libraries generate a unique signature with an s-value in the lower half order.\n //\n // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value\n // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or\n // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept\n // these malleable signatures as well.\n if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {\n return (address(0), RecoverError.InvalidSignatureS, s);\n }\n\n // If the signature is valid (and not malleable), return the signer address\n address signer = ecrecover(hash, v, r, s);\n if (signer == address(0)) {\n return (address(0), RecoverError.InvalidSignature, bytes32(0));\n }\n\n return (signer, RecoverError.NoError, bytes32(0));\n }\n\n /**\n * @dev Overload of {ECDSA-recover} that receives the `v`,\n * `r` and `s` signature fields separately.\n */\n function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {\n (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s);\n _throwError(error, errorArg);\n return recovered;\n }\n\n /**\n * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.\n */\n function _throwError(RecoverError error, bytes32 errorArg) private pure {\n if (error == RecoverError.NoError) {\n return; // no error: do nothing\n } else if (error == RecoverError.InvalidSignature) {\n revert ECDSAInvalidSignature();\n } else if (error == RecoverError.InvalidSignatureLength) {\n revert ECDSAInvalidSignatureLength(uint256(errorArg));\n } else if (error == RecoverError.InvalidSignatureS) {\n revert ECDSAInvalidSignatureS(errorArg);\n }\n }\n}\n"},"@openzeppelin/contracts/utils/cryptography/EIP712.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/cryptography/EIP712.sol)\n\npragma solidity ^0.8.20;\n\nimport {MessageHashUtils} from \"./MessageHashUtils.sol\";\nimport {ShortStrings, ShortString} from \"../ShortStrings.sol\";\nimport {IERC5267} from \"../../interfaces/IERC5267.sol\";\n\n/**\n * @dev https://eips.ethereum.org/EIPS/eip-712[EIP-712] is a standard for hashing and signing of typed structured data.\n *\n * The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose\n * encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract\n * does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to\n * produce the hash of their typed data using a combination of `abi.encode` and `keccak256`.\n *\n * This contract implements the EIP-712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding\n * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA\n * ({_hashTypedDataV4}).\n *\n * The implementation of the domain separator was designed to be as efficient as possible while still properly updating\n * the chain id to protect against replay attacks on an eventual fork of the chain.\n *\n * NOTE: This contract implements the version of the encoding known as \"v4\", as implemented by the JSON RPC method\n * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].\n *\n * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain\n * separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the\n * separator from the immutable values, which is cheaper than accessing a cached version in cold storage.\n *\n * @custom:oz-upgrades-unsafe-allow state-variable-immutable\n */\nabstract contract EIP712 is IERC5267 {\n using ShortStrings for *;\n\n bytes32 private constant TYPE_HASH =\n keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n\n // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to\n // invalidate the cached domain separator if the chain id changes.\n bytes32 private immutable _cachedDomainSeparator;\n uint256 private immutable _cachedChainId;\n address private immutable _cachedThis;\n\n bytes32 private immutable _hashedName;\n bytes32 private immutable _hashedVersion;\n\n ShortString private immutable _name;\n ShortString private immutable _version;\n // slither-disable-next-line constable-states\n string private _nameFallback;\n // slither-disable-next-line constable-states\n string private _versionFallback;\n\n /**\n * @dev Initializes the domain separator and parameter caches.\n *\n * The meaning of `name` and `version` is specified in\n * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP-712]:\n *\n * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.\n * - `version`: the current major version of the signing domain.\n *\n * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart\n * contract upgrade].\n */\n constructor(string memory name, string memory version) {\n _name = name.toShortStringWithFallback(_nameFallback);\n _version = version.toShortStringWithFallback(_versionFallback);\n _hashedName = keccak256(bytes(name));\n _hashedVersion = keccak256(bytes(version));\n\n _cachedChainId = block.chainid;\n _cachedDomainSeparator = _buildDomainSeparator();\n _cachedThis = address(this);\n }\n\n /**\n * @dev Returns the domain separator for the current chain.\n */\n function _domainSeparatorV4() internal view returns (bytes32) {\n if (address(this) == _cachedThis && block.chainid == _cachedChainId) {\n return _cachedDomainSeparator;\n } else {\n return _buildDomainSeparator();\n }\n }\n\n function _buildDomainSeparator() private view returns (bytes32) {\n return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));\n }\n\n /**\n * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this\n * function returns the hash of the fully encoded EIP712 message for this domain.\n *\n * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:\n *\n * ```solidity\n * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(\n * keccak256(\"Mail(address to,string contents)\"),\n * mailTo,\n * keccak256(bytes(mailContents))\n * )));\n * address signer = ECDSA.recover(digest, signature);\n * ```\n */\n function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {\n return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash);\n }\n\n /**\n * @inheritdoc IERC5267\n */\n function eip712Domain()\n public\n view\n virtual\n returns (\n bytes1 fields,\n string memory name,\n string memory version,\n uint256 chainId,\n address verifyingContract,\n bytes32 salt,\n uint256[] memory extensions\n )\n {\n return (\n hex\"0f\", // 01111\n _EIP712Name(),\n _EIP712Version(),\n block.chainid,\n address(this),\n bytes32(0),\n new uint256[](0)\n );\n }\n\n /**\n * @dev The name parameter for the EIP712 domain.\n *\n * NOTE: By default this function reads _name which is an immutable value.\n * It only reads from storage if necessary (in case the value is too large to fit in a ShortString).\n */\n // solhint-disable-next-line func-name-mixedcase\n function _EIP712Name() internal view returns (string memory) {\n return _name.toStringWithFallback(_nameFallback);\n }\n\n /**\n * @dev The version parameter for the EIP712 domain.\n *\n * NOTE: By default this function reads _version which is an immutable value.\n * It only reads from storage if necessary (in case the value is too large to fit in a ShortString).\n */\n // solhint-disable-next-line func-name-mixedcase\n function _EIP712Version() internal view returns (string memory) {\n return _version.toStringWithFallback(_versionFallback);\n }\n}\n"},"@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/cryptography/MessageHashUtils.sol)\n\npragma solidity ^0.8.20;\n\nimport {Strings} from \"../Strings.sol\";\n\n/**\n * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing.\n *\n * The library provides methods for generating a hash of a message that conforms to the\n * https://eips.ethereum.org/EIPS/eip-191[ERC-191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712]\n * specifications.\n */\nlibrary MessageHashUtils {\n /**\n * @dev Returns the keccak256 digest of an ERC-191 signed data with version\n * `0x45` (`personal_sign` messages).\n *\n * The digest is calculated by prefixing a bytes32 `messageHash` with\n * `\"\\x19Ethereum Signed Message:\\n32\"` and hashing the result. It corresponds with the\n * hash signed when using the https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign[`eth_sign`] JSON-RPC method.\n *\n * NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with\n * keccak256, although any bytes32 value can be safely used because the final digest will\n * be re-hashed.\n *\n * See {ECDSA-recover}.\n */\n function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) {\n assembly (\"memory-safe\") {\n mstore(0x00, \"\\x19Ethereum Signed Message:\\n32\") // 32 is the bytes-length of messageHash\n mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix\n digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20)\n }\n }\n\n /**\n * @dev Returns the keccak256 digest of an ERC-191 signed data with version\n * `0x45` (`personal_sign` messages).\n *\n * The digest is calculated by prefixing an arbitrary `message` with\n * `\"\\x19Ethereum Signed Message:\\n\" + len(message)` and hashing the result. It corresponds with the\n * hash signed when using the https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign[`eth_sign`] JSON-RPC method.\n *\n * See {ECDSA-recover}.\n */\n function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) {\n return\n keccak256(bytes.concat(\"\\x19Ethereum Signed Message:\\n\", bytes(Strings.toString(message.length)), message));\n }\n\n /**\n * @dev Returns the keccak256 digest of an ERC-191 signed data with version\n * `0x00` (data with intended validator).\n *\n * The digest is calculated by prefixing an arbitrary `data` with `\"\\x19\\x00\"` and the intended\n * `validator` address. Then hashing the result.\n *\n * See {ECDSA-recover}.\n */\n function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) {\n return keccak256(abi.encodePacked(hex\"19_00\", validator, data));\n }\n\n /**\n * @dev Variant of {toDataWithIntendedValidatorHash-address-bytes} optimized for cases where `data` is a bytes32.\n */\n function toDataWithIntendedValidatorHash(\n address validator,\n bytes32 messageHash\n ) internal pure returns (bytes32 digest) {\n assembly (\"memory-safe\") {\n mstore(0x00, hex\"19_00\")\n mstore(0x02, shl(96, validator))\n mstore(0x16, messageHash)\n digest := keccak256(0x00, 0x36)\n }\n }\n\n /**\n * @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`).\n *\n * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with\n * `\\x19\\x01` and hashing the result. It corresponds to the hash signed by the\n * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712.\n *\n * See {ECDSA-recover}.\n */\n function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) {\n assembly (\"memory-safe\") {\n let ptr := mload(0x40)\n mstore(ptr, hex\"19_01\")\n mstore(add(ptr, 0x02), domainSeparator)\n mstore(add(ptr, 0x22), structHash)\n digest := keccak256(ptr, 0x42)\n }\n }\n}\n"},"@openzeppelin/contracts/utils/introspection/ERC165.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC165} from \"./IERC165.sol\";\n\n/**\n * @dev Implementation of the {IERC165} interface.\n *\n * Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check\n * for the additional interface id that will be supported. For example:\n *\n * ```solidity\n * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);\n * }\n * ```\n */\nabstract contract ERC165 is IERC165 {\n /**\n * @dev See {IERC165-supportsInterface}.\n */\n function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {\n return interfaceId == type(IERC165).interfaceId;\n }\n}\n"},"@openzeppelin/contracts/utils/introspection/IERC165.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[ERC].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n"},"@openzeppelin/contracts/utils/math/Math.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/math/Math.sol)\n\npragma solidity ^0.8.20;\n\nimport {Panic} from \"../Panic.sol\";\nimport {SafeCast} from \"./SafeCast.sol\";\n\n/**\n * @dev Standard math utilities missing in the Solidity language.\n */\nlibrary Math {\n enum Rounding {\n Floor, // Toward negative infinity\n Ceil, // Toward positive infinity\n Trunc, // Toward zero\n Expand // Away from zero\n }\n\n /**\n * @dev Return the 512-bit addition of two uint256.\n *\n * The result is stored in two 256 variables such that sum = high * 2²⁵⁶ + low.\n */\n function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n assembly (\"memory-safe\") {\n low := add(a, b)\n high := lt(low, a)\n }\n }\n\n /**\n * @dev Return the 512-bit multiplication of two uint256.\n *\n * The result is stored in two 256 variables such that product = high * 2²⁵⁶ + low.\n */\n function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n // 512-bit multiply [high low] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use\n // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256\n // variables such that product = high * 2²⁵⁶ + low.\n assembly (\"memory-safe\") {\n let mm := mulmod(a, b, not(0))\n low := mul(a, b)\n high := sub(sub(mm, low), lt(mm, low))\n }\n }\n\n /**\n * @dev Returns the addition of two unsigned integers, with a success flag (no overflow).\n */\n function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a + b;\n success = c >= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the subtraction of two unsigned integers, with a success flag (no overflow).\n */\n function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a - b;\n success = c <= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow).\n */\n function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a * b;\n assembly (\"memory-safe\") {\n // Only true when the multiplication doesn't overflow\n // (c / a == b) || (a == 0)\n success := or(eq(div(c, a), b), iszero(a))\n }\n // equivalent to: success ? c : 0\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the division of two unsigned integers, with a success flag (no division by zero).\n */\n function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `DIV` opcode returns zero when the denominator is 0.\n result := div(a, b)\n }\n }\n }\n\n /**\n * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).\n */\n function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `MOD` opcode returns zero when the denominator is 0.\n result := mod(a, b)\n }\n }\n }\n\n /**\n * @dev Unsigned saturating addition, bounds to `2²⁵⁶ - 1` instead of overflowing.\n */\n function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryAdd(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing.\n */\n function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {\n (, uint256 result) = trySub(a, b);\n return result;\n }\n\n /**\n * @dev Unsigned saturating multiplication, bounds to `2²⁵⁶ - 1` instead of overflowing.\n */\n function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryMul(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.\n *\n * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.\n * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute\n * one branch when needed, making this function more expensive.\n */\n function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n // branchless ternary works because:\n // b ^ (a ^ b) == a\n // b ^ 0 == b\n return b ^ ((a ^ b) * SafeCast.toUint(condition));\n }\n }\n\n /**\n * @dev Returns the largest of two numbers.\n */\n function max(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a > b, a, b);\n }\n\n /**\n * @dev Returns the smallest of two numbers.\n */\n function min(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a < b, a, b);\n }\n\n /**\n * @dev Returns the average of two numbers. The result is rounded towards\n * zero.\n */\n function average(uint256 a, uint256 b) internal pure returns (uint256) {\n // (a + b) / 2 can overflow.\n return (a & b) + (a ^ b) / 2;\n }\n\n /**\n * @dev Returns the ceiling of the division of two numbers.\n *\n * This differs from standard division with `/` in that it rounds towards infinity instead\n * of rounding towards zero.\n */\n function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\n if (b == 0) {\n // Guarantee the same behavior as in a regular Solidity division.\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n\n // The following calculation ensures accurate ceiling division without overflow.\n // Since a is non-zero, (a - 1) / b will not overflow.\n // The largest possible result occurs when (a - 1) / b is type(uint256).max,\n // but the largest value we can obtain is type(uint256).max - 1, which happens\n // when a = type(uint256).max and b = 1.\n unchecked {\n return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);\n }\n }\n\n /**\n * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or\n * denominator == 0.\n *\n * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by\n * Uniswap Labs also under MIT license.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n\n // Handle non-overflow cases, 256 by 256 division.\n if (high == 0) {\n // Solidity will revert if denominator == 0, unlike the div opcode on its own.\n // The surrounding unchecked block does not change this fact.\n // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.\n return low / denominator;\n }\n\n // Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0.\n if (denominator <= high) {\n Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));\n }\n\n ///////////////////////////////////////////////\n // 512 by 256 division.\n ///////////////////////////////////////////////\n\n // Make division exact by subtracting the remainder from [high low].\n uint256 remainder;\n assembly (\"memory-safe\") {\n // Compute remainder using mulmod.\n remainder := mulmod(x, y, denominator)\n\n // Subtract 256 bit number from 512 bit number.\n high := sub(high, gt(remainder, low))\n low := sub(low, remainder)\n }\n\n // Factor powers of two out of denominator and compute largest power of two divisor of denominator.\n // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.\n\n uint256 twos = denominator & (0 - denominator);\n assembly (\"memory-safe\") {\n // Divide denominator by twos.\n denominator := div(denominator, twos)\n\n // Divide [high low] by twos.\n low := div(low, twos)\n\n // Flip twos such that it is 2²⁵⁶ / twos. If twos is zero, then it becomes one.\n twos := add(div(sub(0, twos), twos), 1)\n }\n\n // Shift in bits from high into low.\n low |= high * twos;\n\n // Invert denominator mod 2²⁵⁶. Now that denominator is an odd number, it has an inverse modulo 2²⁵⁶ such\n // that denominator * inv ≡ 1 mod 2²⁵⁶. Compute the inverse by starting with a seed that is correct for\n // four bits. That is, denominator * inv ≡ 1 mod 2⁴.\n uint256 inverse = (3 * denominator) ^ 2;\n\n // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also\n // works in modular arithmetic, doubling the correct bits in each step.\n inverse *= 2 - denominator * inverse; // inverse mod 2⁸\n inverse *= 2 - denominator * inverse; // inverse mod 2¹⁶\n inverse *= 2 - denominator * inverse; // inverse mod 2³²\n inverse *= 2 - denominator * inverse; // inverse mod 2⁶⁴\n inverse *= 2 - denominator * inverse; // inverse mod 2¹²⁸\n inverse *= 2 - denominator * inverse; // inverse mod 2²⁵⁶\n\n // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.\n // This will give us the correct result modulo 2²⁵⁶. Since the preconditions guarantee that the outcome is\n // less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and high\n // is no longer required.\n result = low * inverse;\n return result;\n }\n }\n\n /**\n * @dev Calculates x * y / denominator with full precision, following the selected rounding direction.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {\n return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);\n }\n\n /**\n * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256.\n */\n function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n if (high >= 1 << n) {\n Panic.panic(Panic.UNDER_OVERFLOW);\n }\n return (high << (256 - n)) | (low >> n);\n }\n }\n\n /**\n * @dev Calculates x * y >> n with full precision, following the selected rounding direction.\n */\n function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) {\n return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0);\n }\n\n /**\n * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.\n *\n * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.\n * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.\n *\n * If the input value is not inversible, 0 is returned.\n *\n * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the\n * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.\n */\n function invMod(uint256 a, uint256 n) internal pure returns (uint256) {\n unchecked {\n if (n == 0) return 0;\n\n // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version)\n // Used to compute integers x and y such that: ax + ny = gcd(a, n).\n // When the gcd is 1, then the inverse of a modulo n exists and it's x.\n // ax + ny = 1\n // ax = 1 + (-y)n\n // ax ≡ 1 (mod n) # x is the inverse of a modulo n\n\n // If the remainder is 0 the gcd is n right away.\n uint256 remainder = a % n;\n uint256 gcd = n;\n\n // Therefore the initial coefficients are:\n // ax + ny = gcd(a, n) = n\n // 0a + 1n = n\n int256 x = 0;\n int256 y = 1;\n\n while (remainder != 0) {\n uint256 quotient = gcd / remainder;\n\n (gcd, remainder) = (\n // The old remainder is the next gcd to try.\n remainder,\n // Compute the next remainder.\n // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd\n // where gcd is at most n (capped to type(uint256).max)\n gcd - remainder * quotient\n );\n\n (x, y) = (\n // Increment the coefficient of a.\n y,\n // Decrement the coefficient of n.\n // Can overflow, but the result is casted to uint256 so that the\n // next value of y is \"wrapped around\" to a value between 0 and n - 1.\n x - y * int256(quotient)\n );\n }\n\n if (gcd != 1) return 0; // No inverse exists.\n return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.\n }\n }\n\n /**\n * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.\n *\n * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is\n * prime, then `a**(p-1) ≡ 1 mod p`. As a consequence, we have `a * a**(p-2) ≡ 1 mod p`, which means that\n * `a**(p-2)` is the modular multiplicative inverse of a in Fp.\n *\n * NOTE: this function does NOT check that `p` is a prime greater than `2`.\n */\n function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {\n unchecked {\n return Math.modExp(a, p - 2, p);\n }\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)\n *\n * Requirements:\n * - modulus can't be zero\n * - underlying staticcall to precompile must succeed\n *\n * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make\n * sure the chain you're using it on supports the precompiled contract for modular exponentiation\n * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise,\n * the underlying function will succeed given the lack of a revert, but the result may be incorrectly\n * interpreted as 0.\n */\n function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) {\n (bool success, uint256 result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).\n * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying\n * to operate modulo 0 or if the underlying precompile reverted.\n *\n * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain\n * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in\n * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack\n * of a revert, but the result may be incorrectly interpreted as 0.\n */\n function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {\n if (m == 0) return (false, 0);\n assembly (\"memory-safe\") {\n let ptr := mload(0x40)\n // | Offset | Content | Content (Hex) |\n // |-----------|------------|--------------------------------------------------------------------|\n // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x60:0x7f | value of b | 0x<.............................................................b> |\n // | 0x80:0x9f | value of e | 0x<.............................................................e> |\n // | 0xa0:0xbf | value of m | 0x<.............................................................m> |\n mstore(ptr, 0x20)\n mstore(add(ptr, 0x20), 0x20)\n mstore(add(ptr, 0x40), 0x20)\n mstore(add(ptr, 0x60), b)\n mstore(add(ptr, 0x80), e)\n mstore(add(ptr, 0xa0), m)\n\n // Given the result < m, it's guaranteed to fit in 32 bytes,\n // so we can use the memory scratch space located at offset 0.\n success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20)\n result := mload(0x00)\n }\n }\n\n /**\n * @dev Variant of {modExp} that supports inputs of arbitrary length.\n */\n function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) {\n (bool success, bytes memory result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Variant of {tryModExp} that supports inputs of arbitrary length.\n */\n function tryModExp(\n bytes memory b,\n bytes memory e,\n bytes memory m\n ) internal view returns (bool success, bytes memory result) {\n if (_zeroBytes(m)) return (false, new bytes(0));\n\n uint256 mLen = m.length;\n\n // Encode call args in result and move the free memory pointer\n result = abi.encodePacked(b.length, e.length, mLen, b, e, m);\n\n assembly (\"memory-safe\") {\n let dataPtr := add(result, 0x20)\n // Write result on top of args to avoid allocating extra memory.\n success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)\n // Overwrite the length.\n // result.length > returndatasize() is guaranteed because returndatasize() == m.length\n mstore(result, mLen)\n // Set the memory pointer after the returned data.\n mstore(0x40, add(dataPtr, mLen))\n }\n }\n\n /**\n * @dev Returns whether the provided byte array is zero.\n */\n function _zeroBytes(bytes memory byteArray) private pure returns (bool) {\n for (uint256 i = 0; i < byteArray.length; ++i) {\n if (byteArray[i] != 0) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded\n * towards zero.\n *\n * This method is based on Newton's method for computing square roots; the algorithm is restricted to only\n * using integer operations.\n */\n function sqrt(uint256 a) internal pure returns (uint256) {\n unchecked {\n // Take care of easy edge cases when a == 0 or a == 1\n if (a <= 1) {\n return a;\n }\n\n // In this function, we use Newton's method to get a root of `f(x) := x² - a`. It involves building a\n // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between\n // the current value as `ε_n = | x_n - sqrt(a) |`.\n //\n // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root\n // of the target. (i.e. `2**(e-1) ≤ sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is\n // bigger than any uint256.\n //\n // By noticing that\n // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ a < 2**(2*e)`\n // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar\n // to the msb function.\n uint256 aa = a;\n uint256 xn = 1;\n\n if (aa >= (1 << 128)) {\n aa >>= 128;\n xn <<= 64;\n }\n if (aa >= (1 << 64)) {\n aa >>= 64;\n xn <<= 32;\n }\n if (aa >= (1 << 32)) {\n aa >>= 32;\n xn <<= 16;\n }\n if (aa >= (1 << 16)) {\n aa >>= 16;\n xn <<= 8;\n }\n if (aa >= (1 << 8)) {\n aa >>= 8;\n xn <<= 4;\n }\n if (aa >= (1 << 4)) {\n aa >>= 4;\n xn <<= 2;\n }\n if (aa >= (1 << 2)) {\n xn <<= 1;\n }\n\n // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * x_n`. This implies ε_n ≤ 2**(e-1).\n //\n // We can refine our estimation by noticing that the middle of that interval minimizes the error.\n // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to ε_n ≤ 2**(e-2).\n // This is going to be our x_0 (and ε_0)\n xn = (3 * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2)\n\n // From here, Newton's method give us:\n // x_{n+1} = (x_n + a / x_n) / 2\n //\n // One should note that:\n // x_{n+1}² - a = ((x_n + a / x_n) / 2)² - a\n // = ((x_n² + a) / (2 * x_n))² - a\n // = (x_n⁴ + 2 * a * x_n² + a²) / (4 * x_n²) - a\n // = (x_n⁴ + 2 * a * x_n² + a² - 4 * a * x_n²) / (4 * x_n²)\n // = (x_n⁴ - 2 * a * x_n² + a²) / (4 * x_n²)\n // = (x_n² - a)² / (2 * x_n)²\n // = ((x_n² - a) / (2 * x_n))²\n // ≥ 0\n // Which proves that for all n ≥ 1, sqrt(a) ≤ x_n\n //\n // This gives us the proof of quadratic convergence of the sequence:\n // ε_{n+1} = | x_{n+1} - sqrt(a) |\n // = | (x_n + a / x_n) / 2 - sqrt(a) |\n // = | (x_n² + a - 2*x_n*sqrt(a)) / (2 * x_n) |\n // = | (x_n - sqrt(a))² / (2 * x_n) |\n // = | ε_n² / (2 * x_n) |\n // = ε_n² / | (2 * x_n) |\n //\n // For the first iteration, we have a special case where x_0 is known:\n // ε_1 = ε_0² / | (2 * x_0) |\n // ≤ (2**(e-2))² / (2 * (2**(e-1) + 2**(e-2)))\n // ≤ 2**(2*e-4) / (3 * 2**(e-1))\n // ≤ 2**(e-3) / 3\n // ≤ 2**(e-3-log2(3))\n // ≤ 2**(e-4.5)\n //\n // For the following iterations, we use the fact that, 2**(e-1) ≤ sqrt(a) ≤ x_n:\n // ε_{n+1} = ε_n² / | (2 * x_n) |\n // ≤ (2**(e-k))² / (2 * 2**(e-1))\n // ≤ 2**(2*e-2*k) / 2**e\n // ≤ 2**(e-2*k)\n xn = (xn + a / xn) >> 1; // ε_1 := | x_1 - sqrt(a) | ≤ 2**(e-4.5) -- special case, see above\n xn = (xn + a / xn) >> 1; // ε_2 := | x_2 - sqrt(a) | ≤ 2**(e-9) -- general case with k = 4.5\n xn = (xn + a / xn) >> 1; // ε_3 := | x_3 - sqrt(a) | ≤ 2**(e-18) -- general case with k = 9\n xn = (xn + a / xn) >> 1; // ε_4 := | x_4 - sqrt(a) | ≤ 2**(e-36) -- general case with k = 18\n xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72) -- general case with k = 36\n xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144) -- general case with k = 72\n\n // Because e ≤ 128 (as discussed during the first estimation phase), we know have reached a precision\n // ε_6 ≤ 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either\n // sqrt(a) or sqrt(a) + 1.\n return xn - SafeCast.toUint(xn > a / xn);\n }\n }\n\n /**\n * @dev Calculates sqrt(a), following the selected rounding direction.\n */\n function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = sqrt(a);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a);\n }\n }\n\n /**\n * @dev Return the log in base 2 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log2(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // If upper 8 bits of 16-bit half set, add 8 to result\n r |= SafeCast.toUint((x >> r) > 0xff) << 3;\n // If upper 4 bits of 8-bit half set, add 4 to result\n r |= SafeCast.toUint((x >> r) > 0xf) << 2;\n\n // Shifts value right by the current result and use it as an index into this lookup table:\n //\n // | x (4 bits) | index | table[index] = MSB position |\n // |------------|---------|-----------------------------|\n // | 0000 | 0 | table[0] = 0 |\n // | 0001 | 1 | table[1] = 0 |\n // | 0010 | 2 | table[2] = 1 |\n // | 0011 | 3 | table[3] = 1 |\n // | 0100 | 4 | table[4] = 2 |\n // | 0101 | 5 | table[5] = 2 |\n // | 0110 | 6 | table[6] = 2 |\n // | 0111 | 7 | table[7] = 2 |\n // | 1000 | 8 | table[8] = 3 |\n // | 1001 | 9 | table[9] = 3 |\n // | 1010 | 10 | table[10] = 3 |\n // | 1011 | 11 | table[11] = 3 |\n // | 1100 | 12 | table[12] = 3 |\n // | 1101 | 13 | table[13] = 3 |\n // | 1110 | 14 | table[14] = 3 |\n // | 1111 | 15 | table[15] = 3 |\n //\n // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes.\n assembly (\"memory-safe\") {\n r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000))\n }\n }\n\n /**\n * @dev Return the log in base 2, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log2(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value);\n }\n }\n\n /**\n * @dev Return the log in base 10 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log10(uint256 value) internal pure returns (uint256) {\n uint256 result = 0;\n unchecked {\n if (value >= 10 ** 64) {\n value /= 10 ** 64;\n result += 64;\n }\n if (value >= 10 ** 32) {\n value /= 10 ** 32;\n result += 32;\n }\n if (value >= 10 ** 16) {\n value /= 10 ** 16;\n result += 16;\n }\n if (value >= 10 ** 8) {\n value /= 10 ** 8;\n result += 8;\n }\n if (value >= 10 ** 4) {\n value /= 10 ** 4;\n result += 4;\n }\n if (value >= 10 ** 2) {\n value /= 10 ** 2;\n result += 2;\n }\n if (value >= 10 ** 1) {\n result += 1;\n }\n }\n return result;\n }\n\n /**\n * @dev Return the log in base 10, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log10(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value);\n }\n }\n\n /**\n * @dev Return the log in base 256 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n *\n * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.\n */\n function log256(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8\n return (r >> 3) | SafeCast.toUint((x >> r) > 0xff);\n }\n\n /**\n * @dev Return the log in base 256, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log256(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value);\n }\n }\n\n /**\n * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.\n */\n function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {\n return uint8(rounding) % 2 == 1;\n }\n}\n"},"@openzeppelin/contracts/utils/math/SafeCast.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)\n// This file was procedurally generated from scripts/generate/templates/SafeCast.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow\n * checks.\n *\n * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can\n * easily result in undesired exploitation or bugs, since developers usually\n * assume that overflows raise errors. `SafeCast` restores this intuition by\n * reverting the transaction when such an operation overflows.\n *\n * Using this library instead of the unchecked operations eliminates an entire\n * class of bugs, so it's recommended to use it always.\n */\nlibrary SafeCast {\n /**\n * @dev Value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);\n\n /**\n * @dev An int value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedIntToUint(int256 value);\n\n /**\n * @dev Value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);\n\n /**\n * @dev An uint value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedUintToInt(uint256 value);\n\n /**\n * @dev Returns the downcasted uint248 from uint256, reverting on\n * overflow (when the input is greater than largest uint248).\n *\n * Counterpart to Solidity's `uint248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toUint248(uint256 value) internal pure returns (uint248) {\n if (value > type(uint248).max) {\n revert SafeCastOverflowedUintDowncast(248, value);\n }\n return uint248(value);\n }\n\n /**\n * @dev Returns the downcasted uint240 from uint256, reverting on\n * overflow (when the input is greater than largest uint240).\n *\n * Counterpart to Solidity's `uint240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toUint240(uint256 value) internal pure returns (uint240) {\n if (value > type(uint240).max) {\n revert SafeCastOverflowedUintDowncast(240, value);\n }\n return uint240(value);\n }\n\n /**\n * @dev Returns the downcasted uint232 from uint256, reverting on\n * overflow (when the input is greater than largest uint232).\n *\n * Counterpart to Solidity's `uint232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toUint232(uint256 value) internal pure returns (uint232) {\n if (value > type(uint232).max) {\n revert SafeCastOverflowedUintDowncast(232, value);\n }\n return uint232(value);\n }\n\n /**\n * @dev Returns the downcasted uint224 from uint256, reverting on\n * overflow (when the input is greater than largest uint224).\n *\n * Counterpart to Solidity's `uint224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toUint224(uint256 value) internal pure returns (uint224) {\n if (value > type(uint224).max) {\n revert SafeCastOverflowedUintDowncast(224, value);\n }\n return uint224(value);\n }\n\n /**\n * @dev Returns the downcasted uint216 from uint256, reverting on\n * overflow (when the input is greater than largest uint216).\n *\n * Counterpart to Solidity's `uint216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toUint216(uint256 value) internal pure returns (uint216) {\n if (value > type(uint216).max) {\n revert SafeCastOverflowedUintDowncast(216, value);\n }\n return uint216(value);\n }\n\n /**\n * @dev Returns the downcasted uint208 from uint256, reverting on\n * overflow (when the input is greater than largest uint208).\n *\n * Counterpart to Solidity's `uint208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toUint208(uint256 value) internal pure returns (uint208) {\n if (value > type(uint208).max) {\n revert SafeCastOverflowedUintDowncast(208, value);\n }\n return uint208(value);\n }\n\n /**\n * @dev Returns the downcasted uint200 from uint256, reverting on\n * overflow (when the input is greater than largest uint200).\n *\n * Counterpart to Solidity's `uint200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toUint200(uint256 value) internal pure returns (uint200) {\n if (value > type(uint200).max) {\n revert SafeCastOverflowedUintDowncast(200, value);\n }\n return uint200(value);\n }\n\n /**\n * @dev Returns the downcasted uint192 from uint256, reverting on\n * overflow (when the input is greater than largest uint192).\n *\n * Counterpart to Solidity's `uint192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toUint192(uint256 value) internal pure returns (uint192) {\n if (value > type(uint192).max) {\n revert SafeCastOverflowedUintDowncast(192, value);\n }\n return uint192(value);\n }\n\n /**\n * @dev Returns the downcasted uint184 from uint256, reverting on\n * overflow (when the input is greater than largest uint184).\n *\n * Counterpart to Solidity's `uint184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toUint184(uint256 value) internal pure returns (uint184) {\n if (value > type(uint184).max) {\n revert SafeCastOverflowedUintDowncast(184, value);\n }\n return uint184(value);\n }\n\n /**\n * @dev Returns the downcasted uint176 from uint256, reverting on\n * overflow (when the input is greater than largest uint176).\n *\n * Counterpart to Solidity's `uint176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toUint176(uint256 value) internal pure returns (uint176) {\n if (value > type(uint176).max) {\n revert SafeCastOverflowedUintDowncast(176, value);\n }\n return uint176(value);\n }\n\n /**\n * @dev Returns the downcasted uint168 from uint256, reverting on\n * overflow (when the input is greater than largest uint168).\n *\n * Counterpart to Solidity's `uint168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toUint168(uint256 value) internal pure returns (uint168) {\n if (value > type(uint168).max) {\n revert SafeCastOverflowedUintDowncast(168, value);\n }\n return uint168(value);\n }\n\n /**\n * @dev Returns the downcasted uint160 from uint256, reverting on\n * overflow (when the input is greater than largest uint160).\n *\n * Counterpart to Solidity's `uint160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toUint160(uint256 value) internal pure returns (uint160) {\n if (value > type(uint160).max) {\n revert SafeCastOverflowedUintDowncast(160, value);\n }\n return uint160(value);\n }\n\n /**\n * @dev Returns the downcasted uint152 from uint256, reverting on\n * overflow (when the input is greater than largest uint152).\n *\n * Counterpart to Solidity's `uint152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toUint152(uint256 value) internal pure returns (uint152) {\n if (value > type(uint152).max) {\n revert SafeCastOverflowedUintDowncast(152, value);\n }\n return uint152(value);\n }\n\n /**\n * @dev Returns the downcasted uint144 from uint256, reverting on\n * overflow (when the input is greater than largest uint144).\n *\n * Counterpart to Solidity's `uint144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toUint144(uint256 value) internal pure returns (uint144) {\n if (value > type(uint144).max) {\n revert SafeCastOverflowedUintDowncast(144, value);\n }\n return uint144(value);\n }\n\n /**\n * @dev Returns the downcasted uint136 from uint256, reverting on\n * overflow (when the input is greater than largest uint136).\n *\n * Counterpart to Solidity's `uint136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toUint136(uint256 value) internal pure returns (uint136) {\n if (value > type(uint136).max) {\n revert SafeCastOverflowedUintDowncast(136, value);\n }\n return uint136(value);\n }\n\n /**\n * @dev Returns the downcasted uint128 from uint256, reverting on\n * overflow (when the input is greater than largest uint128).\n *\n * Counterpart to Solidity's `uint128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toUint128(uint256 value) internal pure returns (uint128) {\n if (value > type(uint128).max) {\n revert SafeCastOverflowedUintDowncast(128, value);\n }\n return uint128(value);\n }\n\n /**\n * @dev Returns the downcasted uint120 from uint256, reverting on\n * overflow (when the input is greater than largest uint120).\n *\n * Counterpart to Solidity's `uint120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toUint120(uint256 value) internal pure returns (uint120) {\n if (value > type(uint120).max) {\n revert SafeCastOverflowedUintDowncast(120, value);\n }\n return uint120(value);\n }\n\n /**\n * @dev Returns the downcasted uint112 from uint256, reverting on\n * overflow (when the input is greater than largest uint112).\n *\n * Counterpart to Solidity's `uint112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toUint112(uint256 value) internal pure returns (uint112) {\n if (value > type(uint112).max) {\n revert SafeCastOverflowedUintDowncast(112, value);\n }\n return uint112(value);\n }\n\n /**\n * @dev Returns the downcasted uint104 from uint256, reverting on\n * overflow (when the input is greater than largest uint104).\n *\n * Counterpart to Solidity's `uint104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toUint104(uint256 value) internal pure returns (uint104) {\n if (value > type(uint104).max) {\n revert SafeCastOverflowedUintDowncast(104, value);\n }\n return uint104(value);\n }\n\n /**\n * @dev Returns the downcasted uint96 from uint256, reverting on\n * overflow (when the input is greater than largest uint96).\n *\n * Counterpart to Solidity's `uint96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toUint96(uint256 value) internal pure returns (uint96) {\n if (value > type(uint96).max) {\n revert SafeCastOverflowedUintDowncast(96, value);\n }\n return uint96(value);\n }\n\n /**\n * @dev Returns the downcasted uint88 from uint256, reverting on\n * overflow (when the input is greater than largest uint88).\n *\n * Counterpart to Solidity's `uint88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toUint88(uint256 value) internal pure returns (uint88) {\n if (value > type(uint88).max) {\n revert SafeCastOverflowedUintDowncast(88, value);\n }\n return uint88(value);\n }\n\n /**\n * @dev Returns the downcasted uint80 from uint256, reverting on\n * overflow (when the input is greater than largest uint80).\n *\n * Counterpart to Solidity's `uint80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toUint80(uint256 value) internal pure returns (uint80) {\n if (value > type(uint80).max) {\n revert SafeCastOverflowedUintDowncast(80, value);\n }\n return uint80(value);\n }\n\n /**\n * @dev Returns the downcasted uint72 from uint256, reverting on\n * overflow (when the input is greater than largest uint72).\n *\n * Counterpart to Solidity's `uint72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toUint72(uint256 value) internal pure returns (uint72) {\n if (value > type(uint72).max) {\n revert SafeCastOverflowedUintDowncast(72, value);\n }\n return uint72(value);\n }\n\n /**\n * @dev Returns the downcasted uint64 from uint256, reverting on\n * overflow (when the input is greater than largest uint64).\n *\n * Counterpart to Solidity's `uint64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toUint64(uint256 value) internal pure returns (uint64) {\n if (value > type(uint64).max) {\n revert SafeCastOverflowedUintDowncast(64, value);\n }\n return uint64(value);\n }\n\n /**\n * @dev Returns the downcasted uint56 from uint256, reverting on\n * overflow (when the input is greater than largest uint56).\n *\n * Counterpart to Solidity's `uint56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toUint56(uint256 value) internal pure returns (uint56) {\n if (value > type(uint56).max) {\n revert SafeCastOverflowedUintDowncast(56, value);\n }\n return uint56(value);\n }\n\n /**\n * @dev Returns the downcasted uint48 from uint256, reverting on\n * overflow (when the input is greater than largest uint48).\n *\n * Counterpart to Solidity's `uint48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toUint48(uint256 value) internal pure returns (uint48) {\n if (value > type(uint48).max) {\n revert SafeCastOverflowedUintDowncast(48, value);\n }\n return uint48(value);\n }\n\n /**\n * @dev Returns the downcasted uint40 from uint256, reverting on\n * overflow (when the input is greater than largest uint40).\n *\n * Counterpart to Solidity's `uint40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toUint40(uint256 value) internal pure returns (uint40) {\n if (value > type(uint40).max) {\n revert SafeCastOverflowedUintDowncast(40, value);\n }\n return uint40(value);\n }\n\n /**\n * @dev Returns the downcasted uint32 from uint256, reverting on\n * overflow (when the input is greater than largest uint32).\n *\n * Counterpart to Solidity's `uint32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toUint32(uint256 value) internal pure returns (uint32) {\n if (value > type(uint32).max) {\n revert SafeCastOverflowedUintDowncast(32, value);\n }\n return uint32(value);\n }\n\n /**\n * @dev Returns the downcasted uint24 from uint256, reverting on\n * overflow (when the input is greater than largest uint24).\n *\n * Counterpart to Solidity's `uint24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toUint24(uint256 value) internal pure returns (uint24) {\n if (value > type(uint24).max) {\n revert SafeCastOverflowedUintDowncast(24, value);\n }\n return uint24(value);\n }\n\n /**\n * @dev Returns the downcasted uint16 from uint256, reverting on\n * overflow (when the input is greater than largest uint16).\n *\n * Counterpart to Solidity's `uint16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toUint16(uint256 value) internal pure returns (uint16) {\n if (value > type(uint16).max) {\n revert SafeCastOverflowedUintDowncast(16, value);\n }\n return uint16(value);\n }\n\n /**\n * @dev Returns the downcasted uint8 from uint256, reverting on\n * overflow (when the input is greater than largest uint8).\n *\n * Counterpart to Solidity's `uint8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toUint8(uint256 value) internal pure returns (uint8) {\n if (value > type(uint8).max) {\n revert SafeCastOverflowedUintDowncast(8, value);\n }\n return uint8(value);\n }\n\n /**\n * @dev Converts a signed int256 into an unsigned uint256.\n *\n * Requirements:\n *\n * - input must be greater than or equal to 0.\n */\n function toUint256(int256 value) internal pure returns (uint256) {\n if (value < 0) {\n revert SafeCastOverflowedIntToUint(value);\n }\n return uint256(value);\n }\n\n /**\n * @dev Returns the downcasted int248 from int256, reverting on\n * overflow (when the input is less than smallest int248 or\n * greater than largest int248).\n *\n * Counterpart to Solidity's `int248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toInt248(int256 value) internal pure returns (int248 downcasted) {\n downcasted = int248(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(248, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int240 from int256, reverting on\n * overflow (when the input is less than smallest int240 or\n * greater than largest int240).\n *\n * Counterpart to Solidity's `int240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toInt240(int256 value) internal pure returns (int240 downcasted) {\n downcasted = int240(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(240, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int232 from int256, reverting on\n * overflow (when the input is less than smallest int232 or\n * greater than largest int232).\n *\n * Counterpart to Solidity's `int232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toInt232(int256 value) internal pure returns (int232 downcasted) {\n downcasted = int232(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(232, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int224 from int256, reverting on\n * overflow (when the input is less than smallest int224 or\n * greater than largest int224).\n *\n * Counterpart to Solidity's `int224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toInt224(int256 value) internal pure returns (int224 downcasted) {\n downcasted = int224(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(224, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int216 from int256, reverting on\n * overflow (when the input is less than smallest int216 or\n * greater than largest int216).\n *\n * Counterpart to Solidity's `int216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toInt216(int256 value) internal pure returns (int216 downcasted) {\n downcasted = int216(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(216, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int208 from int256, reverting on\n * overflow (when the input is less than smallest int208 or\n * greater than largest int208).\n *\n * Counterpart to Solidity's `int208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toInt208(int256 value) internal pure returns (int208 downcasted) {\n downcasted = int208(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(208, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int200 from int256, reverting on\n * overflow (when the input is less than smallest int200 or\n * greater than largest int200).\n *\n * Counterpart to Solidity's `int200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toInt200(int256 value) internal pure returns (int200 downcasted) {\n downcasted = int200(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(200, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int192 from int256, reverting on\n * overflow (when the input is less than smallest int192 or\n * greater than largest int192).\n *\n * Counterpart to Solidity's `int192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toInt192(int256 value) internal pure returns (int192 downcasted) {\n downcasted = int192(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(192, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int184 from int256, reverting on\n * overflow (when the input is less than smallest int184 or\n * greater than largest int184).\n *\n * Counterpart to Solidity's `int184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toInt184(int256 value) internal pure returns (int184 downcasted) {\n downcasted = int184(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(184, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int176 from int256, reverting on\n * overflow (when the input is less than smallest int176 or\n * greater than largest int176).\n *\n * Counterpart to Solidity's `int176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toInt176(int256 value) internal pure returns (int176 downcasted) {\n downcasted = int176(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(176, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int168 from int256, reverting on\n * overflow (when the input is less than smallest int168 or\n * greater than largest int168).\n *\n * Counterpart to Solidity's `int168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toInt168(int256 value) internal pure returns (int168 downcasted) {\n downcasted = int168(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(168, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int160 from int256, reverting on\n * overflow (when the input is less than smallest int160 or\n * greater than largest int160).\n *\n * Counterpart to Solidity's `int160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toInt160(int256 value) internal pure returns (int160 downcasted) {\n downcasted = int160(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(160, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int152 from int256, reverting on\n * overflow (when the input is less than smallest int152 or\n * greater than largest int152).\n *\n * Counterpart to Solidity's `int152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toInt152(int256 value) internal pure returns (int152 downcasted) {\n downcasted = int152(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(152, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int144 from int256, reverting on\n * overflow (when the input is less than smallest int144 or\n * greater than largest int144).\n *\n * Counterpart to Solidity's `int144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toInt144(int256 value) internal pure returns (int144 downcasted) {\n downcasted = int144(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(144, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int136 from int256, reverting on\n * overflow (when the input is less than smallest int136 or\n * greater than largest int136).\n *\n * Counterpart to Solidity's `int136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toInt136(int256 value) internal pure returns (int136 downcasted) {\n downcasted = int136(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(136, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int128 from int256, reverting on\n * overflow (when the input is less than smallest int128 or\n * greater than largest int128).\n *\n * Counterpart to Solidity's `int128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toInt128(int256 value) internal pure returns (int128 downcasted) {\n downcasted = int128(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(128, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int120 from int256, reverting on\n * overflow (when the input is less than smallest int120 or\n * greater than largest int120).\n *\n * Counterpart to Solidity's `int120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toInt120(int256 value) internal pure returns (int120 downcasted) {\n downcasted = int120(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(120, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int112 from int256, reverting on\n * overflow (when the input is less than smallest int112 or\n * greater than largest int112).\n *\n * Counterpart to Solidity's `int112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toInt112(int256 value) internal pure returns (int112 downcasted) {\n downcasted = int112(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(112, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int104 from int256, reverting on\n * overflow (when the input is less than smallest int104 or\n * greater than largest int104).\n *\n * Counterpart to Solidity's `int104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toInt104(int256 value) internal pure returns (int104 downcasted) {\n downcasted = int104(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(104, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int96 from int256, reverting on\n * overflow (when the input is less than smallest int96 or\n * greater than largest int96).\n *\n * Counterpart to Solidity's `int96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toInt96(int256 value) internal pure returns (int96 downcasted) {\n downcasted = int96(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(96, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int88 from int256, reverting on\n * overflow (when the input is less than smallest int88 or\n * greater than largest int88).\n *\n * Counterpart to Solidity's `int88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toInt88(int256 value) internal pure returns (int88 downcasted) {\n downcasted = int88(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(88, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int80 from int256, reverting on\n * overflow (when the input is less than smallest int80 or\n * greater than largest int80).\n *\n * Counterpart to Solidity's `int80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toInt80(int256 value) internal pure returns (int80 downcasted) {\n downcasted = int80(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(80, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int72 from int256, reverting on\n * overflow (when the input is less than smallest int72 or\n * greater than largest int72).\n *\n * Counterpart to Solidity's `int72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toInt72(int256 value) internal pure returns (int72 downcasted) {\n downcasted = int72(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(72, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int64 from int256, reverting on\n * overflow (when the input is less than smallest int64 or\n * greater than largest int64).\n *\n * Counterpart to Solidity's `int64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toInt64(int256 value) internal pure returns (int64 downcasted) {\n downcasted = int64(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(64, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int56 from int256, reverting on\n * overflow (when the input is less than smallest int56 or\n * greater than largest int56).\n *\n * Counterpart to Solidity's `int56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toInt56(int256 value) internal pure returns (int56 downcasted) {\n downcasted = int56(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(56, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int48 from int256, reverting on\n * overflow (when the input is less than smallest int48 or\n * greater than largest int48).\n *\n * Counterpart to Solidity's `int48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toInt48(int256 value) internal pure returns (int48 downcasted) {\n downcasted = int48(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(48, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int40 from int256, reverting on\n * overflow (when the input is less than smallest int40 or\n * greater than largest int40).\n *\n * Counterpart to Solidity's `int40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toInt40(int256 value) internal pure returns (int40 downcasted) {\n downcasted = int40(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(40, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int32 from int256, reverting on\n * overflow (when the input is less than smallest int32 or\n * greater than largest int32).\n *\n * Counterpart to Solidity's `int32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toInt32(int256 value) internal pure returns (int32 downcasted) {\n downcasted = int32(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(32, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int24 from int256, reverting on\n * overflow (when the input is less than smallest int24 or\n * greater than largest int24).\n *\n * Counterpart to Solidity's `int24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toInt24(int256 value) internal pure returns (int24 downcasted) {\n downcasted = int24(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(24, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int16 from int256, reverting on\n * overflow (when the input is less than smallest int16 or\n * greater than largest int16).\n *\n * Counterpart to Solidity's `int16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toInt16(int256 value) internal pure returns (int16 downcasted) {\n downcasted = int16(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(16, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int8 from int256, reverting on\n * overflow (when the input is less than smallest int8 or\n * greater than largest int8).\n *\n * Counterpart to Solidity's `int8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toInt8(int256 value) internal pure returns (int8 downcasted) {\n downcasted = int8(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(8, value);\n }\n }\n\n /**\n * @dev Converts an unsigned uint256 into a signed int256.\n *\n * Requirements:\n *\n * - input must be less than or equal to maxInt256.\n */\n function toInt256(uint256 value) internal pure returns (int256) {\n // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive\n if (value > uint256(type(int256).max)) {\n revert SafeCastOverflowedUintToInt(value);\n }\n return int256(value);\n }\n\n /**\n * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.\n */\n function toUint(bool b) internal pure returns (uint256 u) {\n assembly (\"memory-safe\") {\n u := iszero(iszero(b))\n }\n }\n}\n"},"@openzeppelin/contracts/utils/math/SignedMath.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SignedMath.sol)\n\npragma solidity ^0.8.20;\n\nimport {SafeCast} from \"./SafeCast.sol\";\n\n/**\n * @dev Standard signed math utilities missing in the Solidity language.\n */\nlibrary SignedMath {\n /**\n * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.\n *\n * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.\n * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute\n * one branch when needed, making this function more expensive.\n */\n function ternary(bool condition, int256 a, int256 b) internal pure returns (int256) {\n unchecked {\n // branchless ternary works because:\n // b ^ (a ^ b) == a\n // b ^ 0 == b\n return b ^ ((a ^ b) * int256(SafeCast.toUint(condition)));\n }\n }\n\n /**\n * @dev Returns the largest of two signed numbers.\n */\n function max(int256 a, int256 b) internal pure returns (int256) {\n return ternary(a > b, a, b);\n }\n\n /**\n * @dev Returns the smallest of two signed numbers.\n */\n function min(int256 a, int256 b) internal pure returns (int256) {\n return ternary(a < b, a, b);\n }\n\n /**\n * @dev Returns the average of two signed numbers without overflow.\n * The result is rounded towards zero.\n */\n function average(int256 a, int256 b) internal pure returns (int256) {\n // Formula from the book \"Hacker's Delight\"\n int256 x = (a & b) + ((a ^ b) >> 1);\n return x + (int256(uint256(x) >> 255) & (a ^ b));\n }\n\n /**\n * @dev Returns the absolute unsigned value of a signed value.\n */\n function abs(int256 n) internal pure returns (uint256) {\n unchecked {\n // Formula from the \"Bit Twiddling Hacks\" by Sean Eron Anderson.\n // Since `n` is a signed integer, the generated bytecode will use the SAR opcode to perform the right shift,\n // taking advantage of the most significant (or \"sign\" bit) in two's complement representation.\n // This opcode adds new most significant bits set to the value of the previous most significant bit. As a result,\n // the mask will either be `bytes32(0)` (if n is positive) or `~bytes32(0)` (if n is negative).\n int256 mask = n >> 255;\n\n // A `bytes32(0)` mask leaves the input unchanged, while a `~bytes32(0)` mask complements it.\n return uint256((n + mask) ^ mask);\n }\n }\n}\n"},"@solidity-bits/contracts/BitScan.sol":{"content":"// SPDX-License-Identifier: MIT\n/**\n _____ ___ ___ __ ____ _ __ \n / ___/____ / (_)___/ (_) /___ __ / __ )(_) /______\n \\__ \\/ __ \\/ / / __ / / __/ / / / / __ / / __/ ___/\n ___/ / /_/ / / / /_/ / / /_/ /_/ / / /_/ / / /_(__ ) \n/____/\\____/_/_/\\__,_/_/\\__/\\__, / /_____/_/\\__/____/ \n /____/ \n\n- npm: https://www.npmjs.com/package/solidity-bits\n- github: https://github.com/estarriolvetch/solidity-bits\n\n */\n\npragma solidity ^0.8.0;\n\n\nlibrary BitScan {\n uint256 constant private DEBRUIJN_256 = 0x818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff;\n bytes constant private LOOKUP_TABLE_256 = hex\"0001020903110a19042112290b311a3905412245134d2a550c5d32651b6d3a7506264262237d468514804e8d2b95569d0d495ea533a966b11c886eb93bc176c9071727374353637324837e9b47af86c7155181ad4fd18ed32c9096db57d59ee30e2e4a6a5f92a6be3498aae067ddb2eb1d5989b56fd7baf33ca0c2ee77e5caf7ff0810182028303840444c545c646c7425617c847f8c949c48a4a8b087b8c0c816365272829aaec650acd0d28fdad4e22d6991bd97dfdcea58b4d6f29fede4f6fe0f1f2f3f4b5b6b607b8b93a3a7b7bf357199c5abcfd9e168bcdee9b3f1ecf5fd1e3e5a7a8aa2b670c4ced8bbe8f0f4fc3d79a1c3cde7effb78cce6facbf9f8\";\n\n /**\n @dev Isolate the least significant set bit.\n */ \n function isolateLS1B256(uint256 bb) pure internal returns (uint256) {\n require(bb > 0);\n unchecked {\n return bb & (0 - bb);\n }\n } \n\n /**\n @dev Isolate the most significant set bit.\n */ \n function isolateMS1B256(uint256 bb) pure internal returns (uint256) {\n require(bb > 0);\n unchecked {\n bb |= bb >> 128;\n bb |= bb >> 64;\n bb |= bb >> 32;\n bb |= bb >> 16;\n bb |= bb >> 8;\n bb |= bb >> 4;\n bb |= bb >> 2;\n bb |= bb >> 1;\n \n return (bb >> 1) + 1;\n }\n } \n\n /**\n @dev Find the index of the lest significant set bit. (trailing zero count)\n */ \n function bitScanForward256(uint256 bb) pure internal returns (uint8) {\n unchecked {\n return uint8(LOOKUP_TABLE_256[(isolateLS1B256(bb) * DEBRUIJN_256) >> 248]);\n } \n }\n\n /**\n @dev Find the index of the most significant set bit.\n */ \n function bitScanReverse256(uint256 bb) pure internal returns (uint8) {\n unchecked {\n return 255 - uint8(LOOKUP_TABLE_256[((isolateMS1B256(bb) * DEBRUIJN_256) >> 248)]);\n } \n }\n\n function log2(uint256 bb) pure internal returns (uint8) {\n unchecked {\n return uint8(LOOKUP_TABLE_256[(isolateMS1B256(bb) * DEBRUIJN_256) >> 248]);\n } \n }\n}\n"},"@solidity-bits/contracts/Popcount.sol":{"content":"// SPDX-License-Identifier: MIT\n/**\n _____ ___ ___ __ ____ _ __ \n / ___/____ / (_)___/ (_) /___ __ / __ )(_) /______\n \\__ \\/ __ \\/ / / __ / / __/ / / / / __ / / __/ ___/\n ___/ / /_/ / / / /_/ / / /_/ /_/ / / /_/ / / /_(__ ) \n/____/\\____/_/_/\\__,_/_/\\__/\\__, / /_____/_/\\__/____/ \n /____/ \n\n- npm: https://www.npmjs.com/package/solidity-bits\n- github: https://github.com/estarriolvetch/solidity-bits\n\n */\n\npragma solidity ^0.8.0;\n\nlibrary Popcount {\n uint256 private constant m1 = 0x5555555555555555555555555555555555555555555555555555555555555555;\n uint256 private constant m2 = 0x3333333333333333333333333333333333333333333333333333333333333333;\n uint256 private constant m4 = 0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f;\n uint256 private constant h01 = 0x0101010101010101010101010101010101010101010101010101010101010101;\n\n function popcount256A(uint256 x) internal pure returns (uint256 count) {\n unchecked{\n for (count=0; x!=0; count++)\n x &= x - 1;\n }\n }\n\n function popcount256B(uint256 x) internal pure returns (uint256) {\n if (x == type(uint256).max) {\n return 256;\n }\n unchecked {\n x -= (x >> 1) & m1; //put count of each 2 bits into those 2 bits\n x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits \n x = (x + (x >> 4)) & m4; //put count of each 8 bits into those 8 bits \n x = (x * h01) >> 248; //returns left 8 bits of x + (x<<8) + (x<<16) + (x<<24) + ... \n }\n return x;\n }\n}"},"contracts/AccessControl.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {EasyAccessControl} from \"./EasyAccessControl.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport \"./AccessControlErrors.sol\";\n\n\n/**\n * @title AccessControl (external)\n * @notice Standalone access control with bitmask roles. Intended to be shared by multiple contracts.\n */\ncontract AccessControl is\n IAccessControl,\n EasyAccessControl,\n ERC2771Context\n{\n constructor(\n address contractAdmin_,\n address reserveAdmin_,\n address transferAdmin_,\n address trustedForwarder_\n )\n EasyAccessControl(contractAdmin_)\n ERC2771Context(trustedForwarder_)\n validAddress(reserveAdmin_)\n validAddress(transferAdmin_)\n validAddress(trustedForwarder_)\n {\n _grantRole(reserveAdmin_, RESERVE_ADMIN_ROLE);\n _grantRole(transferAdmin_, TRANSFER_ADMIN_ROLE);\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n}\n"},"contracts/AccessControlErrors.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\n// Centralized error definitions for AccessControl\n// These errors are used by EasyAccessControl, AccessControl, and contracts that use access control\n\nerror EasyAccessControl_InvalidZeroAddress();\nerror EasyAccessControl_InvalidRole();\nerror EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(address addr);\nerror EasyAccessControl_DoesNotHaveContractAdminRole(address addr);\nerror EasyAccessControl_DoesNotHaveTransferAdminRole(address addr);\nerror EasyAccessControl_DoesNotHaveReserveAdminRole(address addr);\nerror EasyAccessControl_DoesNotHaveWalletsAdminRole(address addr);\nerror EasyAccessControl_DoesNotHaveSoftBurnAdminRole(address addr);\nerror EasyAccessControl_DoesNotHaveAdminRole(address addr);\nerror EasyAccessControl_AlreadyHasRole();\nerror EasyAccessControl_CannotRevokeRole();\nerror EasyAccessControl_AtLeastOneContractAdminRequired();\nerror EasyAccessControl_ArraysMustBeSameLength();\n"},"contracts/EasyAccessControl.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title Binary Access control\n * @author By CoMakery, Inc., Upside, Republic\n * @dev Binary equivalent to OpenZeppelin/AccessControl\n * Uses bits for storing user roles, minify gas cost and contract size\n */\ncontract EasyAccessControl is IAccessControl, Context {\n uint8 public constant CONTRACT_ADMIN_ROLE = 1; // 0000 0001\n uint8 public constant RESERVE_ADMIN_ROLE = 2; // 0000 0010\n uint8 public constant WALLETS_ADMIN_ROLE = 4; // 0000 0100\n uint8 public constant TRANSFER_ADMIN_ROLE = 8; // 0000 1000\n uint8 public constant SOFT_BURN_ADMIN_ROLE = 16;// 0001 0000\n uint8 public constant MINT_ADMIN_ROLE = 32; // 0010 0000\n\n // MAX_ROLE_BIT is the bitmask with all defined roles set (1|2|4|8|16|32 = 63)\n uint8 internal constant MAX_ROLE_BIT = \n CONTRACT_ADMIN_ROLE |\n RESERVE_ADMIN_ROLE |\n WALLETS_ADMIN_ROLE |\n TRANSFER_ADMIN_ROLE |\n SOFT_BURN_ADMIN_ROLE |\n MINT_ADMIN_ROLE;\n\n mapping(address => uint8) private _roles; // address => binary roles\n uint8 public contractAdminCount; // counter of contract admins to keep at least one\n\n // Note: Events and errors are now defined in IAccessControl interface\n\n modifier validAddress(address addr_) {\n if (addr_ == address(0)) {\n revert EasyAccessControl_InvalidZeroAddress();\n }\n _;\n }\n\n modifier validRole(uint8 role) {\n if (role == 0 || role > MAX_ROLE_BIT) {\n revert EasyAccessControl_InvalidRole();\n }\n _;\n }\n\n modifier onlyContractAdminOrTransferAdmin() {\n _onlyContractAdminOrTransferAdmin();\n _;\n }\n function _onlyContractAdminOrTransferAdmin() internal view {\n if (\n !hasRole(_msgSender(), CONTRACT_ADMIN_ROLE) &&\n !hasRole(_msgSender(), TRANSFER_ADMIN_ROLE)\n ) {\n revert EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(_msgSender());\n }\n }\n\n modifier onlyContractAdmin() {\n _onlyContractAdmin();\n _;\n }\n function _onlyContractAdmin() internal view {\n if (!hasRole(_msgSender(), CONTRACT_ADMIN_ROLE)) {\n revert EasyAccessControl_DoesNotHaveContractAdminRole(_msgSender());\n }\n }\n\n modifier onlyTransferAdmin() {\n _onlyTransferAdmin();\n _;\n }\n function _onlyTransferAdmin() internal view {\n if (!hasRole(_msgSender(), TRANSFER_ADMIN_ROLE)) {\n revert EasyAccessControl_DoesNotHaveTransferAdminRole(_msgSender());\n }\n }\n\n modifier onlyReserveAdmin() {\n _onlyReserveAdmin();\n _;\n }\n function _onlyReserveAdmin() internal view {\n if (!hasRole(_msgSender(), RESERVE_ADMIN_ROLE)) {\n revert EasyAccessControl_DoesNotHaveReserveAdminRole(_msgSender());\n }\n }\n\n modifier onlyWalletsAdmin() {\n _onlyWalletsAdmin();\n _;\n }\n function _onlyWalletsAdmin() internal view {\n if (!hasRole(_msgSender(), WALLETS_ADMIN_ROLE)) {\n revert EasyAccessControl_DoesNotHaveWalletsAdminRole(_msgSender());\n }\n }\n\n modifier onlyWalletsAdminOrTransferAdmin() {\n _onlyWalletsAdminOrTransferAdmin();\n _;\n }\n function _onlyWalletsAdminOrTransferAdmin() internal view {\n if (\n !hasRole(_msgSender(), WALLETS_ADMIN_ROLE) &&\n !hasRole(_msgSender(), TRANSFER_ADMIN_ROLE)\n ) {\n revert EasyAccessControl_DoesNotHaveAdminRole(_msgSender());\n }\n }\n\n modifier anyAdmin() {\n _anyAdmin();\n _;\n }\n function _anyAdmin() internal view {\n if (\n !hasRole(_msgSender(), RESERVE_ADMIN_ROLE) &&\n !hasRole(_msgSender(), WALLETS_ADMIN_ROLE) &&\n !hasRole(_msgSender(), TRANSFER_ADMIN_ROLE) &&\n !hasRole(_msgSender(), CONTRACT_ADMIN_ROLE)\n ) {\n revert EasyAccessControl_DoesNotHaveAdminRole(_msgSender());\n }\n }\n\n /**\n * @notice Constructor, init base role\n * @param contractAdmin_ address of contract admin\n */\n constructor(address contractAdmin_) validAddress(contractAdmin_) {\n _grantRole(contractAdmin_, CONTRACT_ADMIN_ROLE);\n }\n\n /**\n * @notice Grant roles to addresses using role bitmasks\n * @param addresses to grant roles\n * @param roles_ bitmasks array\n */\n function batchGrantRoles(\n address[] calldata addresses,\n uint8[] calldata roles_\n ) public onlyContractAdmin {\n uint256 _len = addresses.length;\n if (_len != roles_.length) {\n revert EasyAccessControl_ArraysMustBeSameLength();\n }\n for (uint256 i; i < _len; i++) {\n grantRole(addresses[i], roles_[i]);\n }\n }\n\n /**\n * @notice Revoke roles to addresses using role bitmasks\n * @param addresses to revoke roles\n * @param roles_ bitmasks array\n */\n function batchRevokeRoles(\n address[] calldata addresses,\n uint8[] calldata roles_\n ) public onlyContractAdmin {\n uint256 _len = addresses.length;\n if (_len != roles_.length) {\n revert EasyAccessControl_ArraysMustBeSameLength();\n }\n for (uint256 i; i < _len; i++) {\n revokeRole(addresses[i], roles_[i]);\n }\n }\n\n /**\n * @notice Grant role/roles to address using role bitmask\n * @param addr to grant role\n * @param role bitmask of role/roles to grant\n */\n function grantRole(\n address addr,\n uint8 role\n ) public validRole(role) validAddress(addr) onlyContractAdmin {\n _grantRole(addr, role);\n }\n\n /**\n * @notice Grant role/roles to address using role bitmask\n * @param addr to grant role\n * @param role bitmask of role/roles to grant\n */\n function _grantRole(address addr, uint8 role) internal virtual {\n if (hasRole(addr, role)) {\n revert EasyAccessControl_AlreadyHasRole();\n }\n if (\n _roles[addr] & CONTRACT_ADMIN_ROLE == 0 &&\n role & CONTRACT_ADMIN_ROLE > 0\n ) contractAdminCount++;\n _roles[addr] |= role;\n emit RoleChange(_msgSender(), addr, role, true);\n }\n\n /**\n * @notice Revoke role/roles from address using role bitmask\n * @param addr to revoke role\n * @param role bitmask of role/roles to revoke\n */\n function revokeRole(\n address addr,\n uint8 role\n ) public validRole(role) validAddress(addr) onlyContractAdmin {\n if (!hasRole(addr, role)) {\n revert EasyAccessControl_CannotRevokeRole();\n }\n if (role & CONTRACT_ADMIN_ROLE > 0) {\n if (contractAdminCount == 1) {\n revert EasyAccessControl_AtLeastOneContractAdminRequired();\n }\n contractAdminCount--;\n }\n _roles[addr] ^= role;\n emit RoleChange(_msgSender(), addr, role, false);\n }\n\n /**\n * @notice Check role/roles availability at address\n * @param addr to check role\n * @param role bitmask of role/roles to check\n * @return bool true or false\n */\n function hasRole(\n address addr,\n uint8 role\n ) public view validRole(role) validAddress(addr) returns (bool) {\n return _roles[addr] & role == role;\n }\n\n /**\n * @notice Get the roles for an address (equivalent to admins mapping)\n * @param addr The address to check\n * @return uint8 The bitmask of roles for the address\n */\n function roles(address addr) public view returns (uint8) {\n return _roles[addr];\n }\n}\n"},"contracts/IdentityRegistry.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\nimport {RestrictedLockupToken} from \"./RestrictedLockupToken.sol\";\nimport {IIdentityRegistry} from \"./interfaces/IIdentityRegistry.sol\";\nimport {ERC165} from \"@openzeppelin/contracts/utils/introspection/ERC165.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {EasyAccessControl} from \"./EasyAccessControl.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\n\ncontract IdentityRegistry is\n IIdentityRegistry,\n ERC165,\n EasyAccessControl,\n ERC2771Context\n{\n uint256 constant MAX_REGIONS_COUNT = 10;\n uint256 constant MAX_VALIDITY_DURATION = 100 * 365 days;\n bytes4 public immutable INTERFACE_ID;\n uint256 private constant NO_ACCREDITATION = 0;\n\n mapping(address wallet => IdentityInfo info) private _identity;\n uint256 private _amlKycValidityDurationSeconds;\n\n error IdentityRegistry_InvalidWalletsAdmin();\n error IdentityRegistry_InvalidTrustedForwarder();\n error IdentityRegistry_WalletHasNoIdentityToRemove();\n error IdentityRegistry_WalletAlreadyHasSameRegions();\n error IdentityRegistry_WalletAlreadyHasRegion();\n error IdentityRegistry_WalletDoesNotHaveRegion();\n error IdentityRegistry_WalletAlreadyHasSameAmlKycStatus();\n error IdentityRegistry_WalletAlreadyHasSameAccreditationLevel();\n error IdentityRegistry_WalletAlreadyHasNoAccreditation();\n error IdentityRegistry_AlreadyHasSameAmlKycValidityDuration();\n error IdentityRegistry_EmptyRegionsArray();\n error IdentityRegistry_InvalidRegion();\n error IdentityRegistry_RegionsExceedsMaxCount();\n error IdentityRegistry_InvalidAmlKycValidityDuration();\n error IdentityRegistry_TimestampInTheFuture();\n\n constructor(\n address contractAdmin_,\n address walletsAdmin_,\n address trustedForwarder_,\n uint256 amlKycValidityDuration_\n ) EasyAccessControl(contractAdmin_) ERC2771Context(trustedForwarder_) {\n // contractAdmin verification is done in EasyAccessControl constructor\n // and reverts with EasyAccessControl_InvalidZeroAddress\n if (walletsAdmin_ == address(0)) {\n revert IdentityRegistry_InvalidWalletsAdmin();\n }\n if (trustedForwarder_ == address(0)) {\n revert IdentityRegistry_InvalidTrustedForwarder();\n }\n\n INTERFACE_ID = type(IIdentityRegistry).interfaceId;\n _grantRole(walletsAdmin_, WALLETS_ADMIN_ROLE);\n _updateAmlKycValidityDuration(amlKycValidityDuration_);\n }\n\n /**\n * Support of ERC165\n * @dev See https://eips.ethereum.org/EIPS/eip-165\n * @param interfaceId The interface identifier, as specified in ERC-165\n */\n function supportsInterface(\n bytes4 interfaceId\n ) public view override returns (bool) {\n return\n interfaceId == INTERFACE_ID || super.supportsInterface(interfaceId);\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n /**\n * @dev Helper function to check if two arrays are equal\n */\n function _arraysEqual(uint256[] memory a, uint256[] memory b) private pure returns (bool) {\n if (a.length != b.length) {\n return false;\n }\n for (uint256 i = 0; i < a.length; i++) {\n if (a[i] != b[i]) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * @dev Helper function to check if an array contains a specific value\n */\n function _arrayContains(uint256[] memory array, uint256 value) private pure returns (bool) {\n for (uint256 i = 0; i < array.length; i++) {\n if (array[i] == value) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * @dev Helper function to remove a value from an array\n */\n function _removeFromArray(uint256[] storage array, uint256 value) private returns (bool) {\n for (uint256 i = 0; i < array.length; i++) {\n if (array[i] == value) {\n // Move the last element to the position of the element to remove\n array[i] = array[array.length - 1];\n array.pop();\n return true;\n }\n }\n return false;\n }\n\n /**\n * @dev Updates the regions for a wallet if they're different from the current values\n * @param owner The wallet address\n * @param newRegions The new regions array\n * @return changed Whether the regions were actually updated\n */\n function _updateRegions(address owner, uint256[] memory newRegions) private returns (bool changed) {\n if (newRegions.length == 0) {\n revert IdentityRegistry_EmptyRegionsArray();\n }\n if (newRegions.length > MAX_REGIONS_COUNT) {\n revert IdentityRegistry_RegionsExceedsMaxCount();\n }\n \n // Check for duplicates in the new regions array\n for (uint256 i = 0; i < newRegions.length; i++) {\n for (uint256 j = i + 1; j < newRegions.length; j++) {\n if (newRegions[i] == newRegions[j]) {\n revert IdentityRegistry_InvalidRegion();\n }\n }\n }\n \n if (!_arraysEqual(_identity[owner].regions, newRegions)) {\n _identity[owner].regions = newRegions;\n emit RegionsSet(_msgSender(), owner, newRegions);\n return true;\n }\n return false;\n }\n\n /**\n * @dev Updates the AML/KYC status for a wallet if it's different from the current value\n * @param owner The wallet address\n * @param passed Whether AML/KYC is passed\n * @param timestamp The timestamp for the change (0 means use block.timestamp)\n * @return changed Whether the AML/KYC status was actually updated\n */\n function _updateAmlKyc(address owner, bool passed, uint256 timestamp) private returns (bool changed) {\n if (timestamp > block.timestamp) {\n revert IdentityRegistry_TimestampInTheFuture();\n }\n uint256 changeTimestamp = timestamp == 0 ? block.timestamp : timestamp;\n \n if (_identity[owner].amlKycPassed != passed || _identity[owner].lastAmlKycChangeTimestamp != changeTimestamp) {\n _identity[owner].amlKycPassed = passed;\n _identity[owner].lastAmlKycChangeTimestamp = changeTimestamp;\n \n if (passed) {\n emit AmlKycPassed(_msgSender(), owner, changeTimestamp);\n } else {\n emit AmlKycFailed(_msgSender(), owner, changeTimestamp);\n }\n return true;\n }\n return false;\n }\n\n /**\n * @dev Updates the accreditation type for a wallet if it's different from the current value\n * @param owner The wallet address\n * @param newAccreditationType The new accreditation type\n * @param timestamp The timestamp for the change (0 means use block.timestamp)\n * @return changed Whether the accreditation was actually updated\n */\n function _updateAccreditation(address owner, uint256 newAccreditationType, uint256 timestamp) private returns (bool changed) {\n if (timestamp > block.timestamp) {\n revert IdentityRegistry_TimestampInTheFuture();\n }\n uint256 changeTimestamp = timestamp == 0 ? block.timestamp : timestamp;\n \n if (_identity[owner].accreditationType != newAccreditationType || _identity[owner].lastAccreditationChangeTimestamp != changeTimestamp) {\n uint256 oldAccreditationType = _identity[owner].accreditationType;\n _identity[owner].accreditationType = newAccreditationType;\n _identity[owner].lastAccreditationChangeTimestamp = changeTimestamp;\n \n if (newAccreditationType == NO_ACCREDITATION) {\n emit AccreditationRevoked(_msgSender(), owner, oldAccreditationType, changeTimestamp);\n } else {\n emit AccreditationGranted(_msgSender(), owner, newAccreditationType, changeTimestamp);\n }\n return true;\n }\n return false;\n }\n\n function _updateAmlKycValidityDuration(uint256 amlKycValidityDuration_) private {\n if (amlKycValidityDuration_ > MAX_VALIDITY_DURATION) {\n revert IdentityRegistry_InvalidAmlKycValidityDuration();\n }\n uint256 oldAmlKycValidityDuration = _amlKycValidityDurationSeconds;\n _amlKycValidityDurationSeconds = amlKycValidityDuration_;\n emit AmlKycValidityDurationSet(_msgSender(), oldAmlKycValidityDuration, amlKycValidityDuration_);\n }\n\n /// @inheritdoc IIdentityRegistry\n function setAmlKycValidityDuration(uint256 amlKycValidityDuration_) external onlyContractAdmin {\n if (amlKycValidityDuration_ == _amlKycValidityDurationSeconds) {\n revert IdentityRegistry_AlreadyHasSameAmlKycValidityDuration();\n }\n _updateAmlKycValidityDuration(amlKycValidityDuration_);\n }\n\n /// @inheritdoc IIdentityRegistry\n function setIdentity(\n address owner,\n IdentityInfo memory info\n ) public override onlyWalletsAdmin {\n IdentityInfo memory oldIdentity = _identity[owner];\n \n bool isNewIdentity = (oldIdentity.regions.length == 0 && \n oldIdentity.accreditationType == NO_ACCREDITATION && \n !oldIdentity.amlKycPassed && \n oldIdentity.lastAmlKycChangeTimestamp == 0 && \n oldIdentity.lastAccreditationChangeTimestamp == 0);\n\n if (isNewIdentity) {\n emit IdentityCreated(_msgSender(), owner);\n }\n\n // Update each field\n _updateRegions(owner, info.regions);\n _updateAmlKyc(owner, info.amlKycPassed, info.lastAmlKycChangeTimestamp);\n _updateAccreditation(owner, info.accreditationType, info.lastAccreditationChangeTimestamp);\n }\n\n /// @inheritdoc IIdentityRegistry\n function batchSetIdentity(address[] memory owners, IdentityInfo memory info) external override onlyWalletsAdmin {\n for (uint256 i = 0; i < owners.length; i++) {\n setIdentity(owners[i], info);\n }\n }\n\n /// @inheritdoc IIdentityRegistry\n function removeIdentity(address owner) external override onlyWalletsAdmin {\n IdentityInfo memory info = _identity[owner];\n if (info.accreditationType == NO_ACCREDITATION\n && info.regions.length == 0\n && !info.amlKycPassed\n && info.lastAmlKycChangeTimestamp == 0\n && info.lastAccreditationChangeTimestamp == 0)\n {\n revert IdentityRegistry_WalletHasNoIdentityToRemove();\n }\n delete _identity[owner];\n emit IdentityRemoved(_msgSender(), owner);\n }\n\n /// @inheritdoc IIdentityRegistry\n function identity(\n address owner\n ) external view override returns (IdentityInfo memory) {\n return _identity[owner];\n }\n\n /// @inheritdoc IIdentityRegistry\n function setRegions(\n address owner,\n uint256[] memory newRegions\n ) external override onlyWalletsAdmin {\n if (!_updateRegions(owner, newRegions)) {\n revert IdentityRegistry_WalletAlreadyHasSameRegions();\n }\n }\n\n /// @inheritdoc IIdentityRegistry\n function addRegion(\n address owner,\n uint256 region\n ) external override onlyWalletsAdmin {\n if (_arrayContains(_identity[owner].regions, region)) {\n revert IdentityRegistry_WalletAlreadyHasRegion();\n }\n if (_identity[owner].regions.length >= MAX_REGIONS_COUNT) {\n revert IdentityRegistry_RegionsExceedsMaxCount();\n }\n \n _identity[owner].regions.push(region);\n emit RegionAdded(_msgSender(), owner, region);\n }\n\n /// @inheritdoc IIdentityRegistry\n function removeRegion(\n address owner,\n uint256 region\n ) external override onlyWalletsAdmin {\n if (!_removeFromArray(_identity[owner].regions, region)) {\n revert IdentityRegistry_WalletDoesNotHaveRegion();\n }\n \n emit RegionRemoved(_msgSender(), owner, region);\n }\n\n /// @inheritdoc IIdentityRegistry\n function grantAmlKyc(address owner, uint256 amlKycTimestamp) external override onlyWalletsAdmin {\n if (!_updateAmlKyc(owner, true, amlKycTimestamp)) {\n revert IdentityRegistry_WalletAlreadyHasSameAmlKycStatus();\n }\n }\n\n /// @inheritdoc IIdentityRegistry\n function revokeAmlKyc(address owner, uint256 amlKycTimestamp) external override onlyWalletsAdmin {\n if (!_updateAmlKyc(owner, false, amlKycTimestamp)) {\n revert IdentityRegistry_WalletAlreadyHasSameAmlKycStatus();\n }\n }\n\n /// @inheritdoc IIdentityRegistry\n function grantAccreditation(\n address owner,\n uint256 _accreditationType,\n uint256 accreditationTimestamp\n ) external override onlyWalletsAdmin {\n if (!_updateAccreditation(owner, _accreditationType, accreditationTimestamp)) {\n revert IdentityRegistry_WalletAlreadyHasSameAccreditationLevel();\n }\n }\n\n /// @inheritdoc IIdentityRegistry\n function revokeAccreditation(\n address owner,\n uint256 accreditationTimestamp\n ) external override onlyWalletsAdmin {\n if (!_updateAccreditation(owner, NO_ACCREDITATION, accreditationTimestamp)) {\n revert IdentityRegistry_WalletAlreadyHasNoAccreditation();\n }\n }\n\n /// @inheritdoc IIdentityRegistry\n function regions(\n address owner\n ) external view override returns (uint256[] memory) {\n return _identity[owner].regions;\n }\n\n /// @inheritdoc IIdentityRegistry\n function hasRegion(\n address owner,\n uint256 region\n ) external view override returns (bool) {\n return _arrayContains(_identity[owner].regions, region);\n }\n\n /// @inheritdoc IIdentityRegistry\n function accreditationType(address owner) external view override returns (uint256) {\n return _identity[owner].accreditationType;\n }\n\n /// @inheritdoc IIdentityRegistry\n function isAmlKycPassed(\n address owner\n ) external view override returns (bool) {\n bool isAmlKycValid = _amlKycValidityDurationSeconds == 0 ? true : (_identity[owner].lastAmlKycChangeTimestamp + _amlKycValidityDurationSeconds) >= block.timestamp;\n\n return _identity[owner].amlKycPassed && isAmlKycValid;\n }\n\n /// @inheritdoc IIdentityRegistry\n function amlKycValidityDuration() external view override returns (uint256) {\n return _amlKycValidityDurationSeconds;\n }\n}\n"},"contracts/InterestPayment.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {ERC20, IERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {Pausable} from \"@openzeppelin/contracts/utils/Pausable.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {Math} from \"@openzeppelin/contracts/utils/math/Math.sol\";\nimport {IRestrictedLockupToken} from \"./interfaces/IRestrictedLockupToken.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport {ISnapshotPeriods} from \"./interfaces/ISnapshotPeriods.sol\";\nimport {IInterestPayment} from \"./interfaces/IInterestPayment.sol\";\nimport {IDividends} from \"./interfaces/IDividends.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title InterestPayment\n * @notice Contract for managing interest payments for loans backed by RestrictedLockupToken\n * @dev Tracks interest accrual based on token holdings and allows claiming of interest payments\n * @dev Use only standard ERC-20 tokens that keep a constant balance. This applies to\n * `paymentToken` and to each dividend token.\n *\n * Do not use these types of token:\n * - a token that subtracts a fee from each transfer;\n * - a token that changes account balances automatically (a rebase token);\n * - an ERC-777 token that calls back into this contract.\n *\n * The functions {fundInterest}, {fundPrincipal} and {fundDividend} reject these tokens. The claim\n * functions and the reclaim functions do not reject them. They send the amount from the internal\n * records. They do not compare that amount with the true token balance. If you use a different\n * type of token, these functions can send too few tokens, or they can fail. The contract has no\n * function that recovers tokens that stay in it.\n */\ncontract InterestPayment is\n IInterestPayment,\n IDividends,\n ReentrancyGuard,\n Pausable,\n ERC2771Context\n{\n // Role constants\n uint8 public constant CONTRACT_ADMIN_ROLE = 1;\n uint8 public constant RESERVE_ADMIN_ROLE = 2;\n uint8 public constant WALLETS_ADMIN_ROLE = 4;\n uint8 public constant TRANSFER_ADMIN_ROLE = 8;\n\n uint256 public constant INTEREST_RATE_PRECISION_FACTOR = 1_000_000_000_000_000_000_000_000; // 10^24\n uint256 private constant BIPS_PRECISION = 10_000_000;\n // Combined precision factor for gas optimization: INTEREST_RATE_PRECISION_FACTOR * BIPS_PRECISION = 10^31\n uint256 private constant INTEREST_RATE_FACTOR_AND_BIPS_PRECISION = INTEREST_RATE_PRECISION_FACTOR * BIPS_PRECISION;\n // Max principal amount per token - combined with MAX_ABSOLUTE_INTEREST_RATE prevents overflow\n // in interestRatePerSecond * principalAmountPerToken multiplication in _calculateInterest\n uint256 public constant MAX_PRINCIPAL_AMOUNT_PER_TOKEN = 100_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000; // 10^47\n // Maximum absolute interest rate (1,000,000% = 10^11) to prevent overflow regardless of maxInterestRate setting\n uint256 public constant MAX_ABSOLUTE_INTEREST_RATE = 100_000_000_000; // 10^11 = 1,000,000%\n \n // Valid duration periods in seconds (360, 365, 366 days)\n uint256 private constant DURATION_360_DAYS = 360 * 24 * 60 * 60; // 31,104,000 seconds\n uint256 private constant DURATION_365_DAYS = 365 * 24 * 60 * 60; // 31,536,000 seconds\n uint256 private constant DURATION_366_DAYS = 366 * 24 * 60 * 60; // 31,622,400 seconds\n\n /// @dev Struct to hold constructor parameters to avoid stack too deep issues\n struct ConstructorParams {\n address accessControl; // external access control address\n address restrictedLockupTokenAddress;\n address trustedForwarder;\n // The token for the interest payments and the principal payments. The contract keeps it\n // in the immutable variable `paymentToken_`. You cannot change it after deployment. Use\n // only a standard ERC-20 token that keeps a constant balance. Refer to the note above.\n address paymentToken;\n uint256 paymentPeriodSeconds;\n uint256 principalAmountPerToken;\n uint256 interestAccrualStartTimestamp;\n uint256 interestAccrualEndTimestamp;\n uint256 maxInterestRate;\n }\n\n IRestrictedLockupToken private immutable restrictedLockupToken_;\n ISnapshotPeriods public immutable snapshotPeriods;\n IERC20 private immutable paymentToken_;\n uint256 public maxInterestRate;\n uint256 private immutable principalAmountPerToken_;\n uint256 public interestRatePeriodSeconds;\n uint256 public interestAccrualStartTimestamp;\n uint256 public interestAccrualEndTimestamp;\n uint256 public paymentPausedAfterTimestamp;\n address public reclaimerAddress;\n using SafeERC20 for IERC20;\n IAccessControl public accessControl;\n\n /// @dev Packed into 2 storage slots:\n /// slot 1 (HOT, write-once after create/updateRate):\n /// uint48 startTimestamp + uint48 endTimestamp + uint112 interestRatePerSecond\n /// + uint40 interestRate + uint8 paused = 256 bits.\n /// slot 2 (write-heavy, claim/reclaim flows):\n /// uint128 totalClaimedInterest + uint128 totalReclaimedInterest = 256 bits.\n /// Bounds (enforced at write):\n /// - timestamps fit uint48 (year ~8.9M)\n /// - interestRate ≤ MAX_ABSOLUTE_INTEREST_RATE = 10^11 fits uint40 (max ~1.1e12)\n /// - interestRatePerSecond = interestRate * 10^24 / duration; with the above\n /// bounds peak ≈ 3.3e27, fits uint112 (max ~5.2e33)\n /// - totalClaimedInterest and totalReclaimedInterest hold payment-token amounts.\n /// Each increase of these two fields decreases _totalInterestAmountUnused by the\n /// same value. Only fundInterest increases _totalInterestAmountUnused, and\n /// fundInterest limits _totalInterestAmountFunded to 2^128-1. Therefore these\n /// two fields stay less than 2^128, and they do not overflow.\n /// ABI: public auto-getter is wire-compatible with consumers declaring\n /// (uint256 × 6, bool) returns — every value type ABI-encodes as a 32-byte word.\n struct PaymentPeriod {\n uint48 startTimestamp;\n uint48 endTimestamp;\n uint112 interestRatePerSecond;\n uint40 interestRate;\n bool paused;\n uint128 totalClaimedInterest;\n uint128 totalReclaimedInterest;\n }\n\n struct AccountInfo {\n uint256 claimedInterest;\n uint256 reclaimedInterest;\n }\n\n /// @dev Dividend-related structures (adapted from Dividends contract)\n struct FundedDividends {\n uint256 totalFunds; // Total dividend tokens\n uint256 unusedFunds; // Unused dividend tokens\n uint256 dividendPerSecurity; // Dividend token per security token\n }\n\n uint256 private _fundedPrincipalAmount;\n uint256 private _totalPrincipalAmountClaimed;\n uint256 private _totalPrincipalAmountReclaimed;\n uint256 private _totalPrincipalAmountUnused;\n\n uint256 private _totalInterestAmountFunded;\n uint256 private _totalInterestAmountUnused;\n uint256 private _totalInterestAmountClaimed;\n uint256 private _totalInterestAmountReclaimed;\n\n PaymentPeriod[] public paymentPeriods;\n\n // @dev periodIdx => receiver => amount of claimed and reclaimed interest funds\n mapping(uint256 => mapping(address => AccountInfo))\n private accountInfoByPeriods; // claimed interest funds of ERC20\n\n /// @dev Dividend-related mappings (adapted from Dividends contract)\n /// @dev timestamp => token => receiver => amount of claimed dividend funds\n mapping(uint256 => mapping(address => mapping(address => uint256)))\n internal dividendClaimedFunds; // claimed dividend funds of ERC20\n \n /// @dev timestamp => token addr => FundedDividends struct\n mapping(uint256 => mapping(address => FundedDividends))\n internal fundedDividends;\n\n // Errors\n error InterestPayment_InvalidRestrictedLockupTokenAddress();\n error InterestPayment_InvalidPaymentPeriodSeconds();\n error InterestPayment_InvalidAmount();\n error InterestPayment_TokenSupplyIsZero();\n error InterestPayment_PrincipalAmountNotDivisibleByTokenSupply();\n error InterestPayment_InvalidFeeApplied();\n error InterestPayment_InvalidUnclaimedAmount(\n uint256 claimableFunds,\n uint256 claimedFunds\n );\n error InterestPayment_InvalidPeriod();\n error InterestPayment_InvalidPeriodIndex();\n error InterestPayment_StartTimestampGreaterThanEndTimestamp();\n error InterestPayment_StartTimestampBeforeAccrualStartTimestamp();\n error InterestPayment_EndTimestampGreaterThanAccrualEndTimestamp();\n error InterestPayment_PeriodDurationNotMultipleOfInterestRatePeriod();\n error InterestPayment_PeriodAlreadyExists();\n error InterestPayment_PeriodNotNext();\n error InterestPayment_NoPaymentPeriods();\n error InterestPayment_NoFundsToClaim();\n error InterestPayment_NotEnoughFundsToClaim();\n error InterestPayment_NotEnoughFundedPrincipal();\n error InterestPayment_PaymentNotPausedAfter();\n error InterestPayment_CannotUnpauseAfterMaturity();\n error InterestPayment_CannotReclaimAllForOngoingPeriod();\n error InterestPayment_InvalidTimestamp();\n error InterestPayment_InvalidInterestAccrualStartTimestamp();\n error InterestPayment_InvalidInterestAccrualEndTimestamp();\n error InterestPayment_InvalidInterestAccrualPeriod();\n error InterestPayment_InvalidPrincipalAmountPerToken();\n error InterestPayment_InvalidReclaimerAddress();\n error InterestPayment_PaymentPeriodPaused(uint256 periodIdx);\n error InterestPayment_MaturityNotReached();\n error InterestPayment_InvalidPaymentToken();\n error InterestPayment_InterestRateGreaterThanMaxInterestRate();\n error InterestPayment_InterestRateExceedsAbsoluteMax(uint256 provided, uint256 maximum);\n error InterestPayment_PeriodAlreadyStarted();\n error InterestPayment_InterestRateNotChanged();\n error InterestPayment_MaxInterestRateNotChanged();\n error InterestPayment_InvalidInterestRatePeriodDuration();\n error InterestPayment_PrincipalAmountPerTokenTooLarge(uint256 provided, uint256 maximum);\n error InterestPayment_PrincipalFundingExceedsEntitlement(uint256 provided, uint256 maximum);\n error InterestPayment_PrincipalNotFullyFunded(uint256 funded, uint256 required);\n error InterestPayment_IncompatibleTokenDecimals(uint8 securityTokenDecimals, uint8 paymentTokenDecimals);\n error InterestPayment_InvalidRecipientAddress();\n error InterestPayment_FieldOverflow();\n\n // Dividend-related errors (adapted from Dividends contract)\n error InterestPayment_InvalidDividendSnapshotId();\n error InterestPayment_NoRemainingUnclaimedDividendBalance();\n error InterestPayment_InvalidDividendTokenAddress();\n error InterestPayment_InvalidTokenDecimals(uint8 restrictedLockupTokenDecimals);\n error InterestPayment_IndivisibleAmount(\n uint256 paymentTokenAmount,\n uint256 totalSupplyAt\n );\n error InterestPayment_InvalidUnclaimedDividendBalance(\n uint256 claimableFunds,\n uint256 claimedFunds\n );\n error InterestPayment_ZeroTotalSupply();\n error InterestPayment_DividendsAlreadyClaimed();\n error InterestPayment_InvalidArrayLengths();\n\n modifier onlyValidPeriod(uint256 periodIdx_) {\n if (periodIdx_ >= paymentPeriods.length) {\n revert InterestPayment_InvalidPeriodIndex();\n }\n _;\n }\n\n modifier onlyValidReclaimerAddress(address reclaimerAddress_) {\n if (reclaimerAddress_ == address(0)) {\n revert InterestPayment_InvalidReclaimerAddress();\n }\n _;\n }\n\n /// @dev Reject the contract itself as a force-claim recipient. Force-claim paths\n /// debit the claim accounting and then safeTransfer to the caller-supplied\n /// wallet; a self-transfer is a balance-preserving no-op, so the funds would\n /// be recorded as paid out while staying in the contract with no sweep path.\n modifier onlyValidClaimRecipient(address wallet) {\n if (wallet == address(this)) {\n revert InterestPayment_InvalidRecipientAddress();\n }\n _;\n }\n\n modifier whenPeriodPaymentNotPaused(uint256 periodIdx) {\n if (paymentPeriodPaused(periodIdx)) {\n revert InterestPayment_PaymentPeriodPaused(periodIdx);\n }\n _;\n }\n\n modifier onlyValidAmount(uint256 amount) {\n if (amount == 0) {\n revert InterestPayment_InvalidAmount();\n }\n _;\n }\n\n modifier onlyValidDividendSnapshotId(uint256 timestamp_) {\n if (timestamp_ == 0 || timestamp_ >= block.timestamp) {\n revert InterestPayment_InvalidDividendSnapshotId();\n }\n _;\n }\n\n modifier onlyValidInterestRatePeriodDuration(uint256 interestRatePeriodDuration_) {\n if (interestRatePeriodDuration_ != DURATION_360_DAYS && \n interestRatePeriodDuration_ != DURATION_365_DAYS && \n interestRatePeriodDuration_ != DURATION_366_DAYS) {\n revert InterestPayment_InvalidInterestRatePeriodDuration();\n }\n _;\n }\n\n constructor(\n ConstructorParams memory params\n ) ReentrancyGuard() ERC2771Context(params.trustedForwarder) {\n if (params.restrictedLockupTokenAddress == address(0)) {\n revert InterestPayment_InvalidRestrictedLockupTokenAddress();\n }\n if (params.paymentPeriodSeconds == 0) {\n revert InterestPayment_InvalidPaymentPeriodSeconds();\n }\n if (params.interestAccrualStartTimestamp == 0) {\n revert InterestPayment_InvalidInterestAccrualStartTimestamp();\n }\n if (params.interestAccrualEndTimestamp == 0) {\n revert InterestPayment_InvalidInterestAccrualEndTimestamp();\n }\n if (\n params.interestAccrualStartTimestamp >=\n params.interestAccrualEndTimestamp\n ) {\n revert InterestPayment_InvalidInterestAccrualPeriod();\n }\n if (params.principalAmountPerToken == 0) {\n revert InterestPayment_InvalidPrincipalAmountPerToken();\n }\n if (params.principalAmountPerToken > MAX_PRINCIPAL_AMOUNT_PER_TOKEN) {\n revert InterestPayment_PrincipalAmountPerTokenTooLarge(params.principalAmountPerToken, MAX_PRINCIPAL_AMOUNT_PER_TOKEN);\n }\n if (params.paymentToken == address(0)) {\n revert InterestPayment_InvalidPaymentToken();\n }\n\n // set external access control\n if (params.accessControl == address(0)) {\n revert EasyAccessControl_InvalidZeroAddress();\n }\n\n accessControl = IAccessControl(params.accessControl);\n interestAccrualStartTimestamp = params.interestAccrualStartTimestamp;\n interestAccrualEndTimestamp = params.interestAccrualEndTimestamp;\n interestRatePeriodSeconds = params.paymentPeriodSeconds;\n maxInterestRate = params.maxInterestRate;\n principalAmountPerToken_ = params.principalAmountPerToken;\n\n restrictedLockupToken_ = IRestrictedLockupToken(params.restrictedLockupTokenAddress);\n snapshotPeriods = ISnapshotPeriods(\n restrictedLockupToken_.snapshotPeriodsAddress()\n );\n paymentToken_ = IERC20(params.paymentToken);\n\n // `principalAmountPerToken` is denominated in payment-token base units per BASE UNIT of the\n // security token (see `requiredPrincipalFunding`). If the security token carried more\n // decimals than the payment token, no integer value could express a face value that is a\n // whole multiple of `10 ** securityTokenDecimals`, so reject that pairing at deployment.\n // Mirrors the same guard on the dividend path in `fundDividend`.\n if (params.paymentToken.code.length == 0) {\n revert InterestPayment_InvalidPaymentToken();\n }\n uint8 _paymentTokenDecimals;\n try ERC20(params.paymentToken).decimals() returns (uint8 decimals_) {\n _paymentTokenDecimals = decimals_;\n } catch {\n // A payment token that does not expose decimals() cannot be validated against the\n // security token's, and the face-value arithmetic depends on that relationship.\n revert InterestPayment_InvalidPaymentToken();\n }\n uint8 _restrictedLockupTokenDecimals = restrictedLockupToken_.decimals();\n if (_restrictedLockupTokenDecimals > _paymentTokenDecimals) {\n revert InterestPayment_IncompatibleTokenDecimals(\n _restrictedLockupTokenDecimals,\n _paymentTokenDecimals\n );\n }\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n /// @inheritdoc IInterestPayment\n function paymentPeriodPaused(\n uint256 periodIdx\n ) public view override returns (bool) {\n return paymentPeriods[periodIdx].paused;\n }\n\n // Access control modifiers (using external accessControl)\n modifier onlyContractAdminOrTransferAdmin() {\n _onlyContractAdminOrTransferAdmin();\n _;\n }\n modifier onlyContractAdmin() {\n _onlyContractAdmin();\n _;\n }\n modifier onlyTransferAdmin() {\n _onlyTransferAdmin();\n _;\n }\n function _onlyTransferAdmin() internal view {\n if (!accessControl.hasRole(_msgSender(), TRANSFER_ADMIN_ROLE)) {\n revert EasyAccessControl_DoesNotHaveTransferAdminRole(_msgSender());\n }\n }\n function _onlyContractAdminOrTransferAdmin() internal view {\n if (\n !accessControl.hasRole(_msgSender(), CONTRACT_ADMIN_ROLE) &&\n !accessControl.hasRole(_msgSender(), TRANSFER_ADMIN_ROLE)\n ) {\n revert EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(_msgSender());\n }\n }\n function _onlyContractAdmin() internal view {\n if (!accessControl.hasRole(_msgSender(), CONTRACT_ADMIN_ROLE)) {\n revert EasyAccessControl_DoesNotHaveContractAdminRole(_msgSender());\n }\n }\n\n /// @inheritdoc IInterestPayment\n function pausePaymentPeriod(\n uint256 periodIdx\n )\n external\n override\n onlyContractAdminOrTransferAdmin\n onlyValidPeriod(periodIdx)\n {\n paymentPeriods[periodIdx].paused = true;\n emit PeriodPaused(_msgSender(), periodIdx);\n }\n\n /// @inheritdoc IInterestPayment\n function unpausePaymentPeriod(\n uint256 periodIdx\n )\n external\n override\n onlyContractAdminOrTransferAdmin\n onlyValidPeriod(periodIdx)\n {\n paymentPeriods[periodIdx].paused = false;\n emit PeriodUnpaused(_msgSender(), periodIdx);\n }\n\n /**\n * @dev pause contract. Only contract or transfer admin.\n */\n function pause(bool isPaused_) external onlyContractAdminOrTransferAdmin {\n if (isPaused_) {\n _pause();\n } else {\n _unpause();\n }\n }\n\n function _pausePaymentAfterTimestamp(uint256 timestamp) internal {\n if (timestamp == 0 || timestamp < block.timestamp) {\n revert InterestPayment_InvalidTimestamp();\n }\n if (timestamp < interestAccrualStartTimestamp) {\n revert InterestPayment_InvalidTimestamp();\n }\n paymentPausedAfterTimestamp = timestamp;\n\n emit PaymentPausedAfter(timestamp);\n }\n\n /**\n * @dev pause contract. Only contract or transfer admin.\n */\n function pausePaymentAfter(\n uint256 timestamp\n ) external onlyContractAdminOrTransferAdmin {\n _pausePaymentAfterTimestamp(timestamp);\n }\n\n function unpausePaymentAfter() external onlyContractAdminOrTransferAdmin {\n if (paymentPausedAfterTimestamp == 0) {\n revert InterestPayment_PaymentNotPausedAfter();\n }\n if (block.timestamp > interestAccrualEndTimestamp) {\n revert InterestPayment_CannotUnpauseAfterMaturity();\n }\n paymentPausedAfterTimestamp = 0;\n\n emit PaymentUnpausedAfter();\n }\n\n /// @inheritdoc IInterestPayment\n function setReclaimerAddress(\n address newReclaimerAddress\n ) external onlyContractAdmin {\n // Reject the contract itself: reclaims safeTransfer to reclaimerAddress while\n // accounting records the funds as paid out, so a self-reclaimer would strand\n // tokens in the contract with no sweep path. address(0) stays allowed - it\n // disables reclaiming via onlyValidReclaimerAddress at call time.\n if (newReclaimerAddress == address(this)) {\n revert InterestPayment_InvalidReclaimerAddress();\n }\n reclaimerAddress = newReclaimerAddress;\n\n emit ReclaimerAddressChanged(_msgSender(), newReclaimerAddress);\n }\n\n function _updateInterestAccrualEnd(uint256 newInterestAccrualEndTimestamp) internal {\n interestAccrualEndTimestamp = newInterestAccrualEndTimestamp;\n\n emit InterestAccrualEndShifted(\n _msgSender(),\n newInterestAccrualEndTimestamp\n );\n }\n\n /// @inheritdoc IInterestPayment\n function shiftInterestAccrualEnd(\n uint256 newInterestAccrualEndTimestamp\n ) external onlyContractAdminOrTransferAdmin {\n if (newInterestAccrualEndTimestamp <= interestAccrualEndTimestamp) {\n revert InterestPayment_InvalidInterestAccrualEndTimestamp();\n }\n _updateInterestAccrualEnd(newInterestAccrualEndTimestamp);\n }\n\n function setMaxInterestRate(\n uint256 maxInterestRate_\n ) external onlyContractAdmin {\n if (maxInterestRate_ == maxInterestRate) {\n revert InterestPayment_MaxInterestRateNotChanged();\n }\n maxInterestRate = maxInterestRate_;\n\n emit SetMaxInterestRate(_msgSender(), maxInterestRate_);\n }\n\n function updateInterestRateForPeriod(\n uint256 periodIdx,\n uint256 interestRate_,\n uint256 interestRatePeriodDuration\n )\n external\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidPeriod(periodIdx)\n onlyValidInterestRatePeriodDuration(interestRatePeriodDuration)\n {\n // Always enforce absolute maximum to prevent overflow in _calculateInterest\n if (interestRate_ > MAX_ABSOLUTE_INTEREST_RATE) {\n revert InterestPayment_InterestRateExceedsAbsoluteMax(interestRate_, MAX_ABSOLUTE_INTEREST_RATE);\n }\n // Also check against configured maxInterestRate if set\n if (maxInterestRate != 0 && interestRate_ > maxInterestRate) {\n revert InterestPayment_InterestRateGreaterThanMaxInterestRate();\n }\n if (block.timestamp > paymentPeriods[periodIdx].startTimestamp) {\n revert InterestPayment_PeriodAlreadyStarted();\n }\n if (interestRate_ == paymentPeriods[periodIdx].interestRate) {\n revert InterestPayment_InterestRateNotChanged();\n }\n uint256 interestRatePerSecond = calculateInterestRatePerSecond(\n interestRate_,\n interestRatePeriodDuration\n );\n paymentPeriods[periodIdx].interestRatePerSecond = _toUint112(interestRatePerSecond);\n paymentPeriods[periodIdx].interestRate = uint40(interestRate_); // bounded by MAX_ABSOLUTE_INTEREST_RATE\n\n emit InterestRateUpdated(_msgSender(), periodIdx, interestRate_);\n }\n\n function totalInterestAmountFunded() public view returns (uint256) {\n return _totalInterestAmountFunded;\n }\n\n function totalInterestAmountUnused() public view returns (uint256) {\n return _totalInterestAmountUnused;\n }\n\n function totalInterestAmountClaimed() public view returns (uint256) {\n return _totalInterestAmountClaimed;\n }\n\n function totalInterestAmountReclaimed() public view returns (uint256) {\n return _totalInterestAmountReclaimed;\n }\n\n /// @inheritdoc IInterestPayment\n function accruedInterest(\n address account\n ) public view override returns (uint256) {\n return accruedInterestAt(account, block.timestamp);\n }\n\n /// @inheritdoc IInterestPayment\n function accruedInterestAt(\n address account,\n uint256 timestamp\n ) public view override returns (uint256) {\n uint256 endTimestamp = nearestInterestPaymentTimestampAt(timestamp);\n if (endTimestamp <= interestAccrualStartTimestamp) {\n return 0;\n }\n\n uint256 accruedInterestAmount = 0;\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n PaymentPeriod memory period = paymentPeriods[i];\n uint256 accountOwnershipForPeriod = 0;\n if (endTimestamp >= period.endTimestamp) {\n // fully accrued period\n accountOwnershipForPeriod = snapshotPeriods.ownershipForPeriod(\n address(restrictedLockupToken_),\n account,\n period.startTimestamp,\n period.endTimestamp\n );\n } else if (endTimestamp > period.startTimestamp) {\n accountOwnershipForPeriod = snapshotPeriods.ownershipForPeriod(\n address(restrictedLockupToken_),\n account,\n period.startTimestamp,\n endTimestamp\n );\n }\n accruedInterestAmount += _calculateInterest(\n period.interestRatePerSecond,\n accountOwnershipForPeriod\n );\n if (endTimestamp <= period.startTimestamp) {\n break;\n }\n }\n\n return accruedInterestAmount;\n }\n\n // it can be calculated\n // as totalAccruedInterestAt(period.endTimestamp) - totalAccruedInterestAt(period.startTimestamp)\n // or as is ownershipForPeriod * period.interestRatePerSecond * principalAmountPerToken / PRECISION_FACTOR / BIPS_PRECISION\n function accruedInterestForPeriod(\n address account,\n uint256 periodIdx\n ) public view returns (uint256) {\n uint256 endTimestamp = nearestInterestPaymentTimestampAt(\n block.timestamp\n );\n PaymentPeriod memory period = paymentPeriods[periodIdx];\n if (endTimestamp <= period.startTimestamp) {\n return 0;\n }\n if (endTimestamp >= period.endTimestamp) {\n endTimestamp = period.endTimestamp;\n }\n uint256 ownershipForPeriodAmount = snapshotPeriods.ownershipForPeriod(\n address(restrictedLockupToken_),\n account,\n period.startTimestamp,\n endTimestamp\n );\n\n return\n _calculateInterest(\n period.interestRatePerSecond,\n ownershipForPeriodAmount\n );\n }\n\n /// @inheritdoc IInterestPayment\n function totalAccruedInterest() external view returns (uint256) {\n return totalAccruedInterestAt(block.timestamp);\n }\n\n /// @inheritdoc IInterestPayment\n function totalAccruedInterestAt(\n uint256 timestamp\n ) public view override returns (uint256 totalAmount) {\n uint256 endTimestamp = timestamp > block.timestamp\n ? nearestInterestPaymentTimestampAt(block.timestamp)\n : nearestInterestPaymentTimestampAt(timestamp);\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n PaymentPeriod memory period = paymentPeriods[i];\n uint256 totalOwnershipForPeriod = 0;\n if (endTimestamp >= period.endTimestamp) {\n // fully accrued period\n totalOwnershipForPeriod = snapshotPeriods\n .totalOwnershipForPeriod(\n address(restrictedLockupToken_),\n period.startTimestamp,\n period.endTimestamp\n );\n } else if (endTimestamp > period.startTimestamp) {\n totalOwnershipForPeriod = snapshotPeriods\n .totalOwnershipForPeriod(\n address(restrictedLockupToken_),\n period.startTimestamp,\n endTimestamp\n );\n }\n totalAmount += _calculateInterest(\n period.interestRatePerSecond,\n totalOwnershipForPeriod\n );\n if (endTimestamp <= period.startTimestamp) {\n break;\n }\n }\n }\n\n function _calculateInterest(\n uint256 interestRatePerSecond,\n uint256 ownershipForPeriod\n ) internal view returns (uint256) {\n // Single mulDiv for maximum precision - only one rounding operation\n // Computes: (interestRatePerSecond * principalAmountPerToken_ * ownershipForPeriod) / (PRECISION * BIPS)\n return Math.mulDiv(\n interestRatePerSecond * principalAmountPerToken_,\n ownershipForPeriod,\n INTEREST_RATE_FACTOR_AND_BIPS_PRECISION\n );\n }\n\n // @inheritdoc IInterestPayment\n /// @dev Rounds `timestamp` down to the most recent payment boundary. This rounding makes the\n /// accrual and claim paths pay whole payment periods only.\n ///\n /// Keep the order below: apply the paymentPausedAfterTimestamp clamp before the\n /// interestAccrualEndTimestamp check. If the maturity check reads the raw `timestamp`\n /// first, pausePaymentAfter() has no effect after maturity, and interest accrues through\n /// the paused window. earlyRepayment() sets both values equal and hides this fault.\n /// test/unit/interest_payment/pausePaymentAfter.test.js catches it.\n function nearestInterestPaymentTimestampAt(\n uint256 timestamp\n ) public view returns (uint256) {\n if (timestamp < interestAccrualStartTimestamp) {\n return interestAccrualStartTimestamp;\n }\n uint256 effectiveTimestamp = timestamp;\n if (\n paymentPausedAfterTimestamp > 0 &&\n timestamp > paymentPausedAfterTimestamp\n ) {\n effectiveTimestamp = paymentPausedAfterTimestamp;\n }\n if (effectiveTimestamp >= interestAccrualEndTimestamp) {\n return interestAccrualEndTimestamp;\n }\n\n uint256 paymentTimestamp = effectiveTimestamp -\n ((effectiveTimestamp - interestAccrualStartTimestamp) %\n interestRatePeriodSeconds);\n\n return paymentTimestamp;\n }\n\n // @inheritdoc IInterestPayment\n function findPaymentPeriodIndex(\n uint256 startTimestamp,\n uint256 endTimestamp\n ) public view returns (uint256) {\n if (paymentPeriods.length == 0) {\n return type(uint256).max;\n }\n\n uint256 left = 0;\n uint256 right = paymentPeriods.length;\n\n while (left < right) {\n uint256 mid = left + (right - left) / 2;\n if (paymentPeriods[mid].startTimestamp < startTimestamp) {\n left = mid + 1;\n } else {\n right = mid;\n }\n }\n if (left == paymentPeriods.length) {\n return type(uint256).max;\n }\n\n if (\n paymentPeriods[left].startTimestamp == startTimestamp &&\n paymentPeriods[left].endTimestamp == endTimestamp\n ) {\n return left;\n }\n\n return type(uint256).max; // Return max if not found\n }\n\n /// @dev fund interest for a period\n /// @param startTimestamp start timestamp\n /// @param endTimestamp end timestamp\n /// @param interestRate_ interest rate in BIPS\n /// @param interestRatePeriodDuration interest rate period duration\n function createPaymentPeriod(\n uint256 startTimestamp,\n uint256 endTimestamp,\n uint256 interestRate_,\n uint256 interestRatePeriodDuration\n ) external nonReentrant onlyTransferAdmin whenNotPaused onlyValidInterestRatePeriodDuration(interestRatePeriodDuration) {\n if (startTimestamp >= endTimestamp) {\n revert InterestPayment_StartTimestampGreaterThanEndTimestamp();\n }\n if (startTimestamp < interestAccrualStartTimestamp) {\n revert InterestPayment_StartTimestampBeforeAccrualStartTimestamp();\n }\n if (endTimestamp > interestAccrualEndTimestamp) {\n revert InterestPayment_EndTimestampGreaterThanAccrualEndTimestamp();\n }\n if ((endTimestamp - startTimestamp) % interestRatePeriodSeconds != 0) {\n if (endTimestamp != interestAccrualEndTimestamp) {\n revert InterestPayment_PeriodDurationNotMultipleOfInterestRatePeriod();\n }\n }\n\n uint256 accrualPeriodIdx = findPaymentPeriodIndex(\n startTimestamp,\n endTimestamp\n );\n if (accrualPeriodIdx != type(uint256).max) {\n revert InterestPayment_PeriodAlreadyExists();\n }\n // no period found -> create new period\n // but check if it is next period after last funded period or startTimestamp is accrual start timestamp\n if (paymentPeriods.length > 0) {\n if (\n startTimestamp !=\n paymentPeriods[paymentPeriods.length - 1].endTimestamp\n ) {\n revert InterestPayment_PeriodNotNext();\n }\n } else {\n if (startTimestamp != interestAccrualStartTimestamp) {\n revert InterestPayment_PeriodNotNext();\n }\n }\n\n _createPaymentPeriod(\n startTimestamp,\n endTimestamp,\n interestRate_,\n interestRatePeriodDuration\n );\n }\n\n function _createPaymentPeriod(\n uint256 startTimestamp,\n uint256 endTimestamp,\n uint256 interestRate_,\n uint256 interestRatePeriodDuration\n ) internal {\n // Always enforce absolute maximum to prevent overflow in _calculateInterest\n if (interestRate_ > MAX_ABSOLUTE_INTEREST_RATE) {\n revert InterestPayment_InterestRateExceedsAbsoluteMax(interestRate_, MAX_ABSOLUTE_INTEREST_RATE);\n }\n // Also check against configured maxInterestRate if set\n if (maxInterestRate != 0 && interestRate_ > maxInterestRate) {\n revert InterestPayment_InterestRateGreaterThanMaxInterestRate();\n }\n uint256 accrualPeriodIdx = paymentPeriods.length;\n uint256 interestRatePerSecond = calculateInterestRatePerSecond(\n interestRate_,\n interestRatePeriodDuration\n );\n\n paymentPeriods.push(\n PaymentPeriod({\n startTimestamp: _toUint48(startTimestamp),\n endTimestamp: _toUint48(endTimestamp),\n interestRatePerSecond: _toUint112(interestRatePerSecond),\n interestRate: uint40(interestRate_), // bounded above by MAX_ABSOLUTE_INTEREST_RATE (10^11) < 2^40\n paused: false,\n totalClaimedInterest: 0,\n totalReclaimedInterest: 0\n })\n );\n\n emit PaymentPeriodCreated(\n _msgSender(),\n accrualPeriodIdx,\n startTimestamp,\n endTimestamp,\n interestRate_\n );\n }\n\n function calculateInterestRatePerSecond(\n uint256 interestRate_,\n uint256 interestRatePeriodDuration\n ) internal pure returns (uint256) {\n return Math.mulDiv(\n interestRate_,\n INTEREST_RATE_PRECISION_FACTOR,\n interestRatePeriodDuration\n );\n }\n\n /// @dev Narrow uint256 to uint48 (timestamp); reverts on overflow. Year ~8.9M cap.\n function _toUint48(uint256 v) internal pure returns (uint48) {\n if (v > type(uint48).max) revert InterestPayment_FieldOverflow();\n return uint48(v);\n }\n\n /// @dev Narrow uint256 to uint112 (interestRatePerSecond); reverts on overflow.\n /// Today's bounds (interestRate ≤ 10^11, duration ≥ 360 days) keep peak ≈ 3.3e27,\n /// well below 2^112 ≈ 5.2e33. Explicit guard preserves the invariant if\n /// MAX_ABSOLUTE_INTEREST_RATE is ever raised.\n function _toUint112(uint256 v) internal pure returns (uint112) {\n if (v > type(uint112).max) revert InterestPayment_FieldOverflow();\n return uint112(v);\n }\n\n /// @dev Narrow uint256 to uint128 for accumulator slots (totalClaimed/Reclaimed).\n function _toUint128(uint256 v) internal pure returns (uint128) {\n if (v > type(uint128).max) revert InterestPayment_FieldOverflow();\n return uint128(v);\n }\n\n /// @inheritdoc IInterestPayment\n /// @dev This function makes sure that the token is of the correct type. It reads the contract\n /// balance before the transfer and after the transfer. If the increase is not the same as the\n /// amount, the function fails with `InterestPayment_InvalidFeeApplied`. The check is exact. It\n /// rejects a transfer fee. It also rejects an unexpected increase of the balance.\n ///\n /// The claim functions and the reclaim functions do not do this check. This is correct. If this\n /// function fails, no tokens stay in the contract. If a claim function fails, the tokens stay in\n /// the contract, and no function can recover them.\n function fundInterest(\n uint256 amount\n )\n external\n nonReentrant\n whenNotPaused\n onlyValidAmount(amount)\n {\n _totalInterestAmountFunded += amount;\n // The period fields totalClaimedInterest and totalReclaimedInterest are uint128.\n // Each increase of these two fields decreases _totalInterestAmountUnused by the same\n // value, and only this function increases _totalInterestAmountUnused. Therefore this\n // limit on _totalInterestAmountFunded prevents an overflow of these two fields.\n if (_totalInterestAmountFunded > type(uint128).max) {\n revert InterestPayment_FieldOverflow();\n }\n _totalInterestAmountUnused += amount;\n\n uint256 _balanceBefore = paymentToken_.balanceOf(address(this));\n paymentToken_.safeTransferFrom(_msgSender(), address(this), amount);\n uint256 _balanceAfter = paymentToken_.balanceOf(address(this));\n if (_balanceBefore + amount != _balanceAfter) {\n revert InterestPayment_InvalidFeeApplied();\n }\n\n emit Funded(_msgSender(), amount);\n }\n\n /// @inheritdoc IInterestPayment\n function claimInterestForPeriod(\n uint256 paymentPeriodIdx,\n uint256 amount\n )\n public\n nonReentrant\n onlyValidPeriod(paymentPeriodIdx)\n whenPeriodPaymentNotPaused(paymentPeriodIdx)\n whenNotPaused\n {\n _claimInterestForPeriod(_msgSender(), paymentPeriodIdx, amount, false);\n }\n\n // @inheritdoc IInterestPayment\n function batchClaimInterestForPeriods(\n uint256[] memory paymentPeriodIdxs,\n uint256 amount\n ) external whenNotPaused {\n if (paymentPeriodIdxs.length == 0) {\n revert InterestPayment_InvalidPeriod();\n }\n if (\n amount > _totalInterestAmountUnused ||\n _totalInterestAmountUnused == 0\n ) {\n revert InterestPayment_NoFundsToClaim();\n }\n uint256 totalClaimableAmount = 0;\n for (uint256 i = 0; i < paymentPeriodIdxs.length; i++) {\n if (paymentPeriodIdxs[i] >= paymentPeriods.length) {\n revert InterestPayment_InvalidPeriodIndex();\n }\n if (paymentPeriodPaused(paymentPeriodIdxs[i])) {\n revert InterestPayment_PaymentPeriodPaused(\n paymentPeriodIdxs[i]\n );\n }\n uint256 claimableAmount = unclaimedAmountForPeriod(\n _msgSender(),\n paymentPeriodIdxs[i]\n );\n if (claimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n\n totalClaimableAmount += claimableAmount;\n if (amount > 0 && totalClaimableAmount > amount) {\n uint256 adjustment = totalClaimableAmount - amount;\n claimableAmount -= adjustment;\n totalClaimableAmount = amount;\n }\n claimInterestForPeriod(paymentPeriodIdxs[i], claimableAmount);\n if (amount > 0 && totalClaimableAmount >= amount) {\n break;\n }\n }\n }\n\n function _claimInterest(\n address target,\n uint256 amount,\n bool isForceClaimByAdmin\n ) internal {\n uint256 currentTimestamp = block.timestamp;\n uint256 currentPaymentTimestamp = nearestInterestPaymentTimestampAt(\n currentTimestamp\n );\n\n uint256 totalClaimableAmount = 0;\n\n // Check if there are any funded periods\n if (paymentPeriods.length == 0) {\n revert InterestPayment_NoPaymentPeriods();\n }\n\n // Process each funded period\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n PaymentPeriod storage period = paymentPeriods[i];\n // Check if period is accrued (fully or partially)\n if (currentPaymentTimestamp <= period.startTimestamp) {\n // Period has not started accruing yet\n // we can break here because all subsequent periods will not be accrued\n break;\n }\n uint256 claimableAmount = unclaimedAmountForPeriod(target, i);\n\n // Skip periods with no unused funds if payment period is paused\n if (claimableAmount == 0 || (!isForceClaimByAdmin && paymentPeriodPaused(i))) {\n continue;\n }\n\n totalClaimableAmount += claimableAmount;\n\n // Check if we've reached the amount cap (if provided)\n if (amount > 0 && totalClaimableAmount > amount) {\n // Adjust the claimable amount for this period to respect the cap\n uint256 adjustment = totalClaimableAmount - amount;\n claimableAmount -= adjustment;\n totalClaimableAmount = amount;\n }\n\n // Update claimed funds and unused funds\n period.totalClaimedInterest += _toUint128(claimableAmount);\n accountInfoByPeriods[i][target]\n .claimedInterest += claimableAmount;\n\n if (isForceClaimByAdmin) {\n emit ForceClaimed(_msgSender(), target, claimableAmount, i);\n } else {\n emit Claimed(target, claimableAmount, i);\n }\n\n // If we've reached the amount cap, exit the loop\n if (amount > 0 && totalClaimableAmount >= amount) {\n break;\n }\n }\n\n if (amount > 0 && totalClaimableAmount < amount) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n // Check if we have any funds to claim after processing all periods\n if (totalClaimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n // INTENTIONAL all-or-nothing gate, not a missing clamp: each period is capped against\n // the full pool, so the sum can exceed what we hold and we refuse rather than settle\n // short. Clamping in the loop would silently make this best-effort.\n if (totalClaimableAmount > _totalInterestAmountUnused) {\n revert InterestPayment_NoFundsToClaim();\n }\n _totalInterestAmountUnused -= totalClaimableAmount;\n _totalInterestAmountClaimed += totalClaimableAmount;\n\n // Transfer the total claimable amount\n paymentToken_.safeTransfer(target, totalClaimableAmount);\n }\n\n function _claimInterestForPeriod(\n address target,\n uint256 paymentPeriodIdx,\n uint256 amount,\n bool isForceClaimByAdmin\n ) internal {\n if (\n amount > _totalInterestAmountUnused ||\n _totalInterestAmountUnused == 0\n ) {\n revert InterestPayment_NoFundsToClaim();\n }\n uint256 availableClaimableAmount = unclaimedAmountForPeriod(\n target,\n paymentPeriodIdx\n );\n if (availableClaimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n if (availableClaimableAmount < amount) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n if (amount > 0) {\n // claim specific amount for period (not all funds)\n availableClaimableAmount = amount;\n }\n\n accountInfoByPeriods[paymentPeriodIdx][target]\n .claimedInterest += availableClaimableAmount;\n paymentPeriods[paymentPeriodIdx]\n .totalClaimedInterest += _toUint128(availableClaimableAmount);\n _totalInterestAmountClaimed += availableClaimableAmount;\n _totalInterestAmountUnused -= availableClaimableAmount;\n \n if (isForceClaimByAdmin) {\n emit ForceClaimed(_msgSender(), target, availableClaimableAmount, paymentPeriodIdx);\n } else {\n emit Claimed(target, availableClaimableAmount, paymentPeriodIdx);\n }\n \n paymentToken_.safeTransfer(target, availableClaimableAmount);\n }\n\n /// @inheritdoc IInterestPayment\n /// @notice this function can run out of gas if there are a lot of periods\n /// so claim each period separately\n function claimInterest(uint256 amount) public nonReentrant whenNotPaused {\n _claimInterest(_msgSender(), amount, false);\n }\n\n /// @notice Force claim interest for a specific wallet address\n /// @dev Only callable by Transfer Admin. Similar to claimInterest but claims on behalf of a wallet address and with amount cap\n /// @param wallet The wallet address to claim interest for\n /// @param amount The maximum amount to claim, or 0 for all available funds\n function forceClaim(\n address wallet,\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidClaimRecipient(wallet)\n whenNotPaused\n {\n _claimInterest(wallet, amount, true);\n }\n\n function paymentPeriodsCount() public view returns (uint256) {\n return paymentPeriods.length;\n }\n\n /// @notice Return only the time bounds of a payment period.\n /// @dev Slim alternative to the auto-generated `paymentPeriods(uint256)` getter\n /// for callers that need only the bounds. Reads slot 1 of the packed\n /// PaymentPeriod struct; returns 64 bytes of calldata vs 224.\n function paymentPeriodBounds(\n uint256 periodIdx\n ) external view returns (uint256 startTimestamp, uint256 endTimestamp) {\n PaymentPeriod storage p = paymentPeriods[periodIdx];\n return (p.startTimestamp, p.endTimestamp);\n }\n\n function periodTotalInterest(\n uint256 periodIdx\n ) public view returns (uint256) {\n uint256 endTimestamp = nearestInterestPaymentTimestampAt(\n block.timestamp\n );\n PaymentPeriod memory period = paymentPeriods[periodIdx];\n if (endTimestamp <= period.startTimestamp) {\n return 0;\n }\n if (endTimestamp >= period.endTimestamp) {\n endTimestamp = period.endTimestamp;\n }\n uint256 totalOwnershipForPeriod = snapshotPeriods\n .totalOwnershipForPeriod(\n address(restrictedLockupToken_),\n period.startTimestamp,\n endTimestamp\n );\n\n return\n _calculateInterest(\n period.interestRatePerSecond,\n totalOwnershipForPeriod\n );\n }\n\n function periodTotalClaimedInterest(\n uint256 periodIdx\n ) public view returns (uint256) {\n return paymentPeriods[periodIdx].totalClaimedInterest;\n }\n\n function periodTotalReclaimedInterest(\n uint256 periodIdx\n ) public view returns (uint256) {\n return paymentPeriods[periodIdx].totalReclaimedInterest;\n }\n\n /// @inheritdoc IInterestPayment\n function periodAvailableInterest(\n uint256 periodIdx\n ) public view returns (uint256) {\n uint256 _totalAccruedInterestForPeriod = periodTotalInterest(periodIdx);\n\n if (\n _totalAccruedInterestForPeriod <=\n uint256(paymentPeriods[periodIdx].totalClaimedInterest) +\n uint256(paymentPeriods[periodIdx].totalReclaimedInterest)\n ) {\n return 0;\n }\n\n return\n _totalAccruedInterestForPeriod -\n paymentPeriods[periodIdx].totalClaimedInterest -\n paymentPeriods[periodIdx].totalReclaimedInterest;\n }\n\n function periodDuration(uint256 periodIdx) public view returns (uint256) {\n return\n paymentPeriods[periodIdx].endTimestamp -\n paymentPeriods[periodIdx].startTimestamp;\n }\n\n function periodStartTimestamp(\n uint256 periodIdx\n ) public view returns (uint256) {\n return paymentPeriods[periodIdx].startTimestamp;\n }\n\n function periodEndTimestamp(\n uint256 periodIdx\n ) public view returns (uint256) {\n return paymentPeriods[periodIdx].endTimestamp;\n }\n\n /// @notice Total interest a receiver is owed across every period accrued as of `timestamp`\n /// @dev Sums unclaimedAmountForPeriod(), so this is what is owed, not what is payable now.\n /// A result above totalInterestAmountUnused() is exactly when claimInterest(0) reverts.\n function unclaimedAmountAt(\n address receiver_,\n uint256 timestamp\n ) public view returns (uint256 totalClaimableAmount) {\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n if (paymentPeriods[i].startTimestamp > timestamp) {\n break;\n }\n totalClaimableAmount += unclaimedAmountForPeriod(receiver_, i);\n }\n }\n\n /// @inheritdoc IInterestPayment\n function unclaimedAmountForPeriod(\n address receiver_,\n uint256 paymentPeriodIdx_\n ) public view returns (uint256 _unclaimedAmountForPeriod) {\n uint256 _claimableFunds = accruedInterestForPeriod(\n receiver_,\n paymentPeriodIdx_\n );\n uint256 _usedFunds = usedAmountForPeriod(receiver_, paymentPeriodIdx_);\n // should not happen because we check for available funds to claim first but just in case\n if (_usedFunds > _claimableFunds) {\n revert InterestPayment_InvalidUnclaimedAmount(\n _claimableFunds,\n _usedFunds\n );\n }\n\n _unclaimedAmountForPeriod = _claimableFunds - _usedFunds;\n // if reclaimed all funds for period, we can't change unclaimed balance for specific user\n uint256 totalPeriodUnusedInterest = periodAvailableInterest(\n paymentPeriodIdx_\n );\n // cover reclaimAll for period case\n if (_unclaimedAmountForPeriod > totalPeriodUnusedInterest) {\n _unclaimedAmountForPeriod = totalPeriodUnusedInterest;\n }\n // cover reclaim all contract funds case\n if (_unclaimedAmountForPeriod > _totalInterestAmountUnused) {\n _unclaimedAmountForPeriod = _totalInterestAmountUnused;\n }\n }\n\n // @inheritdoc IInterestPayment\n function claimedAmountForPeriod(\n address receiver_,\n uint256 periodIdx_\n ) public view returns (uint256) {\n return accountInfoByPeriods[periodIdx_][receiver_].claimedInterest;\n }\n\n function accountTotalClaimedAmount(\n address receiver_\n ) public view returns (uint256 totalClaimed) {\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n totalClaimed += accountInfoByPeriods[i][receiver_].claimedInterest;\n }\n return totalClaimed;\n }\n\n function accountTotalReclaimedAmount(\n address receiver_\n ) public view returns (uint256 totalReclaimed) {\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n totalReclaimed += accountInfoByPeriods[i][receiver_]\n .reclaimedInterest;\n }\n return totalReclaimed;\n }\n\n function reclaimedAmountForPeriod(\n address receiver_,\n uint256 periodIdx_\n ) public view returns (uint256) {\n return accountInfoByPeriods[periodIdx_][receiver_].reclaimedInterest;\n }\n\n function usedAmountForPeriod(\n address receiver_,\n uint256 periodIdx_\n ) public view returns (uint256) {\n return\n accountInfoByPeriods[periodIdx_][receiver_].claimedInterest +\n accountInfoByPeriods[periodIdx_][receiver_].reclaimedInterest;\n }\n\n /// @inheritdoc IInterestPayment\n function reclaimInterestForPeriod(\n address wallet,\n uint256 paymentPeriodIdx,\n uint256 amount\n )\n public\n nonReentrant\n onlyTransferAdmin\n onlyValidPeriod(paymentPeriodIdx)\n onlyValidReclaimerAddress(reclaimerAddress)\n whenNotPaused\n {\n if (\n amount > _totalInterestAmountUnused ||\n _totalInterestAmountUnused == 0\n ) {\n revert InterestPayment_NoFundsToClaim();\n }\n\n // Cap available claimable amount to unused funds in the period or total unused funds\n uint256 availableClaimableAmount = unclaimedAmountForPeriod(\n wallet,\n paymentPeriodIdx\n );\n if (availableClaimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n if (availableClaimableAmount < amount) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n if (amount > 0) {\n // claim specific amount for period (not all funds)\n availableClaimableAmount = amount;\n }\n\n accountInfoByPeriods[paymentPeriodIdx][wallet]\n .reclaimedInterest += availableClaimableAmount;\n paymentPeriods[paymentPeriodIdx]\n .totalReclaimedInterest += _toUint128(availableClaimableAmount);\n _totalInterestAmountReclaimed += availableClaimableAmount;\n _totalInterestAmountUnused -= availableClaimableAmount;\n emit Reclaimed(\n _msgSender(),\n wallet,\n availableClaimableAmount,\n paymentPeriodIdx\n );\n paymentToken_.safeTransfer(reclaimerAddress, availableClaimableAmount);\n }\n\n function reclaimInterest(\n address wallet,\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidReclaimerAddress(reclaimerAddress)\n whenNotPaused\n {\n uint256 currentTimestamp = block.timestamp;\n uint256 currentPaymentTimestamp = nearestInterestPaymentTimestampAt(\n currentTimestamp\n );\n\n uint256 totalClaimableAmount = 0;\n\n // Check if there are any funded periods\n if (paymentPeriods.length == 0) {\n revert InterestPayment_NoPaymentPeriods();\n }\n\n // Process each funded period\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n PaymentPeriod storage period = paymentPeriods[i];\n\n // Check if period is accrued (fully or partially)\n if (currentPaymentTimestamp <= period.startTimestamp) {\n // Period has not started accruing yet\n // we can break here because all subsequent periods will not be accrued\n break;\n }\n\n // Cap available claimable amount to unused funds in the period or total unused funds\n uint256 claimableAmount = unclaimedAmountForPeriod(wallet, i);\n // Skip if no available funds to claim\n if (claimableAmount == 0 || period.paused) {\n continue;\n }\n\n // Add to total claimable amount\n totalClaimableAmount += claimableAmount;\n\n // Check if we've reached the amount cap (if provided)\n if (amount > 0 && totalClaimableAmount > amount) {\n // Adjust the claimable amount for this period to respect the cap\n uint256 adjustment = totalClaimableAmount - amount;\n claimableAmount -= adjustment;\n totalClaimableAmount = amount;\n }\n\n // Update claimed funds and unused funds\n period.totalReclaimedInterest += _toUint128(claimableAmount);\n accountInfoByPeriods[i][wallet]\n .reclaimedInterest += claimableAmount;\n\n emit Reclaimed(_msgSender(), wallet, claimableAmount, i);\n\n // If we've reached the amount cap, exit the loop\n if (amount > 0 && totalClaimableAmount >= amount) {\n break;\n }\n }\n\n if (amount > 0 && totalClaimableAmount < amount) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n // Check if we have any funds to claim after processing all periods\n if (totalClaimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n // INTENTIONAL all-or-nothing gate -- see the matching comment in _claimInterest().\n if (totalClaimableAmount > _totalInterestAmountUnused) {\n revert InterestPayment_NoFundsToClaim();\n }\n _totalInterestAmountUnused -= totalClaimableAmount;\n _totalInterestAmountReclaimed += totalClaimableAmount;\n\n // Transfer the total claimable amount\n paymentToken_.safeTransfer(reclaimerAddress, totalClaimableAmount);\n }\n\n function batchReclaimInterest(\n address wallet,\n uint256[] memory paymentPeriodIdxs,\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidReclaimerAddress(reclaimerAddress)\n whenNotPaused\n {\n uint256 currentTimestamp = block.timestamp;\n uint256 currentPaymentTimestamp = nearestInterestPaymentTimestampAt(\n currentTimestamp\n );\n\n uint256 totalClaimableAmount = 0;\n\n if (paymentPeriodIdxs.length == 0) {\n revert InterestPayment_InvalidPeriodIndex();\n }\n\n // Check if there are any funded periods\n if (paymentPeriods.length == 0) {\n revert InterestPayment_NoPaymentPeriods();\n }\n\n // Process each funded period\n for (uint256 i = 0; i < paymentPeriodIdxs.length; i++) {\n if (paymentPeriodIdxs[i] >= paymentPeriods.length) {\n revert InterestPayment_InvalidPeriodIndex();\n }\n PaymentPeriod storage period = paymentPeriods[paymentPeriodIdxs[i]];\n\n // Check if period is accrued (fully or partially)\n if (currentPaymentTimestamp <= period.startTimestamp) {\n // Period has not started accruing yet\n // we can break here because all subsequent periods will not be accrued\n break;\n }\n\n // Cap available claimable amount to unused funds in the period or total unused funds\n uint256 claimableAmount = unclaimedAmountForPeriod(\n wallet,\n paymentPeriodIdxs[i]\n );\n // Skip if no available funds to claim\n if (claimableAmount == 0) {\n continue;\n }\n\n // Add to total claimable amount\n totalClaimableAmount += claimableAmount;\n\n // Check if we've reached the amount cap (if provided)\n if (amount > 0 && totalClaimableAmount > amount) {\n // Adjust the claimable amount for this period to respect the cap\n uint256 adjustment = totalClaimableAmount - amount;\n claimableAmount -= adjustment;\n totalClaimableAmount = amount;\n }\n\n // Update claimed funds and unused funds\n period.totalReclaimedInterest += _toUint128(claimableAmount);\n accountInfoByPeriods[paymentPeriodIdxs[i]][wallet]\n .reclaimedInterest += claimableAmount;\n\n emit Reclaimed(\n _msgSender(),\n wallet,\n claimableAmount,\n paymentPeriodIdxs[i]\n );\n\n // If we've reached the amount cap, exit the loop\n if (amount > 0 && totalClaimableAmount >= amount) {\n break;\n }\n }\n\n if (amount > 0 && totalClaimableAmount < amount) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n // Check if we have any funds to claim after processing all periods\n if (totalClaimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n // INTENTIONAL all-or-nothing gate -- see the matching comment in _claimInterest().\n if (totalClaimableAmount > _totalInterestAmountUnused) {\n revert InterestPayment_NoFundsToClaim();\n }\n _totalInterestAmountUnused -= totalClaimableAmount;\n _totalInterestAmountReclaimed += totalClaimableAmount;\n\n // Transfer the total claimable amount\n paymentToken_.safeTransfer(reclaimerAddress, totalClaimableAmount);\n }\n\n /// @notice Batch force claim interest for specific periods for a wallet\n /// @dev Only callable by Transfer Admin. Similar to batchReclaimInterest but transfers amount to wallet instead of reclaimer.\n /// All-or-nothing, as in `claimInterest`; `forceClaimForPeriod` settles one period.\n /// @param wallet The wallet address to claim interest for\n /// @param paymentPeriodIdxs Array of payment period indices to claim interest from\n /// @param amount The maximum amount to claim, or 0 for all available funds\n function batchForceClaimInterest(\n address wallet,\n uint256[] memory paymentPeriodIdxs,\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidClaimRecipient(wallet)\n whenNotPaused\n {\n uint256 currentTimestamp = block.timestamp;\n uint256 currentPaymentTimestamp = nearestInterestPaymentTimestampAt(\n currentTimestamp\n );\n\n uint256 totalClaimableAmount = 0;\n\n if (paymentPeriodIdxs.length == 0) {\n revert InterestPayment_InvalidPeriodIndex();\n }\n\n // Check if there are any funded periods\n if (paymentPeriods.length == 0) {\n revert InterestPayment_NoPaymentPeriods();\n }\n\n // Process each specified period\n for (uint256 i = 0; i < paymentPeriodIdxs.length; i++) {\n if (paymentPeriodIdxs[i] >= paymentPeriods.length) {\n revert InterestPayment_InvalidPeriodIndex();\n }\n PaymentPeriod storage period = paymentPeriods[paymentPeriodIdxs[i]];\n\n // Check if period is accrued (fully or partially)\n if (currentPaymentTimestamp <= period.startTimestamp) {\n // Period has not started accruing yet\n continue;\n }\n\n // Get available claimable amount for this period\n uint256 claimableAmount = unclaimedAmountForPeriod(\n wallet,\n paymentPeriodIdxs[i]\n );\n // Skip if no available funds to claim\n if (claimableAmount == 0) {\n continue;\n }\n\n // Add to total claimable amount\n totalClaimableAmount += claimableAmount;\n\n // Check if we've reached the amount cap (if provided)\n if (amount > 0 && totalClaimableAmount > amount) {\n // Adjust the claimable amount for this period to respect the cap\n uint256 adjustment = totalClaimableAmount - amount;\n claimableAmount -= adjustment;\n totalClaimableAmount = amount;\n }\n\n // Update claimed funds and unused funds\n period.totalClaimedInterest += _toUint128(claimableAmount);\n accountInfoByPeriods[paymentPeriodIdxs[i]][wallet]\n .claimedInterest += claimableAmount;\n\n emit ForceClaimed(\n _msgSender(),\n wallet,\n claimableAmount,\n paymentPeriodIdxs[i]\n );\n\n // If we've reached the amount cap, exit the loop\n if (amount > 0 && totalClaimableAmount >= amount) {\n break;\n }\n }\n\n if (amount > 0 && totalClaimableAmount < amount) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n // Check if we have any funds to claim after processing all periods\n if (totalClaimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n // INTENTIONAL all-or-nothing gate -- see the matching comment in _claimInterest().\n if (totalClaimableAmount > _totalInterestAmountUnused) {\n revert InterestPayment_NoFundsToClaim();\n }\n _totalInterestAmountUnused -= totalClaimableAmount;\n _totalInterestAmountClaimed += totalClaimableAmount;\n\n // Transfer the total claimable amount to the wallet (not to reclaimerAddress)\n paymentToken_.safeTransfer(wallet, totalClaimableAmount);\n }\n\n function reclaimInterestForAllRecipients(\n uint256 paymentPeriodIdx\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidPeriod(paymentPeriodIdx)\n onlyValidReclaimerAddress(reclaimerAddress)\n {\n if (block.timestamp < paymentPeriods[paymentPeriodIdx].endTimestamp) {\n revert InterestPayment_CannotReclaimAllForOngoingPeriod();\n }\n uint256 availableClaimableAmount = periodAvailableInterest(\n paymentPeriodIdx\n );\n if (availableClaimableAmount == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n if (_totalInterestAmountUnused < availableClaimableAmount) {\n revert InterestPayment_NoFundsToClaim();\n }\n\n paymentPeriods[paymentPeriodIdx]\n .totalReclaimedInterest += _toUint128(availableClaimableAmount);\n _totalInterestAmountReclaimed += availableClaimableAmount;\n _totalInterestAmountUnused -= availableClaimableAmount;\n emit ReclaimedAll(\n reclaimerAddress,\n availableClaimableAmount,\n paymentPeriodIdx\n );\n\n // Transfer the total reclaimed amount to reclaimerAddress\n paymentToken_.safeTransfer(reclaimerAddress, availableClaimableAmount);\n }\n\n function reclaimTotalInterest(\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidAmount(amount)\n onlyValidReclaimerAddress(reclaimerAddress)\n {\n if (\n _totalInterestAmountFunded <=\n _totalInterestAmountClaimed + _totalInterestAmountReclaimed\n ) {\n revert InterestPayment_NoFundsToClaim();\n }\n if (amount > _totalInterestAmountUnused) {\n revert InterestPayment_NoFundsToClaim();\n }\n uint256 totalReclaimedAmount = amount;\n _totalInterestAmountReclaimed += totalReclaimedAmount;\n _totalInterestAmountUnused -= totalReclaimedAmount;\n emit ReclaimedAll(\n reclaimerAddress,\n totalReclaimedAmount,\n type(uint256).max\n );\n paymentToken_.safeTransfer(reclaimerAddress, totalReclaimedAmount);\n }\n\n /// @dev This function makes sure that the token is of the correct type. It reads the contract\n /// balance before the transfer and after the transfer. If the increase is not the same as the\n /// amount, the function fails with `InterestPayment_InvalidFeeApplied`. The check is exact. It\n /// rejects a transfer fee. It also rejects an unexpected increase of the balance.\n ///\n /// The claim functions and the reclaim functions do not do this check. This is correct. If this\n /// function fails, no tokens stay in the contract. If a claim function fails, the tokens stay in\n /// the contract, and no function can recover them.\n function fundPrincipal(\n uint256 amount\n )\n public\n onlyTransferAdmin\n whenNotPaused\n nonReentrant\n onlyValidAmount(amount)\n {\n // The pool may be filled in tranches, but it may never exceed the full entitlement\n // of the supply left in circulation: the excess would be payment tokens that only\n // reclaimPrincipal can recover, and an overshoot is the shape a mis-scaled\n // principalAmountPerToken_ or a fat-fingered amount takes.\n // `requiredPrincipalFunding()` returns this ceiling for callers to read before funding.\n // It is 0 exactly when nothing is in circulation, since principalAmountPerToken_ is\n // non-zero and immutable.\n uint256 maxPrincipalTotalAmount = requiredPrincipalFunding();\n if (maxPrincipalTotalAmount == 0) {\n revert InterestPayment_TokenSupplyIsZero();\n }\n uint256 expectedPrincipalTotalAmount = _totalPrincipalAmountUnused +\n amount;\n if (expectedPrincipalTotalAmount > maxPrincipalTotalAmount) {\n revert InterestPayment_PrincipalFundingExceedsEntitlement(\n expectedPrincipalTotalAmount,\n maxPrincipalTotalAmount\n );\n }\n\n _fundedPrincipalAmount += amount;\n _totalPrincipalAmountUnused += amount;\n\n emit PrincipalFunded(_msgSender(), amount);\n\n // Under-funding is allowed, but principal redemption stays shut until the pool covers\n // every outstanding entitlement -- see _principalRedemptionState. This marks the funding\n // call that gets it there. Redemption is open exactly while principalRedemptionOpen() is\n // true; a later mint or reclaim closes it again with no event, so that view, not this\n // event, is the source of truth.\n if (expectedPrincipalTotalAmount == maxPrincipalTotalAmount) {\n emit PrincipalFullyFunded(expectedPrincipalTotalAmount);\n }\n\n uint256 _balanceBefore = paymentToken_.balanceOf(address(this));\n paymentToken_.safeTransferFrom(_msgSender(), address(this), amount);\n uint256 _balanceAfter = paymentToken_.balanceOf(address(this));\n if (_balanceBefore + amount != _balanceAfter) {\n revert InterestPayment_InvalidFeeApplied();\n }\n }\n\n function _claimPrincipal(\n address target,\n uint256 amount\n ) internal {\n // Redemption is open only while the pool covers every outstanding entitlement. Without\n // this an under-funded pool is a race: every claim is paid at the full\n // principalAmountPerToken_ rate until the pool is empty, so the first claimants redeem\n // whole and later holders get nothing. It also means a mis-scaled\n // principalAmountPerToken_ -- whose requiredPrincipalFunding() is unreachably large --\n // can never pay anyone out.\n (bool open, uint256 required) = _principalRedemptionState();\n if (!open) {\n revert InterestPayment_PrincipalNotFullyFunded(\n _totalPrincipalAmountUnused,\n required\n );\n }\n if (block.timestamp < interestAccrualEndTimestamp) {\n revert InterestPayment_MaturityNotReached();\n }\n\n uint256 accountBalance = restrictedLockupToken_.balanceOf(target);\n if (accountBalance == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n // The gate guarantees _totalPrincipalAmountUnused >= required >= allowedPrincipalAmount:\n // `target` is never this contract (claimPrincipal passes the sender, forceClaimPrincipal\n // rejects it via onlyValidClaimRecipient), so its balance is part of the circulating\n // supply `required` is computed from. No capping against the pool is needed.\n uint256 allowedPrincipalAmount = accountBalance *\n principalAmountPerToken_;\n\n // Handle amount parameter: 0 means claim the full entitlement, >0 a specific amount\n uint256 principalToClaim = allowedPrincipalAmount;\n uint256 tokensToBurn = accountBalance;\n if (amount > 0) {\n // Claim specific amount\n if (amount > allowedPrincipalAmount) {\n revert InterestPayment_NotEnoughFundedPrincipal();\n }\n tokensToBurn = amount / principalAmountPerToken_;\n // Ensure exact division\n if (tokensToBurn * principalAmountPerToken_ != amount) {\n revert InterestPayment_PrincipalAmountNotDivisibleByTokenSupply();\n }\n principalToClaim = amount;\n }\n\n _totalPrincipalAmountUnused -= principalToClaim;\n _totalPrincipalAmountClaimed += principalToClaim;\n\n restrictedLockupToken_.softBurn(target, tokensToBurn);\n emit PrincipalClaimed(target, principalToClaim);\n\n paymentToken_.safeTransfer(target, principalToClaim);\n }\n\n function claimPrincipal(\n uint256 amount\n ) external nonReentrant whenNotPaused {\n address sender = _msgSender();\n _claimPrincipal(sender, amount);\n }\n\n /**\n * @notice Force claim principal for a specific wallet address\n * @dev Only callable by Transfer Admin. Similar to claimPrincipal but claims on behalf of a wallet address\n * @param wallet The wallet address to claim principal for\n * @param amount The amount to claim, or 0 for all available principal (the wallet's\n * full entitlement capped by the unused funded principal)\n */\n function forceClaimPrincipal(\n address wallet,\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidClaimRecipient(wallet)\n whenNotPaused\n {\n _claimPrincipal(wallet, amount);\n }\n\n /// @dev Deliberately not gated on redemption being open. If principalAmountPerToken_ was\n /// mis-configured, `requiredPrincipalFunding()` is unreachable and redemption never opens,\n /// so this is the only way to get the funded payment tokens back out of the contract.\n ///\n /// Note the converse: reclaiming below `requiredPrincipalFunding()` takes the pool under the\n /// full entitlement and therefore CLOSES redemption until it is funded back up. That is\n /// intended -- paying claims out of a short pool is first-come-first-served -- but it means\n /// the reclaimer can pause redemption. They already control the funds, so this grants no new\n /// power; it is called out because the effect is not obvious from the call site.\n function reclaimPrincipal(\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidReclaimerAddress(reclaimerAddress)\n {\n if (_totalPrincipalAmountUnused == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n\n // Handle amount parameter: 0 means reclaim all, >0 means reclaim specific amount\n uint256 principalToReclaim = _totalPrincipalAmountUnused;\n if (amount > 0) {\n // Reclaim specific amount\n if (amount > _totalPrincipalAmountUnused) {\n revert InterestPayment_NotEnoughFundedPrincipal();\n }\n principalToReclaim = amount;\n }\n\n _totalPrincipalAmountUnused -= principalToReclaim;\n _totalPrincipalAmountReclaimed += principalToReclaim;\n\n emit PrincipalReclaimed(_msgSender(), principalToReclaim);\n paymentToken_.safeTransfer(reclaimerAddress, principalToReclaim);\n }\n\n function fundedPrincipalAmount() public view returns (uint256) {\n return _fundedPrincipalAmount;\n }\n\n function reclaimedPrincipalAmount() public view returns (uint256) {\n return _totalPrincipalAmountReclaimed;\n }\n\n function claimedPrincipalAmount() public view returns (uint256) {\n return _totalPrincipalAmountClaimed;\n }\n\n function totalAvailablePrincipalAmount() public view returns (uint256) {\n return _totalPrincipalAmountUnused;\n }\n\n /// @inheritdoc IInterestPayment\n function requiredPrincipalFunding() public view returns (uint256) {\n uint256 circulatingBaseUnits = restrictedLockupToken_.totalSupply() -\n restrictedLockupToken_.balanceOf(address(this));\n if (circulatingBaseUnits == 0) {\n return 0;\n }\n // Saturate rather than revert. An entitlement this large is unreachable, so redemption\n // stays shut and the deployment is unusable either way -- but reverting here would also\n // brick principalRedemptionOpen(), availablePrincipalAmount() and fundPrincipal, leaving\n // no way to observe the state or move the pool. reclaimPrincipal is the exit.\n if (principalAmountPerToken_ > type(uint256).max / circulatingBaseUnits) {\n return type(uint256).max;\n }\n return circulatingBaseUnits * principalAmountPerToken_;\n }\n\n /// @dev Live predicate: the pool must cover every outstanding entitlement *now*, not merely\n /// at some point in the past. Deliberately not latched -- a mint raises the entitlement and\n /// reclaimPrincipal lowers the pool, and in both cases claims must stop until the pool is\n /// whole again, or the first claimants redeem in full at the expense of the rest. Claims\n /// themselves preserve coverage: a claim burns `tokensToBurn` and removes exactly\n /// `tokensToBurn * principalAmountPerToken_` from the pool, so it lowers both sides equally.\n /// `required` is returned alongside so callers need only one pass of the two external reads.\n function _principalRedemptionState()\n internal\n view\n returns (bool open, uint256 required)\n {\n required = requiredPrincipalFunding();\n open = required > 0 && _totalPrincipalAmountUnused >= required;\n }\n\n /// @inheritdoc IInterestPayment\n function principalRedemptionOpen() external view returns (bool) {\n (bool open, ) = _principalRedemptionState();\n return open;\n }\n\n function availablePrincipalAmount(\n address account\n ) public view returns (uint256) {\n uint256 accountBalance = restrictedLockupToken_.balanceOf(account);\n if (accountBalance == 0) {\n return 0;\n }\n // Nothing is claimable until redemption opens; reporting an entitlement that every\n // claim path rejects would be worse than reporting nothing.\n (bool open, ) = _principalRedemptionState();\n if (!open) {\n return 0;\n }\n // Open redemption means the pool covers the whole circulating entitlement, so a\n // holder's full entitlement is always payable -- no capping or rounding required.\n return accountBalance * principalAmountPerToken_;\n }\n\n /// @inheritdoc IInterestPayment\n function earlyRepayment(uint256 timestamp) external onlyContractAdminOrTransferAdmin {\n if (timestamp < block.timestamp) {\n revert InterestPayment_InvalidTimestamp();\n }\n // Early repayment can only shorten the accrual period, not extend it\n // Use shiftInterestAccrualEnd to extend with proper safety checks\n if (timestamp > interestAccrualEndTimestamp) {\n revert InterestPayment_InvalidInterestAccrualEndTimestamp();\n }\n\n _pausePaymentAfterTimestamp(timestamp);\n _updateInterestAccrualEnd(timestamp);\n\n emit EarlyRepayment(_msgSender(), timestamp);\n }\n\n /// @notice Force claim interest for an account for a specific period\n /// @dev Only callable by Transfer Admin. Similar to claimInterestForPeriod but claims on behalf of a wallet address\n /// @param wallet The wallet address to claim interest for\n /// @param paymentPeriodIdx The payment period index to claim interest for\n /// @param amount The amount to claim, capped at available claimable amount\n function forceClaimForPeriod(\n address wallet,\n uint256 paymentPeriodIdx,\n uint256 amount\n )\n external\n nonReentrant\n onlyTransferAdmin\n onlyValidPeriod(paymentPeriodIdx)\n onlyValidClaimRecipient(wallet)\n whenNotPaused\n {\n _claimInterestForPeriod(wallet, paymentPeriodIdx, amount, true);\n }\n\n /// @inheritdoc IInterestPayment\n function restrictedLockupToken() external view returns (address) {\n return address(restrictedLockupToken_);\n }\n\n /// @inheritdoc IInterestPayment\n function paymentToken() external view returns (address) {\n return address(paymentToken_);\n }\n\n /// @inheritdoc IInterestPayment\n function principalAmountPerToken() external view returns (uint256) {\n return principalAmountPerToken_;\n }\n\n /// @inheritdoc IInterestPayment\n function principalAmountPerWholeToken() external view returns (uint256) {\n uint256 wholeTokenScale = 10 ** restrictedLockupToken_.decimals();\n // Saturate rather than revert, as in requiredPrincipalFunding(). A return of\n // type(uint256).max means the pairing overflowed and is NOT a face value.\n if (principalAmountPerToken_ > type(uint256).max / wholeTokenScale) {\n return type(uint256).max;\n }\n return principalAmountPerToken_ * wholeTokenScale;\n }\n\n // =============================================================================\n // DIVIDEND FUNCTIONALITY (Adapted from Dividends Contract)\n // =============================================================================\n\n /// @inheritdoc IDividends\n function totalAwardedBalanceAt(\n address token_,\n address receiver_,\n uint256 timestamp_\n ) public view override onlyValidDividendSnapshotId(timestamp_) returns (uint256) {\n return\n unclaimedBalanceAt(token_, receiver_, timestamp_) +\n claimedBalanceAt(token_, receiver_, timestamp_);\n }\n\n /**\n * @dev Check if amount is divisible into totalSupply at timestamp\n * @param amount_ Amount to check divisibility\n * @param timestamp_ Timestamp for historical data\n * @return isDivisible_ boolean. True if amount is valid and divisible by totalSupply.\n */\n function isAmountDivisible(\n uint256 amount_,\n uint256 timestamp_\n ) public view returns (bool isDivisible_) {\n uint256 totalSupplyAtTimestamp = snapshotPeriods.getPastTotalSupply(\n address(restrictedLockupToken_),\n timestamp_\n );\n isDivisible_ = amount_ % totalSupplyAtTimestamp == 0;\n }\n\n /// @inheritdoc IDividends\n function claimedBalanceAt(\n address token_,\n address receiver_,\n uint256 timestamp_\n ) public view override onlyValidDividendSnapshotId(timestamp_) returns (uint256) {\n return dividendClaimedFunds[timestamp_][token_][receiver_];\n }\n\n /// @inheritdoc IDividends\n function claimDividend(\n address token_,\n uint256 timestamp_,\n uint256 amount_\n )\n public\n override\n nonReentrant\n onlyValidDividendSnapshotId(timestamp_)\n whenNotPaused\n {\n uint256 _unclaimedBalance = unclaimedBalanceAt(\n token_,\n _msgSender(),\n timestamp_\n );\n\n if (amount_ == 0) {\n amount_ = _unclaimedBalance;\n }\n if (amount_ > _unclaimedBalance) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n\n if (_unclaimedBalance == 0) {\n revert InterestPayment_NoRemainingUnclaimedDividendBalance();\n }\n\n dividendClaimedFunds[timestamp_][token_][_msgSender()] += amount_;\n fundedDividends[timestamp_][token_].unusedFunds -= amount_;\n\n emit DividendClaimed(_msgSender(), token_, amount_, timestamp_);\n\n IERC20(token_).safeTransfer(_msgSender(), amount_);\n }\n\n /**\n * @dev Reclaim dividends for a specific target address and send to contract reclaimer\n * Can only be called by transfer admin\n * @param token_ ERC-20 token address\n * @param targetAddress_ Address to reclaim dividends for\n * @param timestamp_ timestamp for dividend distribution\n */\n function reclaimDividend(\n address token_,\n address targetAddress_,\n uint256 timestamp_,\n uint256 amount_\n )\n external\n override\n nonReentrant\n onlyTransferAdmin\n onlyValidDividendSnapshotId(timestamp_)\n whenNotPaused\n onlyValidReclaimerAddress(reclaimerAddress)\n {\n uint256 _unclaimedBalance = unclaimedBalanceAt(\n token_,\n targetAddress_,\n timestamp_\n );\n\n if (amount_ == 0) {\n amount_ = _unclaimedBalance;\n }\n if (amount_ > _unclaimedBalance) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n\n if (_unclaimedBalance == 0) {\n revert InterestPayment_NoRemainingUnclaimedDividendBalance();\n }\n\n // Mark the dividends as claimed for the target address\n dividendClaimedFunds[timestamp_][token_][targetAddress_] += amount_;\n fundedDividends[timestamp_][token_].unusedFunds -= amount_;\n\n emit DividendReclaimed(_msgSender(), targetAddress_, token_, amount_, timestamp_);\n\n // Transfer to contract reclaimer address\n IERC20(token_).safeTransfer(reclaimerAddress, amount_);\n }\n\n /// @inheritdoc IDividends\n function unclaimedBalanceAt(\n address token_,\n address receiver_,\n uint256 timestamp_\n ) public view override onlyValidDividendSnapshotId(timestamp_) returns (uint256 _unclaimedBalanceAt) {\n uint256 _claimableFunds = _claimableFundsAt(\n token_,\n receiver_,\n timestamp_\n );\n uint256 _claimedFunds = claimedBalanceAt(\n token_,\n receiver_,\n timestamp_\n );\n if (_claimedFunds > _claimableFunds) {\n revert InterestPayment_InvalidUnclaimedDividendBalance(\n _claimableFunds,\n _claimedFunds\n );\n }\n\n _unclaimedBalanceAt = _claimableFunds - _claimedFunds;\n }\n\n /// @inheritdoc IDividends\n function batchClaimDividend(\n address token_,\n uint256[] calldata timestamps_,\n uint256[] calldata amounts_\n ) external override whenNotPaused {\n uint256 _timestampsLength = timestamps_.length;\n if (_timestampsLength != amounts_.length) {\n revert InterestPayment_InvalidArrayLengths();\n }\n for (uint256 i; i < _timestampsLength; ++i) {\n claimDividend(token_, timestamps_[i], amounts_[i]);\n }\n }\n\n /// @inheritdoc IDividends\n /// @dev This function makes sure that the token is of the correct type. It reads the contract\n /// balance before the transfer and after the transfer. If the increase is not the same as the\n /// amount, the function fails with `InterestPayment_InvalidFeeApplied`. The check is exact. It\n /// rejects a transfer fee. It also rejects an unexpected increase of the balance.\n ///\n /// The claim functions and the reclaim functions do not do this check. This is correct. If this\n /// function fails, no tokens stay in the contract. If a claim function fails, the tokens stay in\n /// the contract, and no function can recover them.\n function fundDividend(\n address token_,\n uint256 amount_,\n uint256 timestamp_\n )\n external\n override\n nonReentrant\n onlyTransferAdmin\n onlyValidDividendSnapshotId(timestamp_)\n whenNotPaused\n {\n if (amount_ == 0) {\n revert InterestPayment_InvalidAmount();\n }\n if (token_ == address(0)) {\n revert InterestPayment_InvalidDividendTokenAddress();\n }\n IERC20 _paymentToken = IERC20(token_);\n uint8 _restrictedLockupTokenDecimals = restrictedLockupToken_.decimals();\n if (_restrictedLockupTokenDecimals > ERC20(token_).decimals()) {\n revert InterestPayment_InvalidTokenDecimals(\n _restrictedLockupTokenDecimals\n );\n }\n\n uint256 _totalSupply = snapshotPeriods.getPastTotalSupply(\n address(restrictedLockupToken_),\n timestamp_\n );\n if (_totalSupply == 0) {\n revert InterestPayment_ZeroTotalSupply();\n }\n if (amount_ % _totalSupply != 0) {\n revert InterestPayment_IndivisibleAmount(amount_, _totalSupply);\n }\n\n uint256 _dividendPerSecurity = amount_ / _totalSupply;\n\n fundedDividends[timestamp_][token_].unusedFunds += amount_;\n fundedDividends[timestamp_][token_].totalFunds += amount_;\n fundedDividends[timestamp_][token_].dividendPerSecurity =\n fundedDividends[timestamp_][token_].dividendPerSecurity +\n _dividendPerSecurity;\n\n emit DividendFunded(_msgSender(), token_, amount_, timestamp_);\n uint256 _balanceBefore = _paymentToken.balanceOf(address(this));\n _paymentToken.safeTransferFrom(_msgSender(), address(this), amount_);\n uint256 _balanceAfter = _paymentToken.balanceOf(address(this));\n if (_balanceBefore + amount_ != _balanceAfter) {\n revert InterestPayment_InvalidFeeApplied();\n }\n }\n\n /**\n * @dev Reclaim ERC-20 tokens from a specific dividend snapshot\n * Can only be done if no dividends have been claimed for this snapshot (unusedFunds == totalFunds)\n * If amount is 0, reclaims all available funds\n * @param token_ ERC-20 token address\n * @param amount_ amount of tokens to reclaim (0 = reclaim all)\n * @param timestamp_ timestamp for dividend distribution\n */\n function reclaimTotalDividend(\n address token_,\n uint256 amount_,\n uint256 timestamp_\n )\n external\n override\n nonReentrant\n onlyTransferAdmin\n onlyValidDividendSnapshotId(timestamp_)\n whenNotPaused\n onlyValidReclaimerAddress(reclaimerAddress)\n {\n FundedDividends storage _fundedDividend = fundedDividends[timestamp_][token_];\n \n // Check if any dividends have been claimed (unusedFunds should equal totalFunds if no claims)\n if (_fundedDividend.unusedFunds != _fundedDividend.totalFunds) {\n revert InterestPayment_DividendsAlreadyClaimed();\n }\n\n // Check if there are any funds to reclaim\n if (_fundedDividend.unusedFunds == 0) {\n revert InterestPayment_NoFundsToClaim();\n }\n\n uint256 _amountToReclaim;\n if (amount_ == 0) {\n // Reclaim all funds\n _amountToReclaim = _fundedDividend.totalFunds;\n } else {\n // Validate amount is not greater than available funds\n if (amount_ > _fundedDividend.totalFunds) {\n revert InterestPayment_NotEnoughFundsToClaim();\n }\n\n // Validate divisibility (same as fundDividend)\n uint256 _totalSupply = snapshotPeriods.getPastTotalSupply(\n address(restrictedLockupToken_),\n timestamp_\n );\n // it is always greater than 0 because we can't fund dividend with 0 supply\n // so it fails on InterestPayment_NoFundsToClaim\n if (amount_ % _totalSupply != 0) {\n revert InterestPayment_IndivisibleAmount(amount_, _totalSupply);\n }\n\n _amountToReclaim = amount_;\n }\n\n // Calculate the dividend per security to subtract\n uint256 _dividendPerSecurityToSubtract = _amountToReclaim / snapshotPeriods.getPastTotalSupply(\n address(restrictedLockupToken_),\n timestamp_\n );\n\n // Update dividend tracking\n _fundedDividend.unusedFunds -= _amountToReclaim;\n _fundedDividend.totalFunds -= _amountToReclaim;\n _fundedDividend.dividendPerSecurity -= _dividendPerSecurityToSubtract;\n\n emit DividendReclaimed(_msgSender(), address(this), token_, _amountToReclaim, timestamp_);\n\n // Transfer tokens back to the caller\n IERC20(token_).safeTransfer(reclaimerAddress, _amountToReclaim);\n }\n\n /// @inheritdoc IDividends\n function tokensAt(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidDividendSnapshotId(timestamp_) returns (uint256) {\n return fundedDividends[timestamp_][token_].unusedFunds;\n }\n\n /**\n * @dev Proxy function for restricted token to retrieve totalSupply at timestamp\n * @param timestamp_ Timestamp for historical data\n * @return totalSupply at timestamp\n */\n function totalSupplyAt(\n uint256 timestamp_\n ) external view onlyValidDividendSnapshotId(timestamp_) returns (uint256) {\n return snapshotPeriods.getPastTotalSupply(address(restrictedLockupToken_), timestamp_);\n }\n\n /**\n * @dev Proxy function for restricted token to retrieve balanceOf address at timestamp\n * @param sender_ address to check balance\n * @param timestamp_ timestamp for historical data\n * @return balance of address at timestamp\n */\n function balanceOfAt(\n address sender_,\n uint256 timestamp_\n ) external view onlyValidDividendSnapshotId(timestamp_) returns (uint256) {\n return snapshotPeriods.getPastBalanceOf(address(restrictedLockupToken_), sender_, timestamp_);\n }\n\n /// @inheritdoc IDividends\n function fundsAt(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidDividendSnapshotId(timestamp_) returns (uint256) {\n return fundedDividends[timestamp_][token_].totalFunds;\n }\n\n /**\n * @dev _claimableFundsAt private function to calculate claimable funds for receiver at a given timestamp\n * @param token_ ERC20 token address\n * @param receiver_ address of receiver\n * @param timestamp_ Timestamp for historical data\n * @return claimable funds for receiver at timestamp\n */\n function _claimableFundsAt(\n address token_,\n address receiver_,\n uint256 timestamp_\n ) private view returns (uint256) {\n return (fundedDividends[timestamp_][token_].dividendPerSecurity *\n snapshotPeriods.getPastBalanceOf(address(restrictedLockupToken_), receiver_, timestamp_));\n }\n\n // ============================================\n // BORROW LEND POOL INTEGRATION\n // ============================================\n\n /**\n * @notice Calculate interest for a specific amount over a time range based on internal payment periods\n * @dev Pure calculation function that doesn't depend on token ownership - calculates interest\n * as if the specified amount was held for the entire duration between timestamps.\n *\n * The accrual is continuous to endTimestamp. This function does not round the end down to\n * a payment boundary, but accruedInterestAt() and the claim paths do. Thus a raw\n * block.timestamp gives more than the holder can claim now. This is correct for a quote\n * on a position inside a period. Do not show the result as a claimable amount.\n *\n * To get a claimable amount, first round the end with\n * nearestInterestPaymentTimestampAt().\n * @param amount The amount to calculate interest for (in token units)\n * @param startTimestamp Start timestamp for interest calculation\n * @param endTimestamp End timestamp for interest calculation\n * @return interestAmount The calculated interest amount in payment token units\n */\n function calculateInterestFor(\n uint256 amount,\n uint256 startTimestamp,\n uint256 endTimestamp\n ) external view returns (uint256 interestAmount) {\n if (startTimestamp >= endTimestamp || amount == 0) {\n return 0;\n }\n\n // Cap timestamps to valid accrual range\n uint256 effectiveStart = startTimestamp < interestAccrualStartTimestamp \n ? interestAccrualStartTimestamp \n : startTimestamp;\n uint256 effectiveEnd = endTimestamp > interestAccrualEndTimestamp \n ? interestAccrualEndTimestamp \n : endTimestamp;\n\n // Handle paymentPausedAfterTimestamp if set\n if (paymentPausedAfterTimestamp > 0 && effectiveEnd > paymentPausedAfterTimestamp) {\n effectiveEnd = paymentPausedAfterTimestamp;\n }\n\n if (effectiveStart >= effectiveEnd) {\n return 0;\n }\n\n // Calculate interest for each payment period that overlaps with the time range\n for (uint256 i = 0; i < paymentPeriods.length; i++) {\n PaymentPeriod memory period = paymentPeriods[i];\n\n // Skip periods that don't overlap\n if (period.endTimestamp <= effectiveStart || period.startTimestamp >= effectiveEnd) {\n continue;\n }\n\n // Calculate overlap duration\n uint256 overlapStart = period.startTimestamp > effectiveStart \n ? period.startTimestamp \n : effectiveStart;\n uint256 overlapEnd = period.endTimestamp < effectiveEnd \n ? period.endTimestamp \n : effectiveEnd;\n uint256 duration = overlapEnd - overlapStart;\n\n // Calculate ownership for this duration (amount * duration)\n uint256 ownershipForPeriod = amount * duration;\n\n // Calculate interest using existing internal method\n interestAmount += _calculateInterest(period.interestRatePerSecond, ownershipForPeriod);\n }\n\n return interestAmount;\n }\n}\n"},"contracts/PurchaseContract.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {IInterestPayment} from \"./interfaces/IInterestPayment.sol\";\nimport {RestrictedLockupToken} from \"./RestrictedLockupToken.sol\";\n\n/**\n * @title PurchaseContract\n * @notice Contract for recording token purchases and minting the purchased tokens,\n * optionally distributing an on-chain payment leg to the originator, the admin fee\n * wallet, and the interest pool.\n * @dev SETTLEMENT MODELS -- read before changing any amount validation.\n *\n * A purchase settles in one of two ways, and both go through the same entry points:\n *\n * 1. On-chain settlement. The payment token is transferred to this contract out-of-band\n * (there is no `transferFrom` here -- see below), and `executePurchase*` distributes it\n * to `originatorPaymentWallet`, `adminFeeWallet`, and `interestPayment` via\n * `fundInterest`.\n *\n * 2. Off-chain settlement. The original payment arrives as a USD wire sent directly to\n * those same parties. Nothing needs to move on-chain; only the mint is recorded here.\n * Such a purchase carries `totalAmount == 0` with every component at zero, and\n * `mintAmount > 0`.\n *\n * Consequences that are load-bearing:\n *\n * - This contract never touches the payer's balance or allowance. It holds no `transferFrom`,\n * no deposit function, and no `receive`/`fallback`. `totalAmount` and its components\n * describe an OPTIONAL on-chain distribution leg, not the payment itself.\n * - `params.payerAddress` exists only for the AML/KYC gate and the audit-trail event.\n * - `params.mintAmount` is issuer input. It is deliberately not derived from, or checked\n * against, any payment amount, because in model 2 there is no on-chain payment to check\n * against. Issuance sizing is `automationAdmin`'s decision.\n * - Escrow is pooled. Deposits are not bound to a `purchaseId`, so an on-chain-settled\n * purchase is paid from whatever balance this contract holds.\n */\ncontract PurchaseContract is Ownable, ERC2771Context, ReentrancyGuard {\n using SafeERC20 for IERC20;\n\n // ============================================\n // EVENTS\n // ============================================\n\n event PurchaseExecuted(\n string indexed purchaseId,\n address indexed payerAddress,\n address indexed tokenRecipientAddress,\n address authorityAddress,\n uint256 originatorPurchaseAmount,\n uint256 prefundedInterestAmount,\n uint256 adminFeeExcludingPrefundedInterestAmount,\n uint256 mintAmount,\n address originatorAddress\n );\n\n event PurchaseCanceled(\n string indexed purchaseId,\n address indexed authorityAddress,\n address indexed paymentTokenAddress,\n address canceledPurchaseWallet,\n uint256 amount\n );\n\n event AutomationAdminUpdated(address indexed oldAdmin, address indexed newAdmin);\n event AdminFeeWalletUpdated(address indexed oldWallet, address indexed newWallet);\n event CanceledPurchaseWalletUpdated(address indexed oldWallet, address indexed newWallet);\n event OriginatorPaymentWalletUpdated(address indexed oldWallet, address indexed newWallet);\n\n // ============================================\n // ERRORS\n // ============================================\n\n error PurchaseContract_InvalidZeroAddress();\n error PurchaseContract_InvalidPaymentToken();\n error PurchaseContract_InvalidTokenContract();\n error PurchaseContract_OnlyAutomationAdmin();\n error PurchaseContract_PurchaseIdAlreadyUsed();\n error PurchaseContract_PurchaseIdNotUsed();\n error PurchaseContract_PurchaseIdCanceled();\n error PurchaseContract_InvalidPurchaseId();\n error PurchaseContract_InvalidAmount();\n error PurchaseContract_DecimalPrecisionMismatch();\n error PurchaseContract_TransferFailed();\n error PurchaseContract_AdminFeeWalletNotSet();\n error PurchaseContract_CanceledPurchaseWalletNotSet();\n error PurchaseContract_OriginatorPaymentWalletNotSet();\n error PurchaseContract_TokenRecipientAmlKycNotPassed();\n error PurchaseContract_PayerAmlKycNotPassed();\n error PurchaseContract_InvalidTotalAmount();\n error PurchaseContract_RemainderNotZero();\n error PurchaseContract_InterestPaymentNotConfigured();\n\n // ============================================\n // STATE VARIABLES\n // ============================================\n\n /// @notice Interest payment contract for funding interest\n IInterestPayment public immutable interestPayment;\n RestrictedLockupToken public immutable tokenContract;\n\n /// @notice USDC token contract\n IERC20 public immutable paymentToken;\n\n /// @notice Automation admin address that can execute purchases and cancellations\n address public automationAdmin;\n\n /// @notice Admin fee wallet address\n address public adminFeeWallet;\n\n /// @notice Canceled purchase wallet address\n address public canceledPurchaseWallet;\n\n /// @notice Originator payment wallet address\n address public originatorPaymentWallet;\n\n /// @notice Mapping to track used purchase IDs\n mapping(string => bool) public usedPurchaseIds;\n\n /// @notice Mapping to track canceled purchase IDs\n mapping(string => bool) public canceledPurchaseIds;\n\n // ============================================\n // MODIFIERS\n // ============================================\n\n modifier onlyAutomationAdmin() {\n if (_msgSender() != automationAdmin) {\n revert PurchaseContract_OnlyAutomationAdmin();\n }\n _;\n }\n\n modifier validAddress(address addr) {\n if (addr == address(0)) {\n revert PurchaseContract_InvalidZeroAddress();\n }\n _;\n }\n\n modifier validAmount(uint256 amount) {\n if (amount == 0) {\n revert PurchaseContract_InvalidAmount();\n }\n _;\n }\n\n // ============================================\n // CONSTRUCTOR\n // ============================================\n\n constructor(\n address contractAdmin_,\n address trustedForwarder_,\n address tokenContract_,\n address interestPayment_,\n address paymentToken_\n ) \n Ownable(contractAdmin_)\n ERC2771Context(trustedForwarder_)\n validAddress(contractAdmin_)\n validAddress(trustedForwarder_)\n validAddress(tokenContract_)\n validAddress(paymentToken_)\n {\n if (interestPayment_ != address(0)) {\n interestPayment = IInterestPayment(interestPayment_);\n if (interestPayment.paymentToken() != paymentToken_) {\n revert PurchaseContract_InvalidPaymentToken();\n }\n if (interestPayment.restrictedLockupToken() != tokenContract_) {\n revert PurchaseContract_InvalidTokenContract();\n }\n }\n // Note: interestPayment can be address(0) if contract is deployed without interest functionality\n \n paymentToken = IERC20(paymentToken_);\n tokenContract = RestrictedLockupToken(tokenContract_);\n }\n\n // ============================================\n // ADMIN FUNCTIONS\n // ============================================\n\n /**\n * @notice Update automation admin address\n * @param newAutomationAdmin_ New automation admin address\n */\n function updateAutomationAdmin(address newAutomationAdmin_) \n external \n onlyOwner \n validAddress(newAutomationAdmin_) \n {\n address oldAdmin = automationAdmin;\n automationAdmin = newAutomationAdmin_;\n emit AutomationAdminUpdated(oldAdmin, newAutomationAdmin_);\n }\n\n /**\n * @notice Update admin fee wallet address\n * @param newAdminFeeWallet_ New admin fee wallet address\n */\n function updateAdminFeeWallet(address newAdminFeeWallet_) \n external \n onlyOwner \n validAddress(newAdminFeeWallet_) \n {\n address oldWallet = adminFeeWallet;\n adminFeeWallet = newAdminFeeWallet_;\n emit AdminFeeWalletUpdated(oldWallet, newAdminFeeWallet_);\n }\n\n /**\n * @notice Update canceled purchase wallet address\n * @param newCanceledPurchaseWallet_ New canceled purchase wallet address\n */\n function updateCanceledPurchaseWallet(address newCanceledPurchaseWallet_) \n external \n onlyOwner \n validAddress(newCanceledPurchaseWallet_) \n {\n address oldWallet = canceledPurchaseWallet;\n canceledPurchaseWallet = newCanceledPurchaseWallet_;\n emit CanceledPurchaseWalletUpdated(oldWallet, newCanceledPurchaseWallet_);\n }\n\n /**\n * @notice Update originator payment wallet address\n * @param newOriginatorPaymentWallet_ New originator payment wallet address\n */\n function updateOriginatorPaymentWallet(address newOriginatorPaymentWallet_) \n external \n onlyOwner \n validAddress(newOriginatorPaymentWallet_) \n {\n address oldWallet = originatorPaymentWallet;\n originatorPaymentWallet = newOriginatorPaymentWallet_;\n emit OriginatorPaymentWalletUpdated(oldWallet, newOriginatorPaymentWallet_);\n }\n\n // ============================================\n // PURCHASE FUNCTIONS\n // ============================================\n\n struct PurchaseParams {\n string purchaseId;\n address payerAddress;\n address tokenRecipientAddress;\n uint256 originatorPurchaseAmount;\n uint256 mintAmount;\n uint256 adminFeeAmount;\n uint256 totalAmount;\n }\n\n struct PurchaseParamsWithInterest {\n string purchaseId;\n address payerAddress;\n address tokenRecipientAddress;\n uint256 mintAmount;\n uint256 originatorPurchaseAmount;\n uint256 prefundedInterestAmount;\n uint256 adminFeeExcludingPrefundedInterestAmount;\n uint256 totalAmount;\n }\n\n /**\n * @notice Execute a token purchase with USDC payment and interest funding\n * @dev `params.totalAmount` may legitimately be zero. That is the off-chain (USD wire)\n * settlement model described on the contract: every component is zero, no transfer\n * runs, and only the mint is recorded. Do NOT add `validAmount(params.totalAmount)`\n * or any other nonzero requirement on the total -- it would revert every wire-settled\n * purchase. The `if (amount > 0)` guard on each distribution leg below is what makes\n * hybrid settlement work and must stay.\n * @param params Purchase parameters\n */\n function executePurchaseWithInterest(\n PurchaseParamsWithInterest memory params\n ) \n external \n nonReentrant \n onlyAutomationAdmin\n validAddress(params.payerAddress)\n validAddress(params.tokenRecipientAddress)\n {\n // Validation: Ensure interest payment contract is configured when using this function\n if (address(interestPayment) == address(0)) {\n revert PurchaseContract_InterestPaymentNotConfigured();\n }\n // Validation: Check unique purchaseId\n if (usedPurchaseIds[params.purchaseId]) {\n revert PurchaseContract_PurchaseIdAlreadyUsed();\n }\n // Validation: Reject purchase IDs that have been canceled\n if (canceledPurchaseIds[params.purchaseId]) {\n revert PurchaseContract_PurchaseIdCanceled();\n }\n\n // Validation: Ensure required addresses are set\n if (adminFeeWallet == address(0)) {\n revert PurchaseContract_AdminFeeWalletNotSet();\n }\n if (originatorPaymentWallet == address(0)) {\n revert PurchaseContract_OriginatorPaymentWalletNotSet();\n }\n if (params.mintAmount == 0) {\n revert PurchaseContract_InvalidAmount();\n }\n if (params.totalAmount != params.originatorPurchaseAmount + params.prefundedInterestAmount + params.adminFeeExcludingPrefundedInterestAmount) {\n revert PurchaseContract_InvalidTotalAmount();\n }\n if (!tokenContract.isAmlKycPassed(params.tokenRecipientAddress)) {\n revert PurchaseContract_TokenRecipientAmlKycNotPassed();\n }\n if (!tokenContract.isAmlKycPassed(params.payerAddress)) {\n revert PurchaseContract_PayerAmlKycNotPassed();\n }\n\n // Step 2: Transfer originator amount to originator address\n // Each leg is guarded independently: a zero component means that party was already paid\n // off-chain (USD wire), so there is nothing to distribute here. Deliberate -- do not\n // replace with a nonzero requirement.\n if (params.originatorPurchaseAmount > 0) {\n paymentToken.safeTransfer(originatorPaymentWallet, params.originatorPurchaseAmount);\n }\n\n // Step 3: Fund interest in InterestPayment contract\n if (params.prefundedInterestAmount > 0) {\n paymentToken.safeIncreaseAllowance(address(interestPayment), params.prefundedInterestAmount);\n interestPayment.fundInterest(params.prefundedInterestAmount);\n }\n\n // Step 4: Transfer admin fee to admin fee wallet\n if (params.adminFeeExcludingPrefundedInterestAmount > 0) {\n paymentToken.safeTransfer(adminFeeWallet, params.adminFeeExcludingPrefundedInterestAmount);\n }\n\n // Step 5: Mint tokens to recipient\n tokenContract.mint(params.tokenRecipientAddress, params.mintAmount);\n\n // Step 6: Mark purchase as completed\n usedPurchaseIds[params.purchaseId] = true;\n\n // Step 7: Emit event\n emit PurchaseExecuted(\n params.purchaseId,\n params.payerAddress,\n params.tokenRecipientAddress,\n _msgSender(),\n params.originatorPurchaseAmount,\n params.prefundedInterestAmount,\n params.adminFeeExcludingPrefundedInterestAmount,\n params.mintAmount,\n originatorPaymentWallet\n );\n }\n\n /**\n * @notice Execute a token purchase with USDC payment (without interest funding)\n * @dev This function can be used when the contract is deployed without an interest payment contract\n * @dev `params.totalAmount` may legitimately be zero -- the off-chain (USD wire) settlement\n * model described on the contract. Do NOT add a nonzero requirement on the total; see\n * the note on `executePurchaseWithInterest`.\n * @param params Purchase parameters (prefundedInterestAmount will be ignored)\n */\n function executePurchase(\n PurchaseParams memory params\n ) \n external \n nonReentrant \n onlyAutomationAdmin\n validAddress(params.payerAddress)\n validAddress(params.tokenRecipientAddress)\n {\n // Validation: Check unique purchaseId\n if (usedPurchaseIds[params.purchaseId]) {\n revert PurchaseContract_PurchaseIdAlreadyUsed();\n }\n // Validation: Reject purchase IDs that have been canceled\n if (canceledPurchaseIds[params.purchaseId]) {\n revert PurchaseContract_PurchaseIdCanceled();\n }\n\n // Validation: Ensure required addresses are set\n if (adminFeeWallet == address(0)) {\n revert PurchaseContract_AdminFeeWalletNotSet();\n }\n if (originatorPaymentWallet == address(0)) {\n revert PurchaseContract_OriginatorPaymentWalletNotSet();\n }\n if (!tokenContract.isAmlKycPassed(params.tokenRecipientAddress)) {\n revert PurchaseContract_TokenRecipientAmlKycNotPassed();\n }\n if (!tokenContract.isAmlKycPassed(params.payerAddress)) {\n revert PurchaseContract_PayerAmlKycNotPassed();\n }\n \n // For executePurchase (without interest), we expect prefundedInterestAmount to be 0\n // and totalAmount to equal originatorPurchaseAmount + adminFeeExcludingPrefundedInterestAmount\n if (params.totalAmount != params.originatorPurchaseAmount + params.adminFeeAmount) {\n revert PurchaseContract_InvalidTotalAmount();\n }\n\n // Step 2: Transfer originator amount to originator address\n if (params.originatorPurchaseAmount > 0) {\n paymentToken.safeTransfer(originatorPaymentWallet, params.originatorPurchaseAmount);\n }\n\n // Step 3: Skip interest funding since no interest payment contract is configured\n // params.prefundedInterestAmount is ignored in this function\n\n // Step 4: Transfer admin fee to admin fee wallet\n if (params.adminFeeAmount > 0) {\n paymentToken.safeTransfer(adminFeeWallet, params.adminFeeAmount);\n }\n\n // Step 5: Mint tokens to recipient\n // mintAmount is issuer input and is intentionally independent of the payment amounts:\n // under off-chain settlement there is no on-chain payment to derive it from.\n if (params.mintAmount == 0) {\n revert PurchaseContract_InvalidAmount();\n }\n tokenContract.mint(params.tokenRecipientAddress, params.mintAmount);\n\n // Step 6: Mark purchase as completed\n usedPurchaseIds[params.purchaseId] = true;\n\n // Step 7: Emit event (prefundedInterestAmount will be 0 for this function)\n emit PurchaseExecuted(\n params.purchaseId,\n params.payerAddress,\n params.tokenRecipientAddress,\n _msgSender(),\n params.originatorPurchaseAmount,\n 0, // No prefunded interest amount\n params.adminFeeAmount,\n params.mintAmount,\n originatorPaymentWallet\n );\n }\n\n /**\n * @notice Cancel a purchase and transfer amount to canceled purchase wallet\n * @dev Marks purchaseId_ as canceled, which permanently blocks it from being executed.\n * A purchase ID may be canceled only once, and only if it was never executed.\n * @param purchaseId_ The purchase ID being canceled\n * @param paymentTokenAddress_ The payment token address to cancel\n * @param amount_ Amount to transfer to canceled purchase wallet\n */\n function cancelPurchase(\n string calldata purchaseId_,\n address paymentTokenAddress_,\n uint256 amount_\n )\n external\n nonReentrant\n onlyAutomationAdmin\n validAmount(amount_)\n {\n // Validation: Ensure purchase ID is not empty\n if (bytes(purchaseId_).length == 0) {\n revert PurchaseContract_InvalidPurchaseId();\n }\n\n // Validation: Ensure canceled purchase wallet is set\n if (canceledPurchaseWallet == address(0)) {\n revert PurchaseContract_InvalidZeroAddress();\n }\n\n // Validation: An executed purchase cannot be canceled\n if (usedPurchaseIds[purchaseId_]) {\n revert PurchaseContract_PurchaseIdAlreadyUsed();\n }\n // Validation: A purchase can only be canceled once\n if (canceledPurchaseIds[purchaseId_]) {\n revert PurchaseContract_PurchaseIdCanceled();\n }\n\n // Mark purchase as canceled before transferring (checks-effects-interactions)\n canceledPurchaseIds[purchaseId_] = true;\n\n // Transfer amount to canceled purchase wallet\n IERC20(paymentTokenAddress_).safeTransfer(canceledPurchaseWallet, amount_);\n\n emit PurchaseCanceled(purchaseId_, _msgSender(), paymentTokenAddress_, canceledPurchaseWallet, amount_);\n }\n\n // ============================================\n // VIEW FUNCTIONS\n // ============================================\n\n /**\n * @notice Check if a purchase ID has been used\n * @param purchaseId_ The purchase ID to check\n * @return Whether the purchase ID has been used\n */\n function isPurchaseIdUsed(string calldata purchaseId_) external view returns (bool) {\n return usedPurchaseIds[purchaseId_];\n }\n\n /**\n * @notice Check if a purchase ID has been canceled\n * @param purchaseId_ The purchase ID to check\n * @return Whether the purchase ID has been canceled\n */\n function isPurchaseIdCanceled(string calldata purchaseId_) external view returns (bool) {\n return canceledPurchaseIds[purchaseId_];\n }\n\n // ============================================\n // ERC2771 OVERRIDES\n // ============================================\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n} "},"contracts/RecallablePayment.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {Pausable} from \"@openzeppelin/contracts/utils/Pausable.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport {IDividends} from \"./interfaces/IDividends.sol\";\nimport {IRecallablePayment} from \"./interfaces/IRecallablePayment.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title RecallablePayment\n * @notice Distributes ERC-20 dividends to recipients using explicit, on-chain per-recipient\n * allocations (arbitrary amounts, not pro-rata). Recipients claim directly from the\n * contract without supplying any extra proof data, mirroring the claim UX of the\n * dividend functionality in `InterestPayment`.\n * @dev Allocations are stored on-chain keyed by `(timestamp, token, recipient)`. The\n * `timestamp` is an arbitrary distribution identifier (not a historical snapshot id).\n * Unclaimed allocations can be reclaimed by an admin. Supports ERC2771 meta-transactions.\n * @dev Use only standard ERC-20 tokens that keep a constant balance.\n *\n * Do not use these types of token:\n * - a token that subtracts a fee from each transfer;\n * - a token that changes account balances automatically (a rebase token);\n * - an ERC-777 token that calls back into this contract.\n *\n * The function {fundDividend} rejects these tokens. The claim functions and the reclaim\n * functions do not reject them. They send the amount from the internal records. They do not\n * compare that amount with the true token balance. If you use a different type of token,\n * these functions can send too few tokens, or they can fail. The contract has no function\n * that recovers tokens that stay in it.\n */\ncontract RecallablePayment is\n IRecallablePayment,\n ReentrancyGuard,\n Pausable,\n ERC2771Context\n{\n using SafeERC20 for IERC20;\n\n /// @dev Aggregate accounting for a single `(timestamp, token)` distribution\n struct Distribution {\n uint256 totalFunds; // total ERC-20 deposited (minus any reclaimed surplus)\n uint256 totalAllocated; // total assigned to recipients\n uint256 totalClaimed; // total claimed by recipients\n uint256 totalReclaimed; // total reclaimed from recipients by admins\n uint256 unlockedAtTs; // unix time at/after which recipients may claim (0 = no time lock)\n bool started; // distribution activated (claim time assigned; was fully funded at start).\n // Allocations freeze at unlockedAtTs; funding coverage is re-checked at claim time.\n // The unlock time is pinned by the first 4-arg fundDividend (0 = not yet pinned / no\n // time lock); later 4-arg deposits must match it or revert. The 3-arg overload funds\n // freely and honors the pinned value. Once started, unlockedAtTs is always nonzero:\n // _maybeAutoStart and setUnlockTime both normalize 0 to block.timestamp, so 0 is only\n // ever observable before start.\n }\n\n IAccessControl public accessControl;\n address public reclaimerAddress;\n /// @dev Optional reference to the related security token for discoverability and\n /// integration. Independent of which AccessControl is wired. Zero = standalone.\n address public immutable restrictedLockupToken;\n\n /// @dev Per-recipient accounting for a single `(timestamp, token)` distribution.\n /// Invariant: allocated >= claimed + reclaimed.\n struct UserAllocation {\n uint256 allocated; // amount assigned to the recipient\n uint256 claimed; // amount claimed by the recipient\n uint256 reclaimed; // amount admin-reclaimed from the recipient\n }\n\n /// @dev timestamp => token => Distribution\n mapping(uint256 => mapping(address => Distribution)) internal distributions;\n /// @dev timestamp => token => recipient => per-recipient accounting\n mapping(uint256 => mapping(address => mapping(address => UserAllocation)))\n internal userAllocations;\n\n // Errors\n error RecallablePayment_InvalidTimestamp();\n error RecallablePayment_InvalidTokenAddress();\n error RecallablePayment_InvalidRecipientAddress(uint256 index);\n error RecallablePayment_InvalidAmount(uint256 index);\n error RecallablePayment_AllocationUnchanged(uint256 index);\n error RecallablePayment_InvalidArrayLengths();\n error RecallablePayment_EmptyRecipients();\n error RecallablePayment_InvalidFeeApplied();\n error RecallablePayment_NoFundsToClaim();\n error RecallablePayment_NotEnoughFundsToClaim();\n error RecallablePayment_DividendsAlreadyClaimed();\n error RecallablePayment_InsufficientDistributionFunds(\n uint256 requested,\n uint256 available\n );\n error RecallablePayment_NoRemainingUnclaimedBalance();\n error RecallablePayment_InvalidReclaimerAddress();\n error RecallablePayment_ReclaimerAddressUnchanged();\n error RecallablePayment_InvalidTrustedForwarder();\n error RecallablePayment_UnlockTimeNotApplicableAfterStart(\n uint256 requested,\n uint256 current\n );\n error RecallablePayment_UnlockTimeConflict(\n uint256 provided,\n uint256 scheduled\n );\n error RecallablePayment_DistributionClaimingOpen();\n error RecallablePayment_DistributionNotStarted();\n error RecallablePayment_DistributionNotUnlocked();\n error RecallablePayment_NotFullyFunded(uint256 totalAllocated, uint256 totalFunds);\n error RecallablePayment_NoAllocationsToFund();\n error RecallablePayment_InvalidWalletAddress();\n error RecallablePayment_SameWallet();\n error RecallablePayment_NoPositionToTransfer();\n\n modifier onlyValidTimestamp(uint256 timestamp_) {\n if (timestamp_ == 0) {\n revert RecallablePayment_InvalidTimestamp();\n }\n _;\n }\n\n modifier onlyValidReclaimerAddress() {\n if (reclaimerAddress == address(0)) {\n revert RecallablePayment_InvalidReclaimerAddress();\n }\n _;\n }\n\n modifier onlyContractAdmin() {\n _onlyContractAdmin();\n _;\n }\n\n modifier onlyTransferAdmin() {\n _onlyTransferAdmin();\n _;\n }\n\n modifier onlyContractAdminOrTransferAdmin() {\n _onlyContractAdminOrTransferAdmin();\n _;\n }\n\n /// @param accessControl_ external AccessControl for role checks (required, non-zero).\n /// Deployer chooses any implementation (shared token AC, dedicated payment AC, etc.).\n /// @param trustedForwarder_ ERC2771 forwarder (required, non-zero). The forwarder is\n /// immutable in ERC2771Context, so an accidental zero could never be corrected\n /// without redeploying.\n /// @param restrictedLockupToken_ optional RestrictedLockupToken reference (zero = standalone).\n /// Independent of `accessControl_`; commonly set for integration while payment\n /// authority is isolated in a separate AccessControl contract.\n constructor(\n address accessControl_,\n address trustedForwarder_,\n address restrictedLockupToken_\n ) ReentrancyGuard() ERC2771Context(trustedForwarder_) {\n if (accessControl_ == address(0)) {\n revert EasyAccessControl_InvalidZeroAddress();\n }\n if (trustedForwarder_ == address(0)) {\n revert RecallablePayment_InvalidTrustedForwarder();\n }\n accessControl = IAccessControl(accessControl_);\n restrictedLockupToken = restrictedLockupToken_;\n }\n\n // =============================================================================\n // ERC2771 context overrides\n // =============================================================================\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n // =============================================================================\n // Access control\n // =============================================================================\n\n function _onlyTransferAdmin() internal view {\n if (\n !accessControl.hasRole(\n _msgSender(),\n accessControl.TRANSFER_ADMIN_ROLE()\n )\n ) {\n revert EasyAccessControl_DoesNotHaveTransferAdminRole(_msgSender());\n }\n }\n\n function _onlyContractAdmin() internal view {\n if (\n !accessControl.hasRole(\n _msgSender(),\n accessControl.CONTRACT_ADMIN_ROLE()\n )\n ) {\n revert EasyAccessControl_DoesNotHaveContractAdminRole(_msgSender());\n }\n }\n\n function _onlyContractAdminOrTransferAdmin() internal view {\n if (\n !accessControl.hasRole(\n _msgSender(),\n accessControl.CONTRACT_ADMIN_ROLE()\n ) &&\n !accessControl.hasRole(\n _msgSender(),\n accessControl.TRANSFER_ADMIN_ROLE()\n )\n ) {\n revert EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(\n _msgSender()\n );\n }\n }\n\n /// @inheritdoc IRecallablePayment\n function setReclaimerAddress(\n address newReclaimerAddress\n ) external override onlyContractAdmin {\n address _previousReclaimer = reclaimerAddress;\n // Reject the contract itself: reclaims safeTransfer to reclaimerAddress while\n // accounting records the funds as paid out, so a self-reclaimer would strand\n // tokens in the contract with no sweep path.\n if (\n newReclaimerAddress == address(0) ||\n newReclaimerAddress == address(this)\n ) {\n revert RecallablePayment_InvalidReclaimerAddress();\n }\n if (newReclaimerAddress == reclaimerAddress) {\n revert RecallablePayment_ReclaimerAddressUnchanged();\n }\n reclaimerAddress = newReclaimerAddress;\n emit ReclaimerAddressChanged(\n _msgSender(),\n _previousReclaimer,\n newReclaimerAddress\n );\n }\n\n /// @inheritdoc IRecallablePayment\n function pause(\n bool shouldPause_\n ) external override onlyContractAdminOrTransferAdmin {\n if (shouldPause_) {\n _pause();\n } else {\n _unpause();\n }\n }\n\n // =============================================================================\n // Funding lifecycle: createDistribution (allocate) then fundDividend (deposit).\n // Auto-starts only from fundDividend once fully funded. Allocations stay editable\n // until claiming opens at unlockedAtTs. The claim-unlock time is pinned by the first\n // 4-arg fundDividend, so a schedule set on a partial deposit is honored at auto-start\n // instead of being silently overridden by a later top-up (the 3-arg overload funds freely).\n // =============================================================================\n\n /// @inheritdoc IRecallablePayment\n function createDistribution(\n address token_,\n uint256 timestamp_,\n address[] calldata recipients_,\n uint256[] calldata amounts_\n )\n external\n override\n onlyTransferAdmin\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n {\n if (token_ == address(0)) {\n revert RecallablePayment_InvalidTokenAddress();\n }\n uint256 _length = recipients_.length;\n if (_length == 0) {\n revert RecallablePayment_EmptyRecipients();\n }\n if (_length != amounts_.length) {\n revert RecallablePayment_InvalidArrayLengths();\n }\n\n Distribution storage _distribution = distributions[timestamp_][token_];\n // Allocations stay editable until claiming actually opens (started AND the\n // unlock time has been reached AND fully funded — i.e. `_isDistributionClaimable`).\n // This keeps a correction window open after auto-start for scheduled distributions,\n // and — because the funding-coverage check is included — also keeps it open when the\n // unlock time has passed while under-funded, so an admin can lower over-raised\n // allocations back to coverage instead of being forced to deposit the shortfall.\n // Once claims are actually live (funded), the recipient set is frozen so the coverage\n // invariant cannot be disturbed mid-claim. Editing only ever runs while claimed and\n // reclaimed are 0: a successful claim/reclaim requires coverage, and once\n // started/unlocked/funded all hold the unlock time is frozen and `started` is sticky,\n // so this editable state can never be re-entered after any claim.\n if (_isDistributionClaimable(_distribution)) {\n revert RecallablePayment_DistributionClaimingOpen();\n }\n\n // Cache the inner mapping pointer so the loop computes only the final\n // keccak level per recipient instead of re-hashing timestamp/token each\n // iteration (this is the O(N) hot path).\n mapping(address => UserAllocation)\n storage _userAllocations = userAllocations[timestamp_][token_];\n // Allocations are overwrite-on-change: each submitted amount replaces the\n // recipient's current allocation, applying a signed net delta to the running\n // total. This lets admins correct mistakes by re-uploading only the changed\n // rows (amount 0 removes a recipient) instead of stacking amounts. Edits are\n // only possible while claiming has not opened (`_isDistributionClaimable` reverts\n // above): started AND unlocked AND fully funded.\n // A single AllocationUpdated event per recipient carries the new amount, where\n // 0 means removed and a positive value means created/updated; the resulting\n // distribution total is readable via totalAllocatedAt.\n address _admin = _msgSender();\n uint256 _totalAllocated = _distribution.totalAllocated;\n for (uint256 i = 0; i < _length; ) {\n address _recipient = recipients_[i];\n uint256 _newAmount = amounts_[i];\n // The contract cannot claim from itself, so a self-allocation is either\n // dead weight or an upload mistake — reject alongside the zero address.\n if (_recipient == address(0) || _recipient == address(this)) {\n revert RecallablePayment_InvalidRecipientAddress(i);\n }\n UserAllocation storage _user = _userAllocations[_recipient];\n uint256 _oldAmount = _user.allocated;\n // Re-submitting the same amount (including 0 for a recipient that has\n // none) is a no-op and almost always a stale/duplicate upload: reject it.\n if (_newAmount == _oldAmount) {\n revert RecallablePayment_AllocationUnchanged(i);\n }\n if (_newAmount > _oldAmount) {\n _totalAllocated += (_newAmount - _oldAmount);\n } else {\n // Decrease or removal: allocations are only editable before claiming\n // opens, which means claimed and reclaimed are always 0 in this branch\n // (both are gated on _requireDistributionClaimable). Any underflow in\n // _unclaimedBalance is therefore impossible here and would revert with a\n // Solidity 0.8 panic if the invariant were ever violated by a future change.\n _totalAllocated -= (_oldAmount - _newAmount);\n }\n _user.allocated = _newAmount;\n emit AllocationUpdated(\n token_,\n _recipient,\n timestamp_,\n _newAmount,\n _admin\n );\n unchecked {\n ++i;\n }\n }\n\n _distribution.totalAllocated = _totalAllocated;\n }\n\n /// @inheritdoc IDividends\n /// @dev This function makes sure that the token is of the correct type. `_fundAndMaybeStart`\n /// reads the contract balance before the transfer and after the transfer. If the increase is\n /// not the same as the amount, the function fails with `RecallablePayment_InvalidFeeApplied`.\n /// The check is exact. It rejects a transfer fee. It also rejects an unexpected increase of the\n /// balance.\n ///\n /// The claim functions and the reclaim functions do not do this check. This is correct. If this\n /// function fails, no tokens stay in the contract. If a claim function fails, the tokens stay in\n /// the contract, and no function can recover them.\n function fundDividend(\n address token_,\n uint256 amount_,\n uint256 timestamp_\n )\n external\n override\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n {\n // 3-arg overload carries no explicit unlock: pass 0 (\"open now\"). It funds freely and\n // honors any unlock a prior 4-arg deposit pinned; if none, auto-start uses block.timestamp.\n _fundAndMaybeStart(token_, amount_, timestamp_, 0);\n }\n\n /// @inheritdoc IRecallablePayment\n /// @dev This function makes sure that the token is of the correct type. `_fundAndMaybeStart`\n /// reads the contract balance before the transfer and after the transfer. If the increase is\n /// not the same as the amount, the function fails with `RecallablePayment_InvalidFeeApplied`.\n /// The check is exact. It rejects a transfer fee. It also rejects an unexpected increase of the\n /// balance.\n ///\n /// The claim functions and the reclaim functions do not do this check. This is correct. If this\n /// function fails, no tokens stay in the contract. If a claim function fails, the tokens stay in\n /// the contract, and no function can recover them.\n function fundDividend(\n address token_,\n uint256 amount_,\n uint256 timestamp_,\n uint256 unlockedAtTs_\n )\n external\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n {\n Distribution storage _distribution = distributions[timestamp_][token_];\n if (\n _distribution.started &&\n unlockedAtTs_ != _distribution.unlockedAtTs\n ) {\n revert RecallablePayment_UnlockTimeNotApplicableAfterStart(\n unlockedAtTs_,\n _distribution.unlockedAtTs\n );\n }\n _fundAndMaybeStart(token_, amount_, timestamp_, unlockedAtTs_);\n }\n\n /// @inheritdoc IRecallablePayment\n /// @dev Not gated on global pause: admins may schedule per-distribution claim windows\n /// (including early-open or postpone) while the contract is paused, as long as\n /// claiming has not yet opened.\n function setUnlockTime(\n address token_,\n uint256 timestamp_,\n uint256 unlockedAtTs_\n )\n external\n override\n onlyTransferAdmin\n onlyValidTimestamp(timestamp_)\n {\n Distribution storage _distribution = distributions[timestamp_][token_];\n if (!_distribution.started) {\n revert RecallablePayment_DistributionNotStarted();\n }\n // Once claiming has opened (block.timestamp >= unlockedAtTs) the unlock time\n // is frozen: re-locking would let an admin lock out recipients while keeping\n // reclaimDividend open, and would re-open the createDistribution editing window.\n if (block.timestamp >= _distribution.unlockedAtTs) {\n revert RecallablePayment_DistributionClaimingOpen();\n }\n // \"Open now\" request: commit the actual time (mirrors _maybeAutoStart) so a\n // started distribution never reads back an ambiguous 0 from unlockTimeAt —\n // 0 only ever means \"not yet pinned\" on a not-yet-started distribution.\n if (unlockedAtTs_ == 0) {\n unlockedAtTs_ = block.timestamp;\n }\n _distribution.unlockedAtTs = unlockedAtTs_;\n\n emit DistributionUnlockTimeUpdated(\n _msgSender(),\n token_,\n timestamp_,\n unlockedAtTs_\n );\n }\n\n // =============================================================================\n // Claiming\n // =============================================================================\n\n /// @inheritdoc IDividends\n function claimDividend(\n address token_,\n uint256 timestamp_,\n uint256 amount_\n )\n public\n override\n nonReentrant\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n {\n _requireDistributionClaimable(token_, timestamp_);\n _claimDividend(token_, _msgSender(), timestamp_, amount_, false);\n }\n\n /// @inheritdoc IRecallablePayment\n function forceClaimDividend(\n address token_,\n address wallet_,\n uint256 timestamp_,\n uint256 amount_\n )\n public\n override\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n {\n // A payout to the contract itself would leave the tokens in place while\n // accounting marks them claimed — stranded with no sweep path.\n if (wallet_ == address(0) || wallet_ == address(this)) {\n revert RecallablePayment_InvalidWalletAddress();\n }\n _requireDistributionClaimable(token_, timestamp_);\n _claimDividend(token_, wallet_, timestamp_, amount_, true);\n }\n\n /// @inheritdoc IDividends\n function batchClaimDividend(\n address token_,\n uint256[] calldata timestamps_,\n uint256[] calldata amounts_\n ) external override whenNotPaused {\n uint256 _length = timestamps_.length;\n if (_length != amounts_.length) {\n revert RecallablePayment_InvalidArrayLengths();\n }\n for (uint256 i = 0; i < _length; ++i) {\n claimDividend(token_, timestamps_[i], amounts_[i]);\n }\n }\n\n /// @inheritdoc IRecallablePayment\n function batchForceClaimDividend(\n address token_,\n address wallet_,\n uint256[] calldata timestamps_,\n uint256[] calldata amounts_\n ) external override onlyTransferAdmin whenNotPaused {\n uint256 _length = timestamps_.length;\n if (_length != amounts_.length) {\n revert RecallablePayment_InvalidArrayLengths();\n }\n for (uint256 i = 0; i < _length; ++i) {\n forceClaimDividend(token_, wallet_, timestamps_[i], amounts_[i]);\n }\n }\n\n // =============================================================================\n // Reclaiming\n // =============================================================================\n\n /// @inheritdoc IDividends\n function reclaimDividend(\n address token_,\n address targetAddress_,\n uint256 timestamp_,\n uint256 amount_\n )\n external\n override\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n onlyValidReclaimerAddress\n {\n // Reclaim is symmetric with claimDividend: only callable once the distribution\n // is started, the unlock time has been reached, and it is fully funded. This\n // prevents an admin from reclaiming while recipients are locked out.\n _requireDistributionClaimable(token_, timestamp_);\n\n UserAllocation storage _user = userAllocations[timestamp_][token_][\n targetAddress_\n ];\n uint256 _unclaimed = _unclaimedBalance(_user);\n\n if (_unclaimed == 0) {\n revert RecallablePayment_NoRemainingUnclaimedBalance();\n }\n if (amount_ == 0) {\n amount_ = _unclaimed;\n }\n if (amount_ > _unclaimed) {\n revert RecallablePayment_NotEnoughFundsToClaim();\n }\n\n // Bound the payout by the distribution's own on-hand funds so a reclaim can\n // never pull tokens deposited for other distributions of the same token\n // (which share one physical balance). Defense-in-depth: the claimable check\n // above already guarantees full funding, so this only binds if totalClaimed +\n // totalReclaimed somehow exceeded totalFunds.\n Distribution storage _distribution = distributions[timestamp_][token_];\n uint256 _available = _distribution.totalFunds -\n _distribution.totalClaimed -\n _distribution.totalReclaimed;\n if (amount_ > _available) {\n revert RecallablePayment_InsufficientDistributionFunds(\n amount_,\n _available\n );\n }\n\n _user.reclaimed += amount_;\n _distribution.totalReclaimed += amount_;\n\n emit DividendReclaimed(\n _msgSender(),\n targetAddress_,\n token_,\n amount_,\n timestamp_\n );\n\n IERC20(token_).safeTransfer(reclaimerAddress, amount_);\n }\n\n /// @inheritdoc IRecallablePayment\n function reclaimSurplus(\n address token_,\n uint256 amount_,\n uint256 timestamp_\n )\n external\n override\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n onlyValidReclaimerAddress\n {\n Distribution storage _distribution = distributions[timestamp_][token_];\n // Surplus = deposited funds that were never assigned to a recipient.\n // This can never touch amounts still owed to recipients. With funding\n // decoupled from allocation, over-funding produces a real surplus, so\n // this path is reachable (it is not gated on `started`). Allocations stay\n // editable until claim-open, so totalAllocated may exceed totalFunds (no\n // surplus); guard the subtraction to revert cleanly instead of underflowing.\n if (_distribution.totalFunds <= _distribution.totalAllocated) {\n revert RecallablePayment_NoFundsToClaim();\n }\n uint256 _surplus = _distribution.totalFunds -\n _distribution.totalAllocated;\n if (amount_ == 0) {\n amount_ = _surplus;\n }\n if (amount_ > _surplus) {\n revert RecallablePayment_NotEnoughFundsToClaim();\n }\n\n _distribution.totalFunds -= amount_;\n\n emit DividendReclaimed(\n _msgSender(),\n address(this),\n token_,\n amount_,\n timestamp_\n );\n\n IERC20(token_).safeTransfer(reclaimerAddress, amount_);\n }\n\n /// @inheritdoc IRecallablePayment\n function reclaimTotalDividend(\n address token_,\n uint256 amount_,\n uint256 timestamp_\n )\n external\n override\n nonReentrant\n onlyTransferAdmin\n whenNotPaused\n onlyValidTimestamp(timestamp_)\n onlyValidReclaimerAddress\n {\n Distribution storage _distribution = distributions[timestamp_][token_];\n // IDividends contract: the entire remaining pool may only be recalled while\n // nothing has been claimed or reclaimed for this distribution. Unlike\n // reclaimSurplus, this is not bounded to the over-funded portion, so it can\n // pull funds that were allocated to recipients — safe only because no\n // recipient has exercised any entitlement yet.\n if (\n _distribution.totalClaimed != 0 ||\n _distribution.totalReclaimed != 0\n ) {\n revert RecallablePayment_DividendsAlreadyClaimed();\n }\n uint256 _available = _distribution.totalFunds;\n if (_available == 0) {\n revert RecallablePayment_NoFundsToClaim();\n }\n if (amount_ == 0) {\n amount_ = _available;\n }\n if (amount_ > _available) {\n revert RecallablePayment_NotEnoughFundsToClaim();\n }\n\n _distribution.totalFunds -= amount_;\n\n emit DividendReclaimed(\n _msgSender(),\n address(this),\n token_,\n amount_,\n timestamp_\n );\n\n IERC20(token_).safeTransfer(reclaimerAddress, amount_);\n }\n\n // =============================================================================\n // Wallet recovery\n // =============================================================================\n\n /// @inheritdoc IRecallablePayment\n function forceTransferDividend(\n address token_,\n uint256 timestamp_,\n address oldWallet_,\n address newWallet_\n ) external override onlyContractAdmin onlyValidTimestamp(timestamp_) {\n if (token_ == address(0)) {\n revert RecallablePayment_InvalidTokenAddress();\n }\n // The contract cannot claim from itself, so a position parked on\n // address(this) would be dead weight. Rejecting it here (as\n // createDistribution and forceClaimDividend do) closes the last\n // path that could create a self-position, making it unrepresentable.\n if (\n oldWallet_ == address(0) ||\n newWallet_ == address(0) ||\n newWallet_ == address(this)\n ) {\n revert RecallablePayment_InvalidWalletAddress();\n }\n if (oldWallet_ == newWallet_) {\n revert RecallablePayment_SameWallet();\n }\n\n UserAllocation storage _oldUser = userAllocations[timestamp_][token_][\n oldWallet_\n ];\n uint256 _oldAllocated = _oldUser.allocated;\n uint256 _oldClaimed = _oldUser.claimed;\n uint256 _oldReclaimed = _oldUser.reclaimed;\n\n if (\n _oldAllocated == 0 && _oldClaimed == 0 && _oldReclaimed == 0\n ) {\n revert RecallablePayment_NoPositionToTransfer();\n }\n\n UserAllocation storage _newUser = userAllocations[timestamp_][token_][\n newWallet_\n ];\n _newUser.allocated += _oldAllocated;\n _newUser.claimed += _oldClaimed;\n _newUser.reclaimed += _oldReclaimed;\n\n _oldUser.allocated = 0;\n _oldUser.claimed = 0;\n _oldUser.reclaimed = 0;\n\n emit DividendForceTransferred(\n token_,\n timestamp_,\n oldWallet_,\n newWallet_,\n _oldAllocated,\n _oldClaimed,\n _oldReclaimed,\n _msgSender()\n );\n }\n\n // =============================================================================\n // Views\n // =============================================================================\n\n /// @inheritdoc IDividends\n function fundsAt(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n return distributions[timestamp_][token_].totalFunds;\n }\n\n /// @inheritdoc IRecallablePayment\n function isStarted(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (bool) {\n return distributions[timestamp_][token_].started;\n }\n\n /// @inheritdoc IRecallablePayment\n function unlockTimeAt(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n return distributions[timestamp_][token_].unlockedAtTs;\n }\n\n /// @inheritdoc IRecallablePayment\n function isClaimable(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (bool) {\n return _isDistributionClaimable(distributions[timestamp_][token_]);\n }\n\n /// @inheritdoc IDividends\n function tokensAt(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n Distribution storage _distribution = distributions[timestamp_][token_];\n return\n _distribution.totalFunds -\n _distribution.totalClaimed -\n _distribution.totalReclaimed;\n }\n\n /// @inheritdoc IRecallablePayment\n function totalAllocatedAt(\n address token_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n return distributions[timestamp_][token_].totalAllocated;\n }\n\n /// @inheritdoc IDividends\n function totalAwardedBalanceAt(\n address token_,\n address recipient_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n return userAllocations[timestamp_][token_][recipient_].allocated;\n }\n\n /// @inheritdoc IRecallablePayment\n function allocatedBalanceAt(\n address token_,\n address recipient_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n return userAllocations[timestamp_][token_][recipient_].allocated;\n }\n\n /// @inheritdoc IDividends\n function claimedBalanceAt(\n address token_,\n address recipient_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n return userAllocations[timestamp_][token_][recipient_].claimed;\n }\n\n /// @inheritdoc IRecallablePayment\n function reclaimedBalanceAt(\n address token_,\n address recipient_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n return userAllocations[timestamp_][token_][recipient_].reclaimed;\n }\n\n /// @inheritdoc IRecallablePayment\n function unclaimedBalanceAt(\n address token_,\n address recipient_,\n uint256 timestamp_\n ) external view override onlyValidTimestamp(timestamp_) returns (uint256) {\n // Gated on claimability (not just raw allocated - claimed - reclaimed) so a\n // plain-IDividends consumer never sees a non-zero \"claimable\" balance for a\n // distribution whose claim would actually revert (not started / still\n // time-locked / under-funded). See `_isDistributionClaimable`.\n if (!_isDistributionClaimable(distributions[timestamp_][token_])) {\n return 0;\n }\n return\n _unclaimedBalance(userAllocations[timestamp_][token_][recipient_]);\n }\n\n function _fundAndMaybeStart(\n address token_,\n uint256 amount_,\n uint256 timestamp_,\n uint256 unlockedAtTs_\n ) internal {\n if (token_ == address(0)) {\n revert RecallablePayment_InvalidTokenAddress();\n }\n if (amount_ == 0) {\n revert RecallablePayment_InvalidAmount(0);\n }\n\n // Funding requires at least one allocation and may run incrementally until\n // fully funded, and as a top-up after auto-start. Funds deposited beyond\n // allocations are recoverable via reclaimSurplus.\n Distribution storage _distribution = distributions[timestamp_][token_];\n if (_distribution.totalAllocated == 0) {\n revert RecallablePayment_NoAllocationsToFund();\n }\n\n // Pin the claim-unlock time on the first 4-arg deposit that specifies one; every later\n // 4-arg deposit must present the same value or revert, so a future unlock scheduled on\n // a partial deposit survives to auto-start instead of being overwritten by a top-up.\n // The 3-arg overload carries no unlock (passes 0): it funds freely and honors whatever\n // is pinned, preserving its original \"just fund\" behavior. When nothing is pinned,\n // auto-start commits block.timestamp (open now); `unlockedAtTs == 0` means \"not yet\n // pinned\" (also the documented no-time-lock value). No-op once started: the post-start\n // unlock time is governed by the 4-arg guard above and setUnlockTime.\n if (!_distribution.started && unlockedAtTs_ != 0) {\n if (_distribution.unlockedAtTs == 0) {\n _distribution.unlockedAtTs = unlockedAtTs_;\n } else if (unlockedAtTs_ != _distribution.unlockedAtTs) {\n revert RecallablePayment_UnlockTimeConflict(\n unlockedAtTs_,\n _distribution.unlockedAtTs\n );\n }\n }\n\n _distribution.totalFunds += amount_;\n\n emit DividendFunded(_msgSender(), token_, amount_, timestamp_);\n\n uint256 _balanceBefore = IERC20(token_).balanceOf(address(this));\n IERC20(token_).safeTransferFrom(_msgSender(), address(this), amount_);\n uint256 _balanceAfter = IERC20(token_).balanceOf(address(this));\n if (_balanceBefore + amount_ != _balanceAfter) {\n revert RecallablePayment_InvalidFeeApplied();\n }\n\n _maybeAutoStart(token_, timestamp_);\n }\n\n /// @dev Open claiming as soon as a distribution is both fully allocated and fully\n /// funded. A no-op until both hold; only reachable from `fundDividend`.\n /// When the gate passes, sets `started` and commits the claim-open time: the unlock\n /// pinned by a 4-arg deposit, or `block.timestamp` for the pure 3-arg / open-now flow\n /// (`unlockedAtTs == 0`). A future value schedules claiming; a past/now value opens\n /// it immediately.\n function _maybeAutoStart(address token_, uint256 timestamp_) internal {\n Distribution storage _distribution = distributions[timestamp_][token_];\n if (\n !_distribution.started &&\n _distribution.totalAllocated > 0 &&\n _distribution.totalFunds >= _distribution.totalAllocated\n ) {\n _distribution.started = true;\n if (_distribution.unlockedAtTs == 0) {\n _distribution.unlockedAtTs = block.timestamp;\n }\n\n emit DistributionStarted(\n _msgSender(),\n token_,\n timestamp_,\n _distribution.totalAllocated,\n _distribution.totalFunds,\n _distribution.unlockedAtTs\n );\n }\n }\n\n function _unclaimedBalance(\n UserAllocation storage user_\n ) internal view returns (uint256) {\n return user_.allocated - user_.claimed - user_.reclaimed;\n }\n\n /// @dev Single source of truth for \"claiming is open\": started, at least one live\n /// allocation, unlock time reached, and fully funded. `isClaimable` and the\n /// `createDistribution` edit-freeze both route through this so the two can never\n /// drift. `_requireDistributionClaimable` enforces the same conditions but reverts\n /// with a distinct reason per gate.\n ///\n /// The `totalAllocated > 0` term matters: a started scheduled distribution can have\n /// every allocation removed during the pre-unlock edit window. Without this term the\n /// empty slot would read as claimable once the unlock time passed (`totalFunds >= 0`\n /// always holds), permanently freezing the edit window while `fundDividend` reverts\n /// on `totalAllocated == 0` — bricking the (token, timestamp) identifier. Requiring a\n /// live allocation keeps the emptied slot editable so it can be revived or reused.\n /// A genuinely live distribution always has `totalAllocated > 0` (it cannot start at\n /// 0 and claims never reduce `totalAllocated`), so the edit-freeze is unaffected.\n function _isDistributionClaimable(\n Distribution storage _distribution\n ) private view returns (bool) {\n return\n _distribution.started &&\n _distribution.totalAllocated > 0 &&\n block.timestamp >= _distribution.unlockedAtTs &&\n _distribution.totalFunds >= _distribution.totalAllocated;\n }\n\n function _requireDistributionClaimable(\n address token_,\n uint256 timestamp_\n ) internal view {\n Distribution storage _distribution = distributions[timestamp_][token_];\n if (!_distribution.started) {\n revert RecallablePayment_DistributionNotStarted();\n }\n // Claiming opens only once the admin-defined unlock time is reached.\n // This gate covers claimDividend, forceClaimDividend, batchClaimDividend,\n // and reclaimDividend (all routes through here).\n if (block.timestamp < _distribution.unlockedAtTs) {\n revert RecallablePayment_DistributionNotUnlocked();\n }\n // Coverage is enforced here (not at start) because allocations stay editable\n // until claiming opens: a post-start correction may have pushed totalAllocated\n // above totalFunds. Block claims until the gap is funded so no claim can run the\n // contract dry. Once claiming is live the recipient set is frozen, so this holds\n // for the whole claim window.\n if (_distribution.totalFunds < _distribution.totalAllocated) {\n revert RecallablePayment_NotFullyFunded(\n _distribution.totalAllocated,\n _distribution.totalFunds\n );\n }\n }\n\n function _claimDividend(\n address token_,\n address wallet_,\n uint256 timestamp_,\n uint256 amount_,\n bool isForceClaimByAdmin_\n ) internal {\n UserAllocation storage _user = userAllocations[timestamp_][token_][\n wallet_\n ];\n uint256 _unclaimed = _unclaimedBalance(_user);\n\n if (_unclaimed == 0) {\n revert RecallablePayment_NoRemainingUnclaimedBalance();\n }\n if (amount_ == 0) {\n amount_ = _unclaimed;\n }\n if (amount_ > _unclaimed) {\n revert RecallablePayment_NotEnoughFundsToClaim();\n }\n\n _user.claimed += amount_;\n distributions[timestamp_][token_].totalClaimed += amount_;\n\n if (isForceClaimByAdmin_) {\n emit DividendForceClaimed(\n _msgSender(),\n wallet_,\n token_,\n amount_,\n timestamp_\n );\n } else {\n emit DividendClaimed(wallet_, token_, amount_, timestamp_);\n }\n\n IERC20(token_).safeTransfer(wallet_, amount_);\n }\n}\n"},"contracts/RestrictedLockupToken.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {IERC165, ERC165} from \"@openzeppelin/contracts/utils/introspection/ERC165.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {BitScan} from \"@solidity-bits/contracts/BitScan.sol\";\nimport {ITransferRules} from \"./interfaces/ITransferRules.sol\";\nimport {ISnapshotPeriods} from \"./interfaces/ISnapshotPeriods.sol\";\nimport {IIdentityRegistry} from \"./interfaces/IIdentityRegistry.sol\";\nimport {IERC1404} from \"./interfaces/IERC1404.sol\";\nimport {SnapshotPeriods} from \"./SnapshotPeriods.sol\";\nimport {VestingMath} from \"./libraries/VestingMath.sol\";\nimport {BitManipulationLib} from \"./libraries/BitManipulationLib.sol\";\nimport {EligibilityCacheLib} from \"./libraries/EligibilityCacheLib.sol\";\nimport {Storage} from \"./Storage.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport {IRestrictedLockupTokenExtension} from \"./interfaces/IRestrictedLockupTokenExtension.sol\";\nimport \"./AccessControlErrors.sol\"; // Import for access to errors\n\n/**\n @title A smart contract for unlocking tokens based on a release schedule with multi-type token support\n @author By CoMakery, Inc., Upside, Republic\n @dev When deployed the contract is as a proxy for a single token that it creates release schedules for\n it implements the ERC20 token interface to integrate with wallets but it is not an independent token.\n The token must implement a burn function.\n Enhanced with multi-type token functionality for different regulatory classifications.\n*/\ncontract RestrictedLockupToken is\n Storage,\n ERC20,\n ReentrancyGuard,\n ERC165,\n ERC2771Context\n{\n using BitScan for uint256;\n using BitManipulationLib for uint256;\n /// @dev Struct to hold constructor parameters to avoid stack too deep issues\n struct ConstructorParams {\n address transferRules;\n address accessControl; // external access control\n address snapshotPeriods;\n address trustedForwarder;\n address identityRegistry;\n address restrictedLockupTokenManagementExtension;\n address restrictedLockupTokenExtension;\n address restrictedLockupTokenStandardsExtension;\n string symbol;\n string name;\n uint8 decimals;\n uint256 maxTotalSupply;\n uint256 minTimelockAmount;\n uint256 maxReleaseDelay;\n bool recordMintTimestamp;\n }\n\n struct TransferParams {\n address from;\n address to;\n // Recipient identity, AML/KYC status and the per-token-type eligibility cache\n RecipientEligibility recipient;\n bool validateRestrictions;\n uint256 remainingAmount;\n uint256 bitmaskToSet;\n uint256 bitmaskToClear;\n // mintTimestampCount read once; it cannot change during a transfer\n uint256 mintTimestampCount;\n }\n\n uint8 private immutable _decimals;\n\n /**\n * @dev Configure deployment for a specific token with release schedule security parameters\n * @dev The symbol should end with \" Unlock\" & be less than 11 characters for MetaMask \"custom token\" compatibility\n */\n constructor(\n ConstructorParams memory params\n )\n ERC20(params.name, params.symbol)\n ERC2771Context(params.trustedForwarder)\n {\n // Restricted Token\n if (bytes(params.name).length == 0) {\n revert RestrictedLockupToken_InvalidName();\n }\n if (bytes(params.symbol).length == 0) {\n revert RestrictedLockupToken_InvalidSymbol();\n }\n if (params.transferRules == address(0)) {\n revert RestrictedLockupToken_InvalidTransferRules();\n }\n if (params.accessControl == address(0)) {\n revert RestrictedLockupToken_InvalidAccessControl();\n }\n if (params.trustedForwarder == address(0)) {\n revert RestrictedLockupToken_InvalidTrustedForwarder();\n }\n if (params.identityRegistry == address(0)) {\n revert RestrictedLockupToken_InvalidIdentityRegistry();\n }\n if (params.restrictedLockupTokenManagementExtension == address(0)) {\n revert RestrictedLockupToken_InvalidRestrictedLockupTokenManagementExtension();\n }\n if (params.restrictedLockupTokenExtension == address(0)) {\n revert RestrictedLockupToken_InvalidRestrictedLockupTokenExtension();\n }\n if (params.restrictedLockupTokenStandardsExtension == address(0)) {\n revert RestrictedLockupToken_InvalidRestrictedLockupTokenStandardsExtension();\n }\n if (params.snapshotPeriods == address(0)) {\n snapshotsEnabled = false;\n } else {\n snapshotsEnabled = true;\n }\n // Token Lockup\n if (params.minTimelockAmount == 0) {\n revert RestrictedLockupToken_InvalidMinTimelockAmount();\n }\n if (params.maxTotalSupply > _maxSafeSupply()) {\n revert RestrictedLockupToken_MaxTotalSupplyTooLarge();\n }\n // Transfer rules can be swapped out for a new contract inheriting from the ITransferRules interface\n transferRules = ITransferRules(params.transferRules);\n snapshotPeriods = ISnapshotPeriods(params.snapshotPeriods);\n identityRegistry = IIdentityRegistry(params.identityRegistry);\n accessControl = IAccessControl(params.accessControl);\n restrictedLockupTokenManagementExtension = params.restrictedLockupTokenManagementExtension;\n restrictedLockupTokenExtension = params.restrictedLockupTokenExtension;\n restrictedLockupTokenStandardsExtension = params.restrictedLockupTokenStandardsExtension;\n\n maxTotalSupply = params.maxTotalSupply;\n _decimals = params.decimals;\n\n minTimelockAmount = params.minTimelockAmount;\n maxReleaseDelay = params.maxReleaseDelay;\n recordMintTimestamp = params.recordMintTimestamp;\n deploymentDay = VestingMath.toMidnightTimestamp(block.timestamp);\n \n // Calculate and set immutable slotsPerWord based on maxTotalSupply (already in basis units)\n slotsPerWord = BitManipulationLib.calculateSlotsPerWord(maxTotalSupply);\n uint256 slotsPerWordExtension = IRestrictedLockupTokenExtension(restrictedLockupTokenExtension).slotsPerWord();\n if (slotsPerWordExtension != slotsPerWord) {\n revert RestrictedLockupToken_ExtensionSlotsPerWordMismatch(slotsPerWord, slotsPerWordExtension);\n }\n slotsPerWordExtension = IRestrictedLockupTokenExtension(restrictedLockupTokenManagementExtension).slotsPerWord();\n if (slotsPerWordExtension != slotsPerWord) {\n revert RestrictedLockupToken_ExtensionSlotsPerWordMismatch(slotsPerWord, slotsPerWordExtension);\n }\n slotsPerWordExtension = IRestrictedLockupTokenExtension(restrictedLockupTokenStandardsExtension).slotsPerWord();\n if (slotsPerWordExtension != slotsPerWord) {\n revert RestrictedLockupToken_ExtensionSlotsPerWordMismatch(slotsPerWord, slotsPerWordExtension);\n }\n maxBalancePerSubIndex = BitManipulationLib.calculateMaxBalancePerSubIndex(slotsPerWord);\n elementBitSize = BitManipulationLib.calculateElementBitSize(slotsPerWord);\n \n // Initialize delegated function selectors\n _initializeDelegatedSelectors();\n }\n\n // ============================================\n // FALLBACK PATTERN FOR HOLDER MANAGEMENT\n // ============================================\n\n /// @dev Function selectors that should be delegated to the management extension\n mapping(bytes4 => bool) private _delegatedToManagementExtension;\n /// @dev Function selectors that should be delegated to the extension\n mapping(bytes4 => bool) private _delegatedToExtension;\n /// @dev Function selectors that should be delegated to the permit extension\n mapping(bytes4 => bool) private _delegatedToStandardsExtension;\n\n /**\n * @dev Initialize delegated function selectors in constructor\n * This is done in a separate function to avoid constructor size issues\n */\n function _initializeDelegatedSelectors() private {\n // Management Extension - Holder management functions\n _delegatedToManagementExtension[bytes4(keccak256(\"createHolderFromAddress(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"appendHolderAddress(address,uint256)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"addHolderWithAddresses(address[])\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"removeHolder(uint256)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"removeWalletFromHolder(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"batchRemoveWalletFromHolder(address[])\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"getHolderAddresses(uint256)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"getHolderId(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"addressHasHolder(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"holderExists(uint256)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"setHolderMax(uint256)\"))] = true;\n\n // Management Extension - Admin functions\n _delegatedToManagementExtension[bytes4(keccak256(\"pause(bool)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"freeze(address,bool)\"))] = true;\n\n // Extension - Release schedule functions\n _delegatedToExtension[bytes4(keccak256(\"createReleaseSchedule(uint256,uint256,uint256,uint256)\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"fundReleaseScheduleWithHolding((address,uint256,uint256,uint256,uint256),address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"fundReleaseSchedule(address,uint256,uint256,uint256,address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"batchFundReleaseScheduleWithHolding((address,uint256,uint256,uint256,uint256)[],address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"batchFundReleaseSchedule(address[],uint256[],uint256[],uint256[],address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"mintReleaseScheduleTokenType(address,uint256,uint256,uint256,uint256,address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"mintReleaseSchedule(address,uint256,uint256,uint256,address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"batchMintReleaseScheduleTokenType(address[],uint256[],uint256[],uint256[],uint256[],address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"batchMintReleaseSchedule(address[],uint256[],uint256[],uint256[],address[])\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"updateTimelockTokenType(address,uint256,uint256)\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"findHoldingFor(address,address,uint256)\"))] = true;\n\n // Extension - Burn functions, safe appove, max total supply and holding management\n _delegatedToExtension[bytes4(keccak256(\"burn(address,uint256)\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"softBurn(address,uint256)\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"burnHolding(address,uint256,uint256)\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"updateHoldingTokenType(address,uint256,uint256,uint256,uint256)\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"safeApprove(address,uint256)\"))] = true;\n _delegatedToExtension[bytes4(keccak256(\"setMaxTotalSupply(uint256)\"))] = true;\n\n // Management Extension - Holding view functions\n _delegatedToManagementExtension[bytes4(keccak256(\"holdingCountOf(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"holdingOf(address,uint256)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"previewTransferableFromHoldings(address,address,uint256)\"))] = true;\n // Management Extension - Upgrade functions\n _delegatedToManagementExtension[bytes4(keccak256(\"upgradeTransferRules(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"upgradeIdentityRegistry(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"getFrozenStatus(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"isValidTransferRules(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"isValidIdentityRegistry(address)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"existingTokenTypesCount()\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"existingTokenTypes(uint256)\"))] = true;\n _delegatedToManagementExtension[bytes4(keccak256(\"tokenTypeExists(uint256)\"))] = true;\n\n // Permit Extension - EIP-2612\n _delegatedToStandardsExtension[bytes4(keccak256(\"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)\"))] = true;\n _delegatedToStandardsExtension[bytes4(keccak256(\"nonces(address)\"))] = true;\n _delegatedToStandardsExtension[bytes4(keccak256(\"DOMAIN_SEPARATOR()\"))] = true;\n }\n \n /**\n * @dev Fallback function that delegates calls to appropriate extension\n */\n fallback() external {\n bytes4 selector = msg.sig;\n address extension;\n \n if (_delegatedToManagementExtension[selector]) {\n extension = restrictedLockupTokenManagementExtension;\n } else if (_delegatedToExtension[selector]) {\n extension = restrictedLockupTokenExtension;\n } else if (_delegatedToStandardsExtension[selector]) {\n extension = restrictedLockupTokenStandardsExtension;\n } else {\n revert(\"Function not found\");\n }\n \n assembly (\"memory-safe\") {\n // Copy calldata to memory\n calldatacopy(0, 0, calldatasize())\n \n // Delegatecall to extension\n let result := delegatecall(gas(), extension, 0, calldatasize(), 0, 0)\n \n // Copy return data\n returndatacopy(0, 0, returndatasize())\n \n // Return or revert based on result\n switch result\n case 0 { revert(0, returndatasize()) }\n default { return(0, returndatasize()) }\n }\n }\n\n /**\n * Support of ERC165\n * @dev See https://eips.ethereum.org/EIPS/eip-165\n * @param interfaceId_ The interface identifier, as specified in ERC-165\n */\n function supportsInterface(\n bytes4 interfaceId_\n ) public view override(ERC165) returns (bool) {\n return\n interfaceId_ == type(IERC1404).interfaceId ||\n super.supportsInterface(interfaceId_);\n }\n\n /**\n * @dev get circulating token supply, ie tokens that have already been minted and are in circulation. Akin to Outstanding, Issued shares.\n * @return _circulatingTokenSupply total circulating token supply.\n * @notice circulatingTokenSupply() == totalSupply() == \"outstanding shares\" == \"issued shares\"\n */\n function circulatingTokenSupply()\n public\n view\n returns (uint256 _circulatingTokenSupply)\n {\n _circulatingTokenSupply = totalSupply();\n }\n\n /**\n * @dev get unissued token supply, ie tokens that can still be minted. Akin to Unissued shares.\n * @return _unissuedTokenSupply total unissued token supply.\n */\n function unissuedTokenSupply()\n public\n view\n returns (uint256 _unissuedTokenSupply)\n {\n _unissuedTokenSupply = maxTotalSupply - circulatingTokenSupply();\n }\n\n /**\n * @dev Calls the TransferRules detectTransferRetriction function to determine if tokens can be transferred.\n * detectTransferRestriction returns a status code. ERC-1404 standard.\n * @param from_ The address the tokens are transferred from\n * @param to_ The address the tokens would be transferred to\n * @param value_ The quantity of tokens to be transferred\n */\n function detectTransferRestriction(\n address from_,\n address to_,\n uint256 value_\n ) public view returns (uint256) {\n return\n transferRules.detectTransferRestriction(\n address(this),\n from_,\n to_,\n value_\n );\n }\n\n /**\n * @dev Calls the TransferRules checkTransferAllowed function to determine if tokens of a specific type can be transferred to a recipient.\n * This function checks transfer restrictions based on token type, mint timestamp, and recipient address.\n * @param tokenType_ The type of token being transferred\n * @param mintTimestamp_ The timestamp when the tokens were originally minted\n * @param recipientIdentity The identity of the recipient\n * @param isAmlKycPassed_ Whether the recipient has passed AML/KYC\n * @return uint256 A status code indicating if the transfer is allowed (0 = allowed, non-zero = restricted)\n */\n function detectTransferRestrictionFor(\n uint256 tokenType_,\n uint256 mintTimestamp_,\n IIdentityRegistry.IdentityInfo memory recipientIdentity,\n bool isAmlKycPassed_\n ) public view returns (uint256) {\n return transferRules.detectTransferRestrictionForHolding(\n tokenType_,\n mintTimestamp_,\n recipientIdentity,\n isAmlKycPassed_\n );\n }\n\n /**\n * @dev Calls TransferRules to look up a human readable error message for a given error code.\n * This is the original implementation that accepts a uint256 parameter; the uint8 overload below provides ERC-1404 compliance.\n * @param restrictionCode_ is an error code to lookup an error code for\n * @return a human readable error message\n */\n function messageForTransferRestriction(\n uint256 restrictionCode_\n ) public view returns (string memory) {\n return transferRules.messageForTransferRestriction(restrictionCode_);\n }\n\n /**\n * @dev Calls TransferRules to lookup a human readable error message that goes with an error code. ERC-1404 standard.\n * @param restrictionCode_ is an error code to lookup an error code for, u8 for ERC-1404 standard compatibility\n * @return a human readable error message\n */\n function messageForTransferRestriction(\n uint8 restrictionCode_\n ) public view returns (string memory) {\n return transferRules.messageForTransferRestriction(uint256(restrictionCode_));\n }\n\n /**\n * @notice Balance of simple ERC20 tokens without any timelocks for a given address\n * @param who_ Address to calculate\n * @return amount The amount of simple ERC20 tokens available. See token.balanceOf\n **/\n function superBalanceOf(address who_) public view returns (uint256) {\n return\n balanceOf(who_) - unlockedBalanceOf(who_) - lockedBalanceOf(who_);\n }\n\n /**\n * @notice Total unlocked balance for a given address - 1) balance of simple ERC20 tokens without any timelocks (superBalanceOf) + 2) unlocked tokens that remain (unlockedBalanceOf)\n * @param who_ Address to calculate\n * @return amount The unlocked total balance of\n **/\n function unlockedTotalBalanceOf(\n address who_\n ) public view returns (uint256) {\n return balanceOf(who_) - lockedBalanceOf(who_);\n }\n\n /**\n * @notice Total locked balance for a given address across all timelocks\n * @param who_ Address to calculate\n * @return amount_\n */\n function lockedBalanceOf(\n address who_\n ) public view returns (uint256 amount_) {\n uint256 _timelockCountOf = timelockCountOf(who_);\n for (uint256 i; i < _timelockCountOf; ++i) {\n amount_ += lockedBalanceOfTimelock(who_, i);\n }\n }\n\n /**\n * @dev Override ERC20 transferFrom to enforce transfer restrictions\n * @param from_ is the address to transfer from\n * @param to_ is the address to transfer to\n * @return true if transfer is successful\n */\n function transferFrom(\n address from_,\n address to_,\n uint256 amount_\n ) public virtual override returns (bool) {\n _enforceTransferRestrictions(from_, to_, amount_);\n uint256 transferredAmount = _updateTimelock(from_, to_, amount_, true);\n _handleMultiTypeTransfer(from_, to_, amount_ - transferredAmount, true);\n super.transferFrom(from_, to_, amount_);\n\n return true;\n }\n\n /**\n * @notice Total unlocked balance remaining for a given address across all timelocks\n * @param who_ Address to calculate\n * @return amount_\n */\n function unlockedBalanceOf(\n address who_\n ) public view returns (uint256 amount_) {\n uint256 _timelockCountOf = timelockCountOf(who_);\n for (uint256 i; i < _timelockCountOf; ++i) {\n amount_ += unlockedBalanceOfTimelock(who_, i);\n }\n }\n\n /**\n * @dev get token decimals\n * @return decimals\n */\n function decimals() public view virtual override returns (uint8) {\n return _decimals;\n }\n\n /**\n * @notice calculates how many tokens would be released at a specified time for a scheduleId.\n This is independent of any specific address or address's timelock.\n * @param commencedTimestamp_ the commencement time to use in the calculation for the scheduled\n * @param currentTimestamp_ the timestamp to calculate unlocked tokens for\n * @param amount_ the amount of tokens\n * @param scheduleId_ the schedule id used to calculate the unlocked amount\n * @return _unlocked the total amount unlocked for the schedule given the other parameters\n */\n function calculateUnlocked(\n uint256 commencedTimestamp_,\n uint256 currentTimestamp_,\n uint256 amount_,\n uint256 scheduleId_\n ) public view returns (uint256) {\n return\n calculateUnlocked(\n commencedTimestamp_,\n currentTimestamp_,\n amount_,\n releaseSchedules[scheduleId_]\n );\n }\n\n /**\n * @notice returns the total count of timelocks for a specific address\n * @param who_ the address to get the timelock count for\n * @return number of timelocks\n */\n function timelockCountOf(address who_) public view returns (uint256) {\n return timelocks[who_].length;\n }\n\n /**\n * @notice Get The locked balance for a specific address and specific timelock\n * @param who_ The address to check\n * @param timelockIndex_ Specific timelock belonging to the who address\n * @return _locked Balance of the timelock\n */\n function lockedBalanceOfTimelock(\n address who_,\n uint256 timelockIndex_\n ) public view returns (uint256 _locked) {\n Timelock memory _timelock = timelockOf(who_, timelockIndex_);\n if (_timelock.totalAmount > _timelock.tokensTransferred) {\n _locked =\n _timelock.totalAmount -\n totalUnlockedToDateOfTimelock(who_, timelockIndex_);\n }\n }\n\n /**\n * @notice Get the unlocked balance for a specific address and specific timelock\n * @param who_ the address to check\n * @param timelockIndex_ for a specific timelock belonging to the who address\n * @return _unlocked balance of the timelock\n */\n function unlockedBalanceOfTimelock(\n address who_,\n uint256 timelockIndex_\n ) public view returns (uint256 _unlocked) {\n Timelock memory _timelock = timelockOf(who_, timelockIndex_);\n if (_timelock.totalAmount > _timelock.tokensTransferred) {\n _unlocked =\n totalUnlockedToDateOfTimelock(who_, timelockIndex_) -\n _timelock.tokensTransferred;\n }\n }\n\n /**\n * @notice Gets the total locked and unlocked balance of a specific address's timelocks to the current block timestamp\n * @param who_ The address to check\n * @param timelockIndex_ The index of the timelock for the who address\n * @return Locked and unlocked amount for the specified timelock\n */\n function totalUnlockedToDateOfTimelock(\n address who_,\n uint256 timelockIndex_\n ) public view returns (uint256) {\n Timelock memory _timelock = timelockOf(who_, timelockIndex_);\n\n return\n calculateUnlocked(\n _timelock.commencementTimestamp,\n block.timestamp,\n _timelock.totalAmount,\n _timelock.scheduleId\n );\n }\n\n /**\n * @notice Get the struct details for an address's specific timelock\n * @param who_ Address to check\n * @param index_ The index of the timelock for the who address\n * @return Struct with the attributes of the timelock\n */\n function timelockOf(\n address who_,\n uint256 index_\n ) public view returns (Timelock memory) {\n return timelocks[who_][index_];\n }\n\n /**\n * @notice calculates how many tokens would be released at a specified time for a ReleaseSchedule struct.\n This is independent of any specific address or address's timelock.\n\n * @param commencedTimestamp_ the commencement time to use in the calculation for the scheduled\n * @param currentTimestamp_ the timestamp to calculate unlocked tokens for\n * @param amount_ the amount of tokens\n * @param releaseSchedule_ a ReleaseSchedule struct used to calculate the unlocked amount\n * @return the total amount unlocked for the schedule given the other parameters\n */\n function calculateUnlocked(\n uint256 commencedTimestamp_,\n uint256 currentTimestamp_,\n uint256 amount_,\n ReleaseSchedule memory releaseSchedule_\n ) public pure returns (uint256) {\n return VestingMath.calculateUnlocked(\n commencedTimestamp_,\n currentTimestamp_,\n amount_,\n releaseSchedule_.releaseCount,\n releaseSchedule_.delayUntilFirstReleaseInSeconds,\n releaseSchedule_.initialReleasePortionInBips,\n releaseSchedule_.periodBetweenReleasesInSeconds\n );\n }\n\n /**\n * @notice ERC20 standard interface function\n * Provide controls of Restricted and Lockup tokens\n * Can transfer simple ERC20 tokens and unlocked tokens at the same time\n * First will transfer unlocked tokens and then simple ERC20\n * @param to_ recipient of transfer\n * @param amount_ amount of tokens to transfer\n * @return bool. True on success / Reverted on error\n */\n function transfer(\n address to_,\n uint256 amount_\n ) public virtual override returns (bool) {\n _enforceTransferRestrictions(_msgSender(), to_, amount_);\n return _transfer(to_, amount_);\n }\n\n /**\n * @dev Transfer a holding of a specific token type\n * @param to recipient of transfer\n * @param amount amount of tokens to transfer\n * @param globalHoldingIdx holding index\n * @return bool. True on success / Reverted on error\n */\n function transferHolding(\n address to,\n uint256 amount,\n uint256 globalHoldingIdx\n ) external returns (bool) {\n if (amount == 0) {\n revert RestrictedLockupToken_InvalidAmount();\n }\n if (globalHoldingIdx >= mintTimestampCount) {\n revert RestrictedLockupToken_HoldingIndexOutOfBound();\n }\n\n address _sender = _msgSender();\n // Get real balance (considering overflow)\n uint256 senderBalance = _getRealBalance(_sender, globalHoldingIdx);\n if (amount > senderBalance) {\n revert RestrictedLockupToken_NoItemWithEnoughBalance();\n }\n\n _enforceTransferRestrictions(_sender, to, amount);\n _processHoldingTransfer(\n _sender,\n to,\n amount,\n globalHoldingIdx,\n senderBalance\n );\n super._transfer(_sender, to, amount);\n\n return true;\n }\n\n /**\n * @dev Internal function to process holding transfer (reduces stack depth)\n */\n function _processHoldingTransfer(\n address sender,\n address to,\n uint256 amount,\n uint256 globalHoldingIdx,\n uint256 senderBalance\n ) internal {\n uint256 bucketIndex = globalHoldingIdx / slotsPerWord;\n uint256 subIndex = globalHoldingIdx % slotsPerWord;\n uint256 element = BitManipulationLib.getPackedElement(globalMintTimestamps[bucketIndex], subIndex, elementBitSize);\n (uint256 tokenType, uint256 daysAfterDeployment) = BitManipulationLib.unpackElement(element);\n\n _enforceTransferRestrictionsFor(tokenType, BitManipulationLib.daysSinceDeployToTimestamp(daysAfterDeployment, deploymentDay), to);\n _executeTransferFromHolding(\n sender,\n to,\n tokenType,\n daysAfterDeployment,\n globalHoldingIdx,\n senderBalance,\n amount\n );\n }\n\n /**\n * @dev Allows the reserve admin to create new tokens in a specified address.\n * The total number of tokens cannot exceed the maxTotalSupply (the \"Hard Cap\").\n * Automatically determines the appropriate token type based on recipient's identity.\n * @param to_ The addres to mint tokens into.\n * @param value_ The number of tokens to mint.\n */\n function mint(\n address to_,\n uint256 value_\n ) public validAddress(to_) onlyReserveOrMintAdmin {\n // Determine token type based on recipient's identity\n uint256 _mintTokenType = transferRules.determineTokenType(to_, identityRegistry);\n _mintWithType(to_, value_, _mintTokenType);\n }\n\n /**\n * @dev batchTransfer tokens\n */\n function batchTransfer(\n address[] calldata recipients,\n uint256[] calldata amounts\n ) external returns (bool) {\n uint256 _recipientsLength = recipients.length;\n if (_recipientsLength != amounts.length) {\n revert RestrictedLockupToken_RecipientAndAmountLengthsShouldMatch();\n }\n for (uint256 i; i < _recipientsLength; ++i) {\n transfer(recipients[i], amounts[i]);\n }\n\n return true;\n }\n\n /**\n * @dev Transfer tokens from one address to another with ignoring of transfer rules\n * With reserve admin access only\n * @param from_ sender address\n * @param to_ recipient address\n * @param amount_ amount of tokens\n */\n function forceTransferBetween(\n address from_,\n address to_,\n uint256 amount_\n ) external onlyReserveAdmin onlyExistingAddress(to_) {\n if (from_ == to_) {\n revert RestrictedLockupToken_SenderCannotBeRecipient(from_);\n }\n if (from_ == address(0)) {\n revert RestrictedLockupToken_InvalidZeroAddress();\n }\n if (amount_ == 0) {\n revert RestrictedLockupToken_InvalidAmount();\n }\n\n uint256 transferredAmount = _updateTimelock(from_, to_, amount_, false);\n _handleMultiTypeTransfer(from_, to_, amount_ - transferredAmount, false);\n\n super._transfer(from_, to_, amount_);\n emit ForceTransferBetween(_msgSender(), from_, to_, amount_);\n }\n\n /**\n * @notice the total number of schedules that have been created\n * @return count of schedules\n */\n function scheduleCount() external view returns (uint256 count) {\n return releaseSchedules.length;\n }\n\n /**\n * @notice Check the total remaining balance of a timelock including the locked and unlocked portions\n * @param who_ the address to check\n * @param timelockIndex_ Specific timelock belonging to the who address\n * @return total remaining balance of a timelock\n */\n function balanceOfTimelock(\n address who_,\n uint256 timelockIndex_\n ) external view returns (uint256) {\n Timelock memory _timelock = timelockOf(who_, timelockIndex_);\n if (_timelock.totalAmount <= _timelock.tokensTransferred) {\n return 0;\n } else {\n return _timelock.totalAmount - _timelock.tokensTransferred;\n }\n }\n\n /**\n * @notice Cancel a cancelable timelock created by the fundReleaseSchedule, fundReleaseScheduleWithHolding, mintReleaseSchedule, or mintReleaseScheduleTokenType functions.\n If canceled the tokens that are locked at the time of the cancellation will be returned to the funder\n and unlocked tokens will be transferred to the recipient.\n * @param target_ The initial recipient address of the timelock which will be cancelled.\n * @param timelockIndex_ timelock index\n * @param scheduleId_ require it matches expected\n * @param commencementTimestamp_ require it matches expected\n * @param totalAmount_ require it matches expected\n * @param reclaimTokenTo_ reclaim token to\n * @return success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function cancelTimelock(\n address target_,\n uint256 timelockIndex_,\n uint256 scheduleId_,\n uint256 commencementTimestamp_,\n uint256 totalAmount_,\n address reclaimTokenTo_\n ) external nonReentrant returns (bool success) {\n /// @notice cancelling moves tokens, so it is subject to the global pause kill switch.\n /// cancelableBy may contain arbitrary non-admin addresses, so this must not be bypassable.\n if (isPaused) {\n revert RestrictedLockupToken_TransfersPaused();\n }\n if (timelockCountOf(target_) <= timelockIndex_) {\n revert RestrictedLockupToken_InvalidTimelock();\n }\n if (reclaimTokenTo_ == address(0) || reclaimTokenTo_ == address(this)) {\n revert RestrictedLockupToken_InvalidReclaimTo();\n }\n if (frozenAddresses[reclaimTokenTo_]) {\n revert RestrictedLockupToken_ReclaimToFrozen(reclaimTokenTo_);\n }\n\n Timelock storage _timelock = timelocks[target_][timelockIndex_];\n\n if (!_canBeCanceled(_timelock)) {\n revert RestrictedLockupToken_TimelockCannotBeCanceled();\n }\n if (_timelock.scheduleId != scheduleId_) {\n revert RestrictedLockupToken_ScheduleIdDoesNotMatch();\n }\n if (_timelock.commencementTimestamp != commencementTimestamp_) {\n revert RestrictedLockupToken_CommencementTimestampDoesNotMatch();\n }\n if (_timelock.totalAmount != totalAmount_) {\n revert RestrictedLockupToken_TotalAmountDoesNotMatch();\n }\n\n uint256 _canceledAmount = lockedBalanceOfTimelock(\n target_,\n timelockIndex_\n );\n\n if (_canceledAmount == 0) {\n revert RestrictedLockupToken_TimelockHasNoValueRemaining();\n }\n\n // Get token type and mint timestamp from global holding index\n (uint256 tokenType, uint256 mintTimestamp) = _getTokenTypeAndMintTimestampFromGlobalIndex(_timelock.globalHoldingIndex);\n \n // Check token type transfer restrictions if mintTimestamp + lockDuration > current time\n // to keep restrictions consistent, we check the restrictions on the reclaimTokenTo address\n // in order to not allow to bypass restrictions by using a different address\n //\n // Only the holding-level rules are applied here. The sender-side checks of\n // detectTransferRestrictionBasic are INTENTIONALLY not enforced on this path:\n // - SENDER TOKENS LOCKED - expected, the canceled amount is locked by definition\n // - SENDER NOT AMLKYCPASSED - a clawback must still work after the holder loses eligibility\n // - SENDER ADDRESS FROZEN - required by the freeze -> cancelTimelock -> burn -> mint\n // recovery flow (locked tokens are not burnable directly,\n // see docs/restricted-lockup-token.md)\n // The recipient-side basic checks (pause, reclaimTokenTo frozen / zero / token contract)\n // ARE enforced, at the top of this function.\n _enforceTransferRestrictionsFor(\n tokenType,\n mintTimestamp,\n reclaimTokenTo_\n );\n uint256 _paidAmount = unlockedBalanceOfTimelock(\n target_,\n timelockIndex_\n );\n _timelock.tokensTransferred = _timelock.totalAmount;\n uint256 daysAfterDeployment = BitManipulationLib.calculateDaysAfterDeployment(mintTimestamp, deploymentDay);\n // Add to reclaimer's optimized holdings structure\n _addToHoldingsByIndex(reclaimTokenTo_, _timelock.globalHoldingIndex, _canceledAmount);\n \n // Add to target's optimized holdings structure if there's a paid amount\n if (_paidAmount > 0) {\n _addToHoldingsByIndex(target_, _timelock.globalHoldingIndex, _paidAmount);\n }\n // only canceledAmount must be transferred back from initial target recipient to reclaimTokenTo\n // paid amount (ie already-unlocked tokens) is already initially transferred to the target recipient but locked\n super._transfer(target_, reclaimTokenTo_, _canceledAmount);\n\n emit TimelockCanceled(\n _msgSender(),\n target_,\n timelockIndex_,\n reclaimTokenTo_,\n _canceledAmount,\n _paidAmount\n );\n return true;\n }\n\n /**\n * @notice transfers the unlocked token from an address's specific timelock\n It is typically more convenient to call transfer. But if the account has many timelocks the cost of gas\n for calling transfer may be too high. Calling transferTimelock from a specific timelock limits the transfer cost.\n * @param to_ the address that the tokens will be transferred to\n * @param amount_ the number of token base units to be transferred to the to address\n * @param timelockId_ the specific timelock of the function caller to transfer unlocked tokens from\n * @return bool always true when completed\n */\n function transferTimelock(\n address to_,\n uint256 amount_,\n uint256 timelockId_\n ) external nonReentrant returns (bool) {\n /// @notice a zero-amount transferTimelock moves nothing but still sets the recipient's\n /// holdings bitmap bit via _addToHoldingsByIndex, leaving a permanent zero-balance entry\n /// that costs the recipient gas on every later enumeration\n if (amount_ == 0) {\n revert RestrictedLockupToken_InvalidAmount();\n }\n\n address _sender = _msgSender();\n if (unlockedBalanceOfTimelock(_sender, timelockId_) < amount_) {\n revert RestrictedLockupToken_AmountExceedsUnlockedBalance();\n }\n\n Timelock storage _timelock = timelocks[_sender][timelockId_];\n \n // Get token type and mint timestamp from global holding index\n (uint256 tokenType, uint256 mintTimestamp) = _getTokenTypeAndMintTimestampFromGlobalIndex(_timelock.globalHoldingIndex);\n \n // Check token type transfer restrictions if mintTimestamp + lockDuration > current time\n _enforceTransferRestrictionsFor(\n tokenType,\n mintTimestamp,\n to_\n );\n _enforceTransferRestrictions(_sender, to_, amount_);\n\n // Update timelock state\n _timelock.tokensTransferred += amount_;\n\n // Add to recipient's optimized holdings structure\n _addToHoldingsByIndex(\n to_,\n _timelock.globalHoldingIndex,\n amount_\n );\n\n // Perform standard ERC20 transfer\n super._transfer(_sender, to_, amount_);\n\n // Emit token type transfer event\n emit TokenTypeTransferred(_sender, to_, amount_, tokenType);\n\n return true;\n }\n\n /**\n * @dev update hook\n * @notice During a burn, this is invoked by the _burn function (ERC20.sol) which artificially sets `to` to the 0x0 address. Tokens are NOT actually transferred to 0x0 during a burn.\n * @param from_ from address\n * @param to_ to address\n * @param amount_ amount\n */\n function _update(\n address from_,\n address to_,\n uint256 amount_\n ) internal virtual override {\n if (snapshotsEnabled) {\n snapshotPeriods.onUpdate(address(this), from_, to_, amount_);\n }\n super._update(from_, to_, amount_); // Call parent hook\n\n /// @notice when to is 0x0, it's a burn (note - 0x0 is not actually transferred any tokens during burn)\n /// do NOT create a holder in that case\n /// @notice zero-amount transfers must NOT create a holder either. They bypass the\n /// recipient-side compliance checks that _handleMultiTypeTransfer performs for real\n /// transfers, and transferFrom(from, to, 0) needs no allowance, so anyone could otherwise\n /// register arbitrary addresses as holders and exhaust holderMax.\n if (to_ != address(0) && amount_ > 0 && !_addressHasHolder(to_)) {\n _createHolderFromAddress(to_);\n }\n }\n\n /**\n * @notice Check if timelock can be cancelable by _msgSender()\n * @param timelock_ Timelock struct to check for cancelation\n */\n function _canBeCanceled(\n Timelock storage timelock_\n ) private view returns (bool) {\n uint256 _len = timelock_.cancelableBy.length;\n for (uint256 i; i < _len; ++i) {\n if (_msgSender() == timelock_.cancelableBy[i]) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * @notice Update the timelock state to account for unlocked tokens that are transferred out\n * @param from Address from\n * @param to Address to (for token type restriction checking)\n * @param amount_ Amount of tokens to transfer, includes unlocked tokens still within timelock, and simple tokens\n * @return _unlockedAmount Amount of unlocked tokens that are transferred out (does NOT include simple tokens)\n */\n function _updateTimelock(\n address from,\n address to,\n uint256 amount_,\n bool validateRestrictions\n ) private returns (uint256 _unlockedAmount) {\n if (amount_ == 0) {\n return 0;\n }\n uint256 _simpleTokenBalance = amount_;\n uint256 lockedBalance = 0;\n // Recipient context and per-token-type eligibility cache: the rules are resolved once\n // per token type and reused for every timelock of that type, instead of one\n // TransferRules call per timelock.\n RecipientEligibility memory recipient = _recipientEligibility(to);\n uint256 timelockCount = timelockCountOf(from);\n\n /// @notice transfer from unlocked tokens\n for (uint256 i; i < timelockCount; ++i) {\n Timelock storage timelock = timelocks[from][i];\n /// @notice if the timelock has no value left\n if (timelock.tokensTransferred == timelock.totalAmount) {\n continue;\n }\n uint256 _unlockedBalanceOfTimelock = unlockedBalanceOfTimelock(from, i);\n if (_unlockedBalanceOfTimelock == 0) {\n continue;\n }\n lockedBalance += lockedBalanceOfTimelock(from, i);\n\n uint256 globalHoldingIndex = timelock.globalHoldingIndex;\n (uint256 tokenType, uint256 daysAfterDeployment) = _getTokenTypeAndDaysFromGlobalIndex(globalHoldingIndex);\n if (validateRestrictions && !_timelockEligible(recipient, tokenType, daysAfterDeployment)) {\n continue; // skip this timelock because it's not allowed to transfer\n }\n\n if (_simpleTokenBalance > _unlockedBalanceOfTimelock) {\n _simpleTokenBalance -= _unlockedBalanceOfTimelock;\n timelock.tokensTransferred += _unlockedBalanceOfTimelock;\n // Add to recipient's optimized holdings structure\n _addToHoldingsByIndex(to, globalHoldingIndex, _unlockedBalanceOfTimelock);\n emit TokenTypeTransferred(from, to, _unlockedBalanceOfTimelock, tokenType);\n } else {\n timelock.tokensTransferred += _simpleTokenBalance;\n // Add to recipient's optimized holdings structure\n _addToHoldingsByIndex(to, globalHoldingIndex, _simpleTokenBalance);\n emit TokenTypeTransferred(from, to, _simpleTokenBalance, tokenType);\n _simpleTokenBalance = 0;\n break;\n }\n }\n // validate that we have enough total unlocked balance to transfer if we have any unlocked tokens left\n // balanceOf includes 1) locked, 2) unlocked, and 3) simple tokens\n // check if the amount to transfer is <= the total transferrable balance (unlocked + simple)\n if ((balanceOf(from) - lockedBalance) < amount_) {\n // revert RestrictedLockupToken_InvalidAmount();\n revert RestrictedLockupToken_InsufficientTotalBalanceOf();\n }\n // by the end, _simpleTokenBalance MUST be LTE the amount because unlocked amount is the difference\n _unlockedAmount = amount_ - _simpleTokenBalance;\n }\n\n /**\n * @dev Per-timelock eligibility check for _updateTimelock: the cache idiom described on\n * Storage._resolveEligibility. A separate function with a single call site so the\n * optimizer inlines it while the unoptimized coverage build keeps its stack frame\n * separate from the loop's.\n */\n function _timelockEligible(\n RecipientEligibility memory recipient,\n uint256 tokenType_,\n uint256 daysAfterDeployment_\n ) private view returns (bool) {\n uint256 entry = EligibilityCacheLib.entryFor(recipient.cache, tokenType_);\n if (!EligibilityCacheLib.isHit(entry, tokenType_)) {\n entry = _resolveEligibility(recipient, tokenType_);\n }\n return EligibilityCacheLib.admits(entry, daysAfterDeployment_);\n }\n\n /**\n * @param to address to send to\n * @param amount amount to send\n */\n function _transfer(address to, uint256 amount) private returns (bool) {\n address _sender = _msgSender();\n uint256 transferredAmount = _updateTimelock(_sender, to, amount, true);\n _handleMultiTypeTransfer(_sender, to, amount - transferredAmount, true);\n\n super._transfer(_sender, to, amount);\n return true;\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context, Storage)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n function snapshotPeriodsAddress() external view returns (address) {\n return address(snapshotPeriods);\n }\n\n function isAmlKycPassed(address wallet) external view returns (bool) {\n return identityRegistry.isAmlKycPassed(wallet);\n }\n\n function determineTokenType(address wallet) external view returns (uint256) {\n return transferRules.determineTokenType(wallet, identityRegistry);\n }\n\n /**\n * @dev Get total balance across all token types for an address\n * @param account The address to query\n * @return balance The total balance across all token types\n */\n function totalBalanceOf(address account) external view returns (uint256 balance) {\n return balanceOf(account); // Use standard ERC20 balanceOf which is kept in sync\n }\n\n /**\n * @dev Mint tokens of a specific type to an address\n * @param to The address to mint to\n * @param amount The amount to mint\n * @param tokenType The type of token to mint (0-255, uses 8 bits)\n */\n function mintTokenType(address to, uint256 amount, uint256 tokenType) external onlyReserveOrMintAdmin {\n // Verify tokenType is within 8-bit limit (0-255)\n if (tokenType > 255) {\n revert RestrictedLockupToken_InvalidTokenType();\n }\n \n // Verify if the specified token type is allowed for the recipient\n if (!transferRules.tokenTypeAllowed(tokenType, to, identityRegistry)) {\n revert RestrictedLockupToken_TokenTypeNotAllowedForRecipient();\n }\n _mintWithType(to, amount, tokenType);\n }\n\n /**\n * @dev Internal function to mint tokens with a specific type\n */\n function _mintWithType(address to, uint256 amount, uint256 tokenType) internal {\n if (amount == 0) {\n revert RestrictedLockupToken_InvalidZeroAmount();\n }\n if (totalSupply() + amount > maxTotalSupply) {\n revert RestrictedLockupToken_CannotExceedMaxTotalSupply();\n }\n\n uint256 dayMintTimestamp = recordMintTimestamp ? \n VestingMath.toMidnightTimestamp(block.timestamp) : \n BitManipulationLib.daysSinceDeployToTimestamp(recordPausedOnDaysAfterDeployment, deploymentDay);\n\n // Add to optimized holdings structure\n _addToHoldingsByType(\n to,\n tokenType,\n BitManipulationLib.calculateDaysAfterDeployment(dayMintTimestamp, deploymentDay),\n amount\n );\n\n // Perform standard ERC20 mint to maintain compatibility\n _mint(to, amount);\n\n emit TokenTypeMinted(to, amount, tokenType, dayMintTimestamp);\n }\n\n /**\n * @dev Process transfers for a single token type with cached identity info (optimized direct bitmap iteration)\n * @dev Gas optimization: Split into helper functions to avoid stack too deep, no pre-allocation\n */\n function _processTransfers(\n address from,\n address to,\n uint256 remainingAmount,\n bool validateRestrictions\n ) internal returns (uint256) {\n // Cache identity information outside the loop to save gas\n uint256 walletIndexesLength = walletIndexesByType[from].length;\n TransferParams memory transferParams = TransferParams({\n from: from,\n to: to,\n // Recipient eligibility is resolved once per token type, not once per holding;\n // see Storage._resolveEligibility.\n recipient: _recipientEligibility(to),\n validateRestrictions: validateRestrictions,\n remainingAmount: remainingAmount,\n bitmaskToSet: 0,\n bitmaskToClear: 0,\n mintTimestampCount: mintTimestampCount\n });\n\n // Iterate through bitmap words directly - no pre-allocation!\n for (uint256 i = 0; i < walletIndexesLength && transferParams.remainingAmount > 0; i++) {\n uint256 word = walletIndexesByType[from][i];\n if (word == 0) continue;\n\n // Process word with ultra-optimization (batched bitmap updates)\n transferParams = _processTransferBitmapWord(transferParams, word, i);\n }\n\n return transferParams.remainingAmount;\n }\n\n /**\n * @dev helper function to process transfers for a single bitmap word\n * @dev Processes transfers in bucket-aligned batches for maximum efficiency\n * @dev Uses new globalMintTimestamps structure with slotsPerWord elements per bucket\n */\n function _processTransferBitmapWord(\n TransferParams memory transferParams,\n uint256 word_,\n uint256 wordIndex_\n ) internal returns (TransferParams memory) {\n // Bits actually set in storage for this word. Only these may be cleared at the end:\n // _processTransferAlignedPackedHoldings marks every zero-balance slot of a bucket for\n // clearing, most of which were never set, and clearing an unset bit would be an SSTORE\n // that changes nothing.\n uint256 storedWord = word_;\n\n while (word_ != 0 && transferParams.remainingAmount > 0) {\n uint256 globalIndex = wordIndex_ * 256 + word_.bitScanForward256();\n\n // Process entire bucket if not already processed and it exists\n // Inline bucket calculations to reduce variable count\n if (globalIndex < transferParams.mintTimestampCount) {\n transferParams = _processTransferAlignedPackedHoldings(transferParams, globalIndex);\n\n // Clear all bits in the current word chunk regardless of what was processed\n // This ensures we don't process the same bucket multiple times\n // Clear all bits that belong to this bucket and are in the current word\n word_ = BitManipulationLib.clearBucketBits(word_, globalIndex, wordIndex_, slotsPerWord);\n } else {\n // Index out of range, just clear current bit\n word_ &= word_ - 1;\n }\n }\n\n // Apply batched bitmap changes for this word (only if we have changes)\n uint256 bitsToClear = transferParams.bitmaskToClear & storedWord;\n if (bitsToClear != 0) {\n _applyBitmaskToClear(transferParams.from, wordIndex_, bitsToClear);\n }\n transferParams.bitmaskToClear = 0;\n if (transferParams.bitmaskToSet > 0) {\n _applyBitmaskToWord(transferParams.to, wordIndex_, transferParams.bitmaskToSet);\n transferParams.bitmaskToSet = 0;\n }\n\n return transferParams;\n }\n \n\n /**\n * @dev Whether the holding at `subIndex_` of a bucket whose packed element word is\n * `packedTimestamps_` may move to the recipient of this transfer: unpacks the token type\n * and day, then applies the cache idiom described on Storage._resolveEligibility, so the\n * rules are resolved once per token type per transfer rather than once per holding.\n * Kept as a function with a single call site, and given the unpacking as well: the\n * optimizer inlines it, while the unoptimized coverage build - which already sits at the\n * EVM stack limit inside _processTransferAlignedPackedHoldings - keeps these locals out\n * of the loop's frame.\n */\n function _isHoldingEligible(\n TransferParams memory transferParams,\n uint256 packedTimestamps_,\n uint256 subIndex_\n ) private view returns (bool) {\n (uint256 tokenType, uint256 daysAfterDeployment) = BitManipulationLib.unpackElement(\n (packedTimestamps_ >> (subIndex_ * elementBitSize)) & ((1 << elementBitSize) - 1)\n );\n uint256 entry = EligibilityCacheLib.entryFor(transferParams.recipient.cache, tokenType);\n if (!EligibilityCacheLib.isHit(entry, tokenType)) {\n entry = _resolveEligibility(transferParams.recipient, tokenType);\n }\n return EligibilityCacheLib.admits(entry, daysAfterDeployment);\n }\n\n /**\n * @dev Process transfer from packed holdings (up to slotsPerWord balances at once)\n * @dev Uses new optimized globalMintTimestamps structure with slotsPerWord elements per bucket\n * @dev Returns remaining amount, total transferred amount, and bitmap masks for efficient updates\n */\n function _processTransferAlignedPackedHoldings(\n TransferParams memory transferParams,\n uint256 globalIndex\n ) internal returns (TransferParams memory) {\n uint256 tempRemainingAmount = transferParams.remainingAmount;\n \n // The bucket this global index falls in. Hoisted because it was recomputed seven\n // times below; `hardhat coverage` compiles instrumented and unoptimized, and the\n // repeated nesting pushed this frame one slot past the EVM stack limit there.\n uint256 bucketIndex = globalIndex / slotsPerWord;\n\n // Load the packed timestamps once for efficiency\n uint256 packedTimestamps = globalMintTimestamps[bucketIndex];\n\n // Get packed balances for this bucket once\n uint256 fromPackedBalances = packedBalancesByTypeAndTime[transferParams.from][bucketIndex];\n uint256 toPackedBalances = packedBalancesByTypeAndTime[transferParams.to][bucketIndex];\n\n // Process slotsPerWord consecutive holdings starting from bucket start\n // Align globalIndex to bucket start to process all holdings in the bucket\n for (uint256 i = 0; i < slotsPerWord && tempRemainingAmount > 0; i++) {\n // Bucket-aligned position of this holding. The word-boundary check below already\n // computed it, so hoisting costs nothing and lets every later use share it.\n uint256 currentGlobalIndex = bucketIndex * slotsPerWord + i;\n\n // Only process holdings that are in the same word as the original globalIndex\n // This prevents processing holdings from buckets that span across word boundaries\n if (currentGlobalIndex / 256 != globalIndex / 256) {\n continue;\n }\n uint256 packedBalance = BitManipulationLib.getPackedBalance(fromPackedBalances, i, elementBitSize);\n if (packedBalance == 0) {\n // A set bit with no balance is dead weight: it is re-scanned by every future\n // transfer, burn, holdingCountOf and holdingOf for the lifetime of the wallet, so\n // mark it for the batched clear this word already performs. The only such bits\n // are the amount-0 markers mintReleaseSchedule records for the beneficiary\n // (_fund -> _addToHoldingsByType(to, type, day, 0)). Nothing reads that marker\n // on-chain - the timelock carries its own globalHoldingIndex - so reclaiming it\n // only changes what holdingCountOf/holdingOf report for the wallet. Slots whose\n // bit was never set are masked back out in _processTransferBitmapWord.\n transferParams.bitmaskToClear |= (1 << (currentGlobalIndex % 256));\n continue;\n }\n\n // Check restrictions if validation is enabled\n if (transferParams.validateRestrictions && !_isHoldingEligible(transferParams, packedTimestamps, i)) {\n continue; // Skip this holding due to restrictions\n }\n \n // Get real balance (considering overflow)\n uint256 currentBalance = packedBalance;\n if (packedBalance == maxBalancePerSubIndex) {\n currentBalance = overflowBalances[transferParams.from][currentGlobalIndex];\n }\n \n // Calculate transfer amount for this holding\n uint256 transferAmount = tempRemainingAmount > currentBalance ? currentBalance : tempRemainingAmount;\n\n // Calculate new balances\n uint256 newFromBalance = currentBalance - transferAmount;\n \n // Handle sender balance update (considering overflow)\n if (newFromBalance == 0) {\n // Balance becomes zero, clear everything\n fromPackedBalances = BitManipulationLib.setPackedBalance(fromPackedBalances, i, 0, elementBitSize);\n if (packedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[transferParams.from][currentGlobalIndex];\n }\n // The bitmap bit is recorded in bitmaskToClear below and written once per word\n // by _applyBitmaskToClear; clearing it here as well was a duplicate SSTORE.\n } else if (newFromBalance < maxBalancePerSubIndex) {\n // New balance fits in uint32, store in packed storage\n fromPackedBalances = BitManipulationLib.setPackedBalance(fromPackedBalances, i, newFromBalance, elementBitSize);\n if (packedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[transferParams.from][currentGlobalIndex];\n }\n } else {\n // New balance still exceeds uint32 max, keep max in packed storage and update overflow mapping\n overflowBalances[transferParams.from][currentGlobalIndex] = newFromBalance;\n }\n \n uint256 newToBalance = 0;\n (newToBalance, toPackedBalances) = _updateRecipientBalance(\n transferParams.to,\n toPackedBalances,\n currentGlobalIndex,\n i,\n transferAmount\n );\n \n tempRemainingAmount -= transferAmount;\n \n // Set bit for recipient if they now have balance\n if (newToBalance > 0) {\n transferParams.bitmaskToSet |= (1 << currentGlobalIndex % 256);\n }\n\n // Clear bit for sender if they now have zero balance\n // Only clear bits that are in the current word to avoid cross-word issues\n if (newFromBalance == 0) {\n transferParams.bitmaskToClear |= (1 << currentGlobalIndex % 256);\n }\n \n // Emit transfer event\n emit TokenTypeTransferred(\n transferParams.from,\n transferParams.to,\n transferAmount,\n BitManipulationLib.unpackTokenTypeElement((packedTimestamps >> (i * elementBitSize)) & ((1 << elementBitSize) - 1))\n );\n }\n \n // Update storage only once if any transfers occurred\n if (transferParams.remainingAmount > tempRemainingAmount) {\n packedBalancesByTypeAndTime[transferParams.from][bucketIndex] = fromPackedBalances;\n packedBalancesByTypeAndTime[transferParams.to][bucketIndex] = toPackedBalances;\n transferParams.remainingAmount = tempRemainingAmount;\n }\n \n return transferParams;\n }\n\n function _updateRecipientBalance(\n address to,\n uint256 toPackedBalances_,\n uint256 globalIndex,\n uint256 subIndex,\n uint256 transferAmount\n ) internal returns (uint256 newToBalance, uint256 toPackedBalances) {\n uint256 currentToBalance = BitManipulationLib.getPackedBalance(toPackedBalances_, subIndex, elementBitSize);\n if (currentToBalance == maxBalancePerSubIndex) {\n currentToBalance = overflowBalances[to][globalIndex];\n }\n \n newToBalance = currentToBalance + transferAmount;\n toPackedBalances = _setPackedBalanceWithOverflow(\n to,\n globalIndex,\n subIndex,\n newToBalance,\n toPackedBalances_\n );\n }\n\n function _setPackedBalanceWithOverflow(\n address account,\n uint256 globalIndex,\n uint256 subIndex,\n uint256 newBalance,\n uint256 packedBalances\n ) internal returns (uint256 packedBalancesResult) {\n if (newBalance < maxBalancePerSubIndex) {\n // New balance fits in uint32, store in packed storage\n packedBalancesResult = BitManipulationLib.setPackedBalance(packedBalances, subIndex, newBalance, elementBitSize);\n } else {\n // New balance exceeds uint32 max, store max in packed storage and real amount in overflow mapping\n packedBalancesResult = BitManipulationLib.setPackedBalance(packedBalances, subIndex, maxBalancePerSubIndex, elementBitSize);\n overflowBalances[account][globalIndex] = newBalance;\n }\n }\n \n /**\n * @dev Helper function to update packed balance considering overflow\n * @param account The account to update\n * @param globalIndex_ The global index\n * @param newBalance The new balance value\n * @param oldPackedBalance The old packed balance value (for overflow detection)\n */\n function _updatePackedBalance(\n address account,\n uint256 globalIndex_,\n uint256 newBalance,\n uint256 oldPackedBalance\n ) internal {\n uint256 packedKey = BitManipulationLib.getPackedBalanceKeyFromGlobalIndex(globalIndex_, slotsPerWord);\n uint256 subIndex = globalIndex_ % slotsPerWord;\n uint256 packedBalances = packedBalancesByTypeAndTime[account][packedKey];\n \n if (newBalance == 0) {\n // Balance becomes zero, clear everything\n packedBalances = BitManipulationLib.setPackedBalance(packedBalances, subIndex, 0, elementBitSize);\n packedBalancesByTypeAndTime[account][packedKey] = packedBalances;\n // Clear any overflow balance\n if (oldPackedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[account][globalIndex_];\n }\n // Clear bitmap bit\n _clearBitmapBit(account, globalIndex_);\n } else if (newBalance < maxBalancePerSubIndex) {\n // New balance fits in uint32, store in packed storage\n packedBalances = BitManipulationLib.setPackedBalance(packedBalances, subIndex, newBalance, elementBitSize);\n packedBalancesByTypeAndTime[account][packedKey] = packedBalances;\n // Clear any existing overflow balance\n if (oldPackedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[account][globalIndex_];\n }\n } else {\n // New balance exceeds uint32 max, store max in packed storage and real amount in overflow mapping\n packedBalances = BitManipulationLib.setPackedBalance(packedBalances, subIndex, maxBalancePerSubIndex, elementBitSize);\n packedBalancesByTypeAndTime[account][packedKey] = packedBalances;\n overflowBalances[account][globalIndex_] = newBalance;\n }\n }\n\n /**\n * @dev Execute the actual transfer from a holding\n * @dev Final step with minimal variables\n */\n function _executeTransferFromHolding(\n address from,\n address to,\n uint256 tokenType,\n uint256 daysAfterDeployment,\n uint256 globalIndex_,\n uint256 currentBalance,\n uint256 remainingAmount_\n ) internal returns (uint256) {\n uint256 transferAmount = remainingAmount_ > currentBalance ? currentBalance : remainingAmount_;\n \n // Get old packed balance for overflow detection\n uint256 packedKey = BitManipulationLib.getPackedBalanceKeyFromGlobalIndex(globalIndex_, slotsPerWord);\n uint256 subIndex = globalIndex_ % slotsPerWord;\n uint256 packedBalance = BitManipulationLib.getPackedBalance(packedBalancesByTypeAndTime[from][packedKey], subIndex, elementBitSize);\n \n // Calculate and update new balance using helper\n uint256 newFromBalance = currentBalance - transferAmount;\n _updatePackedBalance(from, globalIndex_, newFromBalance, packedBalance);\n \n // Add to recipient's optimized holdings structure\n _addToHoldingsByIndex(to, globalIndex_, transferAmount);\n \n // Emit transfer event\n emit TokenTypeTransferred(from, to, transferAmount, tokenType);\n \n return remainingAmount_ - transferAmount;\n }\n\n /**\n * @dev Handle transfers with token type consideration (optimized structure only)\n */\n function _handleMultiTypeTransfer(address from, address to, uint256 amount, bool validateRestrictions) internal {\n // self-transfer 0 amount is allowed to convert timelock to simple tokens (holdings)\n if (amount == 0) {\n return;\n }\n if (from == to) {\n revert RestrictedLockupToken_SenderCannotBeRecipient(from);\n }\n \n // Process transfers for this token type\n uint256 remainingAmount = _processTransfers(\n from,\n to,\n amount,\n validateRestrictions\n );\n \n if (remainingAmount > 0) {\n revert RestrictedLockupToken_InsufficientTotalBalanceOf();\n }\n }\n\n /**\n * @dev Apply batched bitmap changes to a specific word\n * @dev This replaces multiple _setBitmapBit calls with a single OR operation\n */\n function _applyBitmaskToWord(\n address account,\n uint256 wordIndex,\n uint256 bitmaskToSet\n ) internal {\n // Ensure bitmap array is large enough\n while (walletIndexesByType[account].length <= wordIndex) {\n walletIndexesByType[account].push(0);\n }\n \n // Apply all bitmap changes for this word at once (major gas optimization)\n walletIndexesByType[account][wordIndex] |= bitmaskToSet;\n }\n\n function getWalletIndexesLength(address who_) public view returns (uint256) {\n return walletIndexesByType[who_].length;\n }\n \n function getWalletIndexes(address who_, uint256 index) public view returns (uint256) {\n return walletIndexesByType[who_][index];\n }\n\n /**\n * @dev Get total count of global holdings (each bucket contains slotsPerWord elements)\n * @return The total number of possible global holding indexes\n */\n function globalHoldingCount() public view returns (uint256) {\n return mintTimestampCount;\n }\n\n function setRecordMintTimestamp(bool enabled) external onlyTransferAdmin {\n if (enabled == recordMintTimestamp) {\n revert RestrictedLockupToken_AlreadySet();\n }\n \n // When disabling recordMintTimestamp, store the current daysAfterDeployment\n if (!enabled) {\n recordPausedOnDaysAfterDeployment = BitManipulationLib.calculateDaysAfterDeployment(\n VestingMath.toMidnightTimestamp(block.timestamp), \n deploymentDay\n );\n }\n \n recordMintTimestamp = enabled;\n }\n\n /**\n * @dev Get token type and mint timestamp from global holding index\n * @param globalIndex The global holding index\n * @return tokenType The token type\n * @return mintTimestamp The mint timestamp\n */\n function getTokenTypeAndMintTimestampFromGlobalIndex(uint256 globalIndex) external view returns (uint256 tokenType, uint256 mintTimestamp) {\n return _getTokenTypeAndMintTimestampFromGlobalIndex(globalIndex);\n }\n}\n"},"contracts/RestrictedLockupTokenExtension.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {BitScan} from \"@solidity-bits/contracts/BitScan.sol\";\nimport {Storage} from \"./Storage.sol\";\nimport {VestingMath} from \"./libraries/VestingMath.sol\";\nimport {BitManipulationLib} from \"./libraries/BitManipulationLib.sol\";\nimport {EligibilityCacheLib} from \"./libraries/EligibilityCacheLib.sol\";\nimport {IIdentityRegistry} from \"./interfaces/IIdentityRegistry.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title RestrictedLockupTokenExtension\n * @notice Extension contract for holding management and burn operations\n * @dev This contract is called via delegatecall from RestrictedLockupToken\n * Following the fallback extension pattern with identical storage layout\n * Handles fundReleaseSchedule, mintReleaseSchedule, setMaxTotalSupply,\n * safeApprove and all burn operations\n */\ncontract RestrictedLockupTokenExtension is\n Storage,\n ERC20,\n ERC2771Context,\n ReentrancyGuard\n{\n using BitScan for uint256;\n using BitManipulationLib for uint256;\n\n struct FundReleaseScheduleParams {\n address to;\n uint256 amount;\n uint256 commencementTimestamp;\n uint256 scheduleId;\n uint256 holdingIdx;\n }\n\n struct BurnParams {\n address from;\n uint256 globalIndex;\n uint256 remainingAmount;\n uint256 totalBurned;\n uint256 bitmaskToClear;\n uint256 bucketIndex;\n uint256 alignedBaseIndex;\n uint256 fromPackedBalances;\n uint256 packedTimestamps;\n }\n\n constructor(address trustedForwarder_, uint256 maxTotalSupply_)\n ERC20(\"Restricted Lockup Token Extension\", \"RLTE\")\n ERC2771Context(trustedForwarder_)\n {\n if (trustedForwarder_ == address(0)) {\n revert RestrictedLockupToken_InvalidTrustedForwarder();\n }\n maxTotalSupply = maxTotalSupply_;\n // Initialize immutable slotsPerWord with the same value as the main contract\n slotsPerWord = BitManipulationLib.calculateSlotsPerWord(maxTotalSupply_);\n maxBalancePerSubIndex = BitManipulationLib.calculateMaxBalancePerSubIndex(slotsPerWord);\n elementBitSize = BitManipulationLib.calculateElementBitSize(slotsPerWord);\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context, Storage)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n // ============================================\n // HOLDING MANAGEMENT\n // ============================================\n\n /**\n * @notice Update the token type for a holding\n * @dev Only callable by reserve admin. Converts tokens from one type to another while preserving mint timestamp\n * @param wallet The wallet address whose holding is being updated\n * @param oldTokenType The current token type of the holding\n * @param newTokenType The new token type to assign\n * @param globalIndex The global index of the holding to update\n * @param amount The amount of tokens to update (must not exceed holding balance). Use 0 to convert entire holding balance.\n */\n function updateHoldingTokenType(\n address wallet,\n uint256 oldTokenType,\n uint256 newTokenType,\n uint256 globalIndex,\n uint256 amount\n ) external onlyReserveAdmin {\n if (oldTokenType == newTokenType) {\n revert RestrictedLockupToken_InvalidTokenType();\n }\n if (globalIndex >= mintTimestampCount) {\n revert RestrictedLockupToken_InvalidHolding();\n }\n (uint256 daysAfterDeployment, , uint256 finalAmount) = \n _validateAndPrepareTokenTypeUpdate(wallet, oldTokenType, newTokenType, globalIndex, amount);\n \n // Remove from old token type\n _removeFromHoldings(wallet, finalAmount, globalIndex);\n // Add to new token type with same daysAfterDeployment\n _addToHoldingsByType(wallet, newTokenType, daysAfterDeployment, finalAmount);\n\n emit HoldingTokenTypeUpdated(\n _msgSender(),\n wallet,\n oldTokenType,\n newTokenType,\n BitManipulationLib.daysSinceDeployToTimestamp(daysAfterDeployment, deploymentDay),\n finalAmount\n );\n }\n\n /**\n * @dev Validate and prepare token type update parameters\n */\n function _validateAndPrepareTokenTypeUpdate(\n address wallet,\n uint256 oldTokenType,\n uint256 newTokenType,\n uint256 globalIndex,\n uint256 amount\n ) internal view returns (uint256 daysAfterDeployment, uint256 currentBalance, uint256 finalAmount) {\n daysAfterDeployment = _validateTokenTypeAndGetDays(oldTokenType, globalIndex);\n currentBalance = _getRealBalance(wallet, globalIndex);\n \n // If amount is 0, convert the entire balance of this holding\n finalAmount = amount == 0 ? currentBalance : amount;\n \n if (finalAmount == 0) {\n revert RestrictedLockupToken_InvalidAmount();\n }\n \n if (currentBalance < finalAmount) {\n revert RestrictedLockupToken_InsufficientTotalBalanceOf();\n }\n\n // Ensure the new token type is allowed for this wallet\n if (!transferRules.tokenTypeAllowed(newTokenType, wallet, identityRegistry)) {\n revert RestrictedLockupToken_TokenTypeNotAllowedForRecipient();\n }\n }\n\n /**\n * @dev Validate token type and get days after deployment\n */\n function _validateTokenTypeAndGetDays(\n uint256 expectedTokenType,\n uint256 globalIndex\n ) internal view returns (uint256 daysAfterDeployment) {\n uint256 packed = globalMintTimestamps[globalIndex / slotsPerWord];\n uint256 element = BitManipulationLib.getPackedElement(packed, globalIndex % slotsPerWord, elementBitSize);\n uint256 tokenType;\n (tokenType, daysAfterDeployment) = BitManipulationLib.unpackElement(element);\n \n if (tokenType != expectedTokenType) {\n revert RestrictedLockupToken_InvalidTokenType();\n }\n }\n\n /**\n * @dev Remove tokens from optimized holdings structure (bitmap-based)\n * @param account The account to remove tokens from\n * @param amount The amount to remove\n * @param holdingGlobalIdx The global index of the holding\n */\n function _removeFromHoldings(\n address account,\n uint256 amount,\n uint256 holdingGlobalIdx\n ) internal {\n // Get current balance from packed storage\n uint256 bucketIndex = holdingGlobalIdx / slotsPerWord;\n uint256 subIndex = holdingGlobalIdx % slotsPerWord;\n uint256 packedBalances = packedBalancesByTypeAndTime[account][bucketIndex];\n uint256 packedBalance = BitManipulationLib.getPackedBalance(packedBalances, subIndex, elementBitSize);\n \n // Get real balance (considering overflow)\n uint256 realBalance = packedBalance;\n if (packedBalance == maxBalancePerSubIndex) {\n realBalance = overflowBalances[account][holdingGlobalIdx];\n }\n \n if (realBalance < amount) {\n revert RestrictedLockupToken_InvalidAmount();\n }\n\n // Calculate new balance\n uint256 newBalance = realBalance - amount;\n \n // Update balance in packed storage and overflow mapping as needed\n if (newBalance == 0) {\n // Balance becomes zero, clear everything\n uint256 newPackedBalances = BitManipulationLib.setPackedBalance(packedBalances, subIndex, 0, elementBitSize);\n packedBalancesByTypeAndTime[account][bucketIndex] = newPackedBalances;\n // Clear any overflow balance\n if (packedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[account][holdingGlobalIdx];\n }\n // Clear bitmap bit\n _clearBitmapBit(account, holdingGlobalIdx);\n } else if (newBalance < maxBalancePerSubIndex) {\n // New balance fits in uint32, store in packed storage\n uint256 newPackedBalances = BitManipulationLib.setPackedBalance(packedBalances, subIndex, newBalance, elementBitSize);\n packedBalancesByTypeAndTime[account][bucketIndex] = newPackedBalances;\n // Clear any existing overflow balance\n if (packedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[account][holdingGlobalIdx];\n }\n } else {\n // New balance still exceeds uint32 max, keep max in packed storage and update overflow mapping\n overflowBalances[account][holdingGlobalIdx] = newBalance;\n }\n }\n\n function _superBalanceOf(address account) internal view returns (uint256) {\n return _queryBalance(\"superBalanceOf(address)\", account);\n }\n\n // ============================================\n // BURN FUNCTIONS\n // ============================================\n \n /**\n * @dev Destroys tokens and removes them from the total supply. Can only be called by an address with a Reserve Admin role.\n * @notice The only burnable amount of tokens are the simple tokens owned by a wallet. Timelocked tokens are not burnable.\n * @notice Cancel a timelock prior to burning tokens if there is a desire to burn timelocked tokens.\n * @notice DOES NOT ACTUALLY TRANSFER TOKENS TO 0X0! Merely emits Transfer event and calls _update with recipient artificially set to 0x0.\n * @param from_ The address to destroy the tokens from.\n * @param value_ The number of tokens to destroy from the address.\n */\n function burn(\n address from_,\n uint256 value_\n ) external validAddress(from_) onlyReserveAdmin {\n if (_superBalanceOf(from_) < value_) {\n revert RestrictedLockupToken_InsufficientBurnableBalance();\n }\n _burnWithHoldingsDestroy(from_, value_);\n }\n\n function burnHolding(\n address from_,\n uint256 globalHoldingIdx,\n uint256 amount\n ) external validAddress(from_) onlyReserveAdmin {\n if (globalHoldingIdx >= mintTimestampCount) {\n revert RestrictedLockupToken_HoldingIndexOutOfBound();\n }\n\n _burnSingleHolding(from_, globalHoldingIdx, amount);\n _burn(from_, amount);\n }\n\n /**\n * @dev Burns tokens from an account using the soft burn mechanism.\n * Can only be called by an address with the Soft Burn Admin role.\n * Requires sufficient allowance and burnable balance.\n * Burns tokens in FIFO order across all token types and holdings.\n * @param from_ The address to burn tokens from.\n * @param value_ The number of tokens to burn.\n */\n function softBurn(\n address from_,\n uint256 value_\n ) external onlySoftBurnAdmin {\n address spender = _msgSender();\n _spendAllowance(from_, spender, value_);\n \n if (_superBalanceOf(from_) < value_) {\n revert RestrictedLockupToken_InsufficientBurnableBalance();\n }\n _burnWithHoldingsDestroy(from_, value_);\n }\n\n /**\n * @dev Internal function to burn tokens and destroy holdings in FIFO order (optimized direct bitmap iteration)\n * @dev Gas optimization: Reduced local variables to avoid stack too deep, iterate bitmap directly\n * @param from_ The address to destroy the tokens from\n * @param value_ The number of tokens to destroy\n */\n function _burnWithHoldingsDestroy(address from_, uint256 value_) internal {\n _burnHoldings(from_, value_);\n \n // Perform standard ERC20 burn\n _burn(from_, value_);\n }\n\n /**\n * @dev Helper function to burn holdings for a specific token type\n * @dev Separated to avoid stack too deep issues\n * @param from_ The address to burn from\n * @param amount Amount left to burn\n */\n function _burnHoldings(\n address from_, \n uint256 amount\n ) internal {\n uint256 remainingAmount = amount;\n \n // Iterate through bitmap words directly - no pre-allocation!\n for (uint256 wordIndex = 0; wordIndex < walletIndexesByType[from_].length && remainingAmount > 0; wordIndex++) {\n uint256 word = walletIndexesByType[from_][wordIndex];\n if (word == 0) continue;\n \n // Process each set bit in this word\n remainingAmount = _processBitmapWordForBurn(from_, word, wordIndex, remainingAmount);\n }\n // we have not validated that remaining amount is 0\n // because it is validated that superBalanceOf is greater than \n // or equal to amount in burn and soft burn functions\n }\n\n /**\n * @dev Process a single bitmap word for burning\n * @dev Further separated to minimize stack usage\n */\n function _processBitmapWordForBurn(\n address from_,\n uint256 word_,\n uint256 wordIndex_,\n uint256 remainingAmount_\n ) internal returns (uint256) {\n uint256 bitmaskToClear = 0; // Collect all bits to clear for sender\n\n while (word_ != 0 && remainingAmount_ > 0) {\n uint256 globalIndex = wordIndex_ * 256 + word_.bitScanForward256();\n\n // Process entire bucket if not already processed and it exists\n if (globalIndex < mintTimestampCount) {\n uint256 totalBurned;\n uint256 bucketBitmaskToClear;\n (remainingAmount_, totalBurned, bucketBitmaskToClear) = _processBurnAlignedPackedHoldings(\n from_, globalIndex, remainingAmount_\n );\n\n // Accumulate bitmasks from the processed bucket\n if (totalBurned > 0) {\n bitmaskToClear |= bucketBitmaskToClear;\n }\n\n // Clear all bits that belong to this bucket and are in the current word\n word_ = BitManipulationLib.clearBucketBits(word_, globalIndex, wordIndex_, slotsPerWord);\n } else {\n // Index out of range, just clear current bit\n word_ &= word_ - 1;\n }\n }\n\n // Apply batched bitmap changes for this word (only if we have changes)\n if (bitmaskToClear > 0) {\n _applyBitmaskToClear(from_, wordIndex_, bitmaskToClear);\n }\n\n return remainingAmount_;\n }\n\n /**\n * @dev OPTIMIZED process burn from packed holdings (up to slotsPerWord balances at once)\n * @dev Uses new optimized globalMintTimestamps structure with slotsPerWord elements per bucket\n * @dev Returns remaining amount, total burned amount, and bitmap mask for efficient updates\n */\n function _processBurnAlignedPackedHoldings(\n address from,\n uint256 globalIndex,\n uint256 remainingAmount_\n ) internal returns (uint256, uint256, uint256) {\n BurnParams memory burnParams = BurnParams({\n from: from,\n globalIndex: globalIndex,\n remainingAmount: remainingAmount_,\n totalBurned: 0,\n bitmaskToClear: 0,\n bucketIndex: globalIndex / slotsPerWord,\n alignedBaseIndex: (globalIndex / slotsPerWord) * slotsPerWord,\n fromPackedBalances: packedBalancesByTypeAndTime[from][globalIndex / slotsPerWord],\n packedTimestamps: globalMintTimestamps[globalIndex / slotsPerWord]\n });\n\n // Process slotsPerWord consecutive holdings starting from bucket start\n // Align globalIndex to bucket start to process all holdings in the bucket\n for (uint256 i = 0; i < slotsPerWord && burnParams.remainingAmount > 0; i++) {\n // Only process holdings that are in the same word as the original globalIndex\n // This prevents processing holdings from buckets that span across word boundaries\n if (((globalIndex / slotsPerWord) * slotsPerWord + i) / 256 != globalIndex / 256) {\n continue;\n }\n burnParams = _processSingleBurnHolding(burnParams, i);\n }\n \n // Update storage only once if any burns occurred\n if (burnParams.totalBurned > 0) {\n packedBalancesByTypeAndTime[burnParams.from][burnParams.bucketIndex] = burnParams.fromPackedBalances;\n }\n \n return (burnParams.remainingAmount, burnParams.totalBurned, burnParams.bitmaskToClear);\n }\n\n /**\n * @dev Process a single burn holding to reduce stack depth\n */\n function _processSingleBurnHolding(\n BurnParams memory burnParams,\n uint256 i\n ) internal returns (BurnParams memory) {\n uint256 packedBalance = BitManipulationLib.getPackedBalance(burnParams.fromPackedBalances, i, elementBitSize);\n if (packedBalance == 0) return burnParams;\n \n // Get real balance and calculate burn amount\n uint256 currentGlobalIndex = burnParams.alignedBaseIndex + i;\n uint256 currentBalance = packedBalance == maxBalancePerSubIndex ? \n overflowBalances[burnParams.from][currentGlobalIndex] : packedBalance;\n \n uint256 burnAmount = burnParams.remainingAmount > currentBalance ? \n currentBalance : burnParams.remainingAmount;\n uint256 newFromBalance = currentBalance - burnAmount;\n \n // Update balance and handle overflow\n burnParams.fromPackedBalances = _updateBalance(\n burnParams.from,\n burnParams.fromPackedBalances,\n i,\n currentGlobalIndex,\n packedBalance,\n newFromBalance\n );\n \n // Update burn tracking\n burnParams.totalBurned += burnAmount;\n burnParams.remainingAmount -= burnAmount;\n \n // Update bitmask if balance becomes zero (matches transfer logic)\n // Only update bits that are in the current word to avoid cross-word issues\n if (newFromBalance == 0) {\n burnParams.bitmaskToClear |= (1 << (currentGlobalIndex % 256));\n }\n \n // Emit burn event\n uint256 element = (burnParams.packedTimestamps >> (i * elementBitSize)) & ((1 << elementBitSize) - 1);\n emit TokenTypeBurned(burnParams.from, burnAmount, BitManipulationLib.unpackTokenTypeElement(element));\n \n return burnParams;\n }\n\n /**\n * @dev Update balance for target address\n */\n function _updateBalance(\n address from,\n uint256 fromPackedBalances,\n uint256 i,\n uint256 currentGlobalIndex,\n uint256 packedBalance,\n uint256 newFromBalance\n ) internal returns (uint256) {\n if (newFromBalance == 0) {\n // Balance becomes zero, clear everything\n if (packedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[from][currentGlobalIndex];\n }\n return BitManipulationLib.setPackedBalance(fromPackedBalances, i, 0, elementBitSize);\n } else if (newFromBalance < maxBalancePerSubIndex) {\n // New balance fits in uint32, store in packed storage\n if (packedBalance == maxBalancePerSubIndex) {\n delete overflowBalances[from][currentGlobalIndex];\n }\n return BitManipulationLib.setPackedBalance(fromPackedBalances, i, newFromBalance, elementBitSize);\n } else {\n // New balance still exceeds uint32 max, update overflow mapping\n overflowBalances[from][currentGlobalIndex] = newFromBalance;\n return BitManipulationLib.setPackedBalance(fromPackedBalances, i, maxBalancePerSubIndex, elementBitSize);\n }\n }\n\n /**\n * @dev Burn from a single holding\n * @dev Minimized to avoid stack issues\n */\n function _burnSingleHolding(\n address from_,\n uint256 globalIndex_,\n uint256 amount\n ) internal {\n // Get balance from packed storage\n uint256 packedKey = BitManipulationLib.getPackedBalanceKeyFromGlobalIndex(globalIndex_, slotsPerWord);\n uint256 subIndex = globalIndex_ % slotsPerWord;\n uint256 packedBalances = packedBalancesByTypeAndTime[from_][packedKey];\n uint256 currentBalance = BitManipulationLib.getPackedBalance(packedBalances, subIndex, elementBitSize);\n if (currentBalance == maxBalancePerSubIndex) {\n uint256 overflowBalance = overflowBalances[from_][globalIndex_];\n currentBalance = overflowBalance;\n }\n if (amount > currentBalance) {\n revert RestrictedLockupToken_NoItemWithEnoughBalance();\n }\n \n // Update packed balances\n uint256 newBalance = currentBalance - amount;\n\n packedBalances = _updateBalance(\n from_,\n packedBalances,\n subIndex,\n globalIndex_,\n currentBalance,\n newBalance\n );\n\n packedBalancesByTypeAndTime[from_][packedKey] = packedBalances;\n \n // Clear bitmap bit if balance becomes zero\n if (newBalance == 0) {\n _clearBitmapBit(from_, globalIndex_);\n }\n \n uint256 tokenType_ = _getTokenTypeFromGlobalIndex(globalIndex_);\n // Emit token type burn event\n emit TokenTypeBurned(from_, amount, tokenType_);\n }\n\n /**\n * @notice Create a release schedule template that can be used to generate many token timelocks\n * @param releaseCount Total number of releases including any initial \"cliff'\n * @param delayUntilFirstReleaseInSeconds \"cliff\" or 0 for immediate release\n * @param initialReleasePortionInBips Portion to release in 100ths of 1% (10000 BIPS per 100%)\n * @param periodBetweenReleasesInSeconds After the delay and initial release\n * the remaining tokens will be distributed evenly across the remaining number of releases (releaseCount - 1)\n * @return unlockScheduleId The id used to refer to the release schedule at the time of funding the schedule\n */\n function createReleaseSchedule(\n uint256 releaseCount,\n uint256 delayUntilFirstReleaseInSeconds,\n uint256 initialReleasePortionInBips,\n uint256 periodBetweenReleasesInSeconds\n ) external anyAdmin returns (uint256 unlockScheduleId) {\n if (delayUntilFirstReleaseInSeconds > maxReleaseDelay) {\n revert RestrictedLockupToken_InvalidFirstRelease();\n }\n if (releaseCount == 0) {\n revert RestrictedLockupToken_InvalidNumberOfRelases();\n }\n if (initialReleasePortionInBips > VestingMath.BIPS_PRECISION) {\n revert RestrictedLockupToken_InvalidRInitialReleaseBIPS();\n }\n\n if (releaseCount > 1) {\n if (periodBetweenReleasesInSeconds == 0) {\n revert RestrictedLockupToken_InvalidReleasePeriod();\n }\n if (initialReleasePortionInBips == VestingMath.BIPS_PRECISION) {\n revert RestrictedLockupToken_CantVestAllForMultipleReleases();\n }\n } else {\n if (initialReleasePortionInBips != VestingMath.BIPS_PRECISION) {\n revert RestrictedLockupToken_InvalidReleasePortionForSingleRelease();\n }\n periodBetweenReleasesInSeconds = 0;\n }\n\n releaseSchedules.push(\n ReleaseSchedule(\n releaseCount,\n delayUntilFirstReleaseInSeconds,\n initialReleasePortionInBips,\n periodBetweenReleasesInSeconds\n )\n );\n\n unlockScheduleId = releaseSchedules.length - 1;\n emit ScheduleCreated(_msgSender(), unlockScheduleId);\n\n return unlockScheduleId;\n }\n\n function timelockCountOf(address wallet) internal view returns (uint256) {\n return timelocks[wallet].length;\n }\n\n /**\n * @notice Update the token type for a specific timelock\n * @dev Only callable by reserve admin. Updates token type classification for timelocked tokens\n * @param wallet The wallet address whose timelock is being updated\n * @param timelockIndex The index of the timelock to update\n * @param newTokenType The new token type to assign\n */\n function updateTimelockTokenType(\n address wallet,\n uint256 timelockIndex,\n uint256 newTokenType\n ) external onlyReserveAdmin {\n if (timelockIndex >= timelockCountOf(wallet)) {\n revert RestrictedLockupToken_InvalidTimelock();\n }\n\n Timelock storage timelock = timelocks[wallet][timelockIndex];\n (uint256 oldTokenType, uint256 mintTimestamp) = _getTokenTypeAndMintTimestampFromGlobalIndex(timelock.globalHoldingIndex);\n\n if (oldTokenType == newTokenType) {\n revert RestrictedLockupToken_InvalidTokenType();\n }\n\n // Ensure the new token type is allowed for this wallet\n if (!transferRules.tokenTypeAllowed(newTokenType, wallet, identityRegistry)) {\n revert RestrictedLockupToken_TokenTypeNotAllowedForRecipient();\n }\n\n // For timelock token type updates, we need to create a new global index\n _removeFromHoldings(wallet, 0, timelock.globalHoldingIndex);\n uint256 globalHoldingIndex = _addToHoldingsByType(\n wallet,\n newTokenType,\n BitManipulationLib.calculateDaysAfterDeployment(mintTimestamp, deploymentDay),\n 0\n );\n timelock.globalHoldingIndex = globalHoldingIndex;\n\n emit TimelockTokenTypeUpdated(\n _msgSender(),\n wallet,\n timelockIndex,\n oldTokenType,\n newTokenType,\n timelock.totalAmount\n );\n }\n\n function _fundReleaseSchedule(\n FundReleaseScheduleParams memory params,\n address[] memory cancelableBy_\n ) internal returns (bool _success) {\n if (cancelableBy_.length > MAX_CANCELABLE_BY) {\n revert RestrictedLockupToken_MaxCancelersExceeded();\n }\n\n address _funder = _msgSender();\n uint256 _timelockId = _fund(\n params.to,\n params.amount,\n params.commencementTimestamp,\n params.scheduleId,\n 0, // token type is not used for fundReleaseSchedule, it is got from the holdingIdx\n _funder,\n false,\n params.holdingIdx\n );\n\n if (cancelableBy_.length > 0) {\n timelocks[params.to][_timelockId].cancelableBy = cancelableBy_;\n }\n\n emit ScheduleFunded(\n _funder,\n params.to,\n params.scheduleId,\n params.amount,\n params.commencementTimestamp,\n _timelockId,\n cancelableBy_\n );\n return true;\n }\n\n /**\n * @notice Fund the programmatic release of tokens to a recipient with explicit parameters including holding index\n * @param params FundReleaseScheduleParams struct containing all necessary parameters\n * @param cancelableBy_ array of canceler addresses\n * @return _success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function fundReleaseScheduleWithHolding(\n FundReleaseScheduleParams memory params,\n address[] memory cancelableBy_\n ) public nonReentrant anyAdmin returns (bool _success) {\n return _fundReleaseSchedule(params, cancelableBy_);\n }\n\n /**\n * @dev Find the first holding for a sender that has enough amount and is allowed to transfer to recipient\n * @dev Optimized to iterate only through set bits using bit manipulation\n * @param sender_ The address to transfer from\n * @param recipient_ The address to transfer to\n * @param amount_ The amount needed\n * @return globalIndex The global index of the holding, or type(uint256).max if not found\n * @return holdingAmount The amount available in this holding\n * @return tokenType The token type of the holding\n */\n function findHoldingFor(\n address sender_,\n address recipient_,\n uint256 amount_\n ) public view returns (\n uint256,\n uint256,\n uint256\n ) {\n // Recipient context plus a per-token-type eligibility cache, so the transfer rules are\n // resolved once per token type for the whole scan rather than once per holding.\n RecipientEligibility memory recipient = _recipientEligibility(recipient_);\n uint256 wordCount = walletIndexesByType[sender_].length;\n\n // Iterate through all holdings for the sender\n for (uint256 wordIndex = 0; wordIndex < wordCount; wordIndex++) {\n uint256 word = walletIndexesByType[sender_][wordIndex];\n if (word == 0) continue;\n\n while (word != 0) {\n // Find the position of the lowest set bit\n uint256 currentGlobalIndex = wordIndex * 256 + word.bitScanForward256();\n \n // Get holding details\n uint256 element = BitManipulationLib.getPackedElement(\n globalMintTimestamps[currentGlobalIndex / slotsPerWord],\n currentGlobalIndex % slotsPerWord,\n elementBitSize\n );\n // Get the actual balance for this holding\n uint256 currentAmount = _getRealBalance(sender_, currentGlobalIndex);\n \n // Check if this holding has enough amount\n if (currentAmount >= amount_) {\n (uint256 currentTokenType, uint256 daysAfterDeployment) = BitManipulationLib.unpackElement(element);\n // Return the first holding the recipient may receive; see Storage._resolveEligibility\n uint256 entry = EligibilityCacheLib.entryFor(recipient.cache, currentTokenType);\n if (!EligibilityCacheLib.isHit(entry, currentTokenType)) {\n entry = _resolveEligibility(recipient, currentTokenType);\n }\n if (EligibilityCacheLib.admits(entry, daysAfterDeployment)) {\n return (currentGlobalIndex, currentAmount, currentTokenType);\n }\n }\n \n // Clear the lowest set bit for next iteration (Brian Kernighan's algorithm)\n word = word & (word - 1);\n }\n }\n return (type(uint256).max, 0, 0);\n }\n\n /**\n * @notice Fund a release schedule by finding a suitable holding from the funder and automatically determining token type.\n * This function finds the first holding with sufficient amount and uses its token type for the release schedule.\n * @param to_ recipient address that will have tokens unlocked on a release schedule\n * @param amount_ of tokens to transfer in base units (the smallest unit without the decimal point)\n * @param commencementTimestamp_ the time (in unixtime) the release schedule will start\n * @param scheduleId_ the id of the release schedule that will be used to release the tokens\n * @param cancelableBy_ array of canceler addresses\n * @return _success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function fundReleaseSchedule(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_,\n uint256 scheduleId_,\n address[] memory cancelableBy_\n ) public nonReentrant anyAdmin returns (bool _success) {\n address _funder = _msgSender();\n \n // Find a suitable holding from the funder using the existing findHoldingFor function\n (uint256 holdingIdx,,) = findHoldingFor(_funder, to_, amount_);\n \n if (holdingIdx == type(uint256).max) {\n revert RestrictedLockupToken_NoHoldingWithEnoughBalance();\n }\n\n FundReleaseScheduleParams memory params = FundReleaseScheduleParams({\n to: to_,\n amount: amount_,\n commencementTimestamp: commencementTimestamp_,\n scheduleId: scheduleId_,\n holdingIdx: holdingIdx\n });\n \n // Create FundReleaseScheduleParams with found values and call fundReleaseSchedule\n return _fundReleaseSchedule(\n params,\n cancelableBy_\n );\n }\n\n function _mintReleaseSchedule(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_,\n uint256 scheduleId_,\n uint256 tokenType_,\n address[] memory cancelableBy_\n ) internal returns (bool _success) {\n if (cancelableBy_.length > MAX_CANCELABLE_BY) {\n revert RestrictedLockupToken_MaxCancelersExceeded();\n }\n\n address _funder = _msgSender();\n uint256 _timelockId = _fund(\n to_,\n amount_,\n commencementTimestamp_,\n scheduleId_,\n tokenType_,\n _funder,\n true,\n 0 // holdingIdx is not used for mintReleaseSchedule\n );\n _registerTokenType(tokenType_);\n\n if (cancelableBy_.length > 0) {\n timelocks[to_][_timelockId].cancelableBy = cancelableBy_;\n }\n\n emit ScheduleFunded(\n _funder,\n to_,\n scheduleId_,\n amount_,\n commencementTimestamp_,\n _timelockId,\n cancelableBy_\n );\n return true;\n }\n\n /**\n * @notice Fund the programmatic release of tokens to a recipient by minting directly to them with explicit token type.\n WARNING: this function IS CANCELABLE by cancelableBy.\n If canceled the tokens that are locked at the time of the cancellation will be returned to the funder\n and unlocked tokens will be transferred to the recipient.\n * @param to_ recipient address that will have tokens unlocked on a release schedule\n * @param amount_ of tokens to transfer in base units (the smallest unit without the decimal point)\n * @param commencementTimestamp_ the time (in unixtime) the release schedule will start\n * @param scheduleId_ the id of the release schedule that will be used to release the tokens\n * @param tokenType_ the token type to assign to the minted tokens\n * @param cancelableBy_ array of canceler addresses\n * @return _success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function mintReleaseScheduleTokenType(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_,\n uint256 scheduleId_,\n uint256 tokenType_,\n address[] memory cancelableBy_\n ) public nonReentrant onlyReserveOrMintAdmin returns (bool _success) {\n return _mintReleaseSchedule(to_, amount_, commencementTimestamp_, scheduleId_, tokenType_, cancelableBy_);\n }\n\n /**\n * @notice Mint tokens directly to a recipient on a release schedule with automatic token type determination.\n * This is akin to token minting, it does not have to adhere to transfer restrictions, and is purely under the discretion of the Reserve Admin.\n * @param to_ recipient address that will have tokens unlocked on a release schedule\n * @param amount_ of tokens to transfer in base units (the smallest unit without the decimal point)\n * @param commencementTimestamp_ the time (in unixtime) the release schedule will start\n * @param scheduleId_ the id of the release schedule that will be used to release the tokens\n * @param cancelableBy_ array of canceler addresses\n * @return _success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function mintReleaseSchedule(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_,\n uint256 scheduleId_,\n address[] memory cancelableBy_\n ) public nonReentrant onlyReserveOrMintAdmin returns (bool _success) {\n // Determine token type based on recipient's identity\n uint256 tokenType_ = transferRules.determineTokenType(to_, identityRegistry);\n\n return _mintReleaseSchedule(to_, amount_, commencementTimestamp_, scheduleId_, tokenType_, cancelableBy_);\n }\n\n\n /**\n * @param to_ Address to\n * @param amount_ Amount of tokens\n * @param commencementTimestamp_ commencement timestamp\n * @param scheduleId_ schedule Id\n * @param funder_ funder address\n * @param isMintReleaseSchedule_ if this funding is via a mint release schedule\n * @return timelock Id\n */\n function _fund(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_, // unix timestamp\n uint256 scheduleId_,\n uint256 tokenType_,\n address funder_,\n bool isMintReleaseSchedule_,\n uint256 globalHoldingIdx_\n ) internal returns (uint256) {\n if (timelocks[to_].length >= MAX_TIMELOCKS) {\n revert RestrictedLockupToken_MaxTimelocksExceeded();\n }\n if (amount_ < minTimelockAmount) {\n revert RestrictedLockupToken_InvalidFundAmount();\n }\n if (to_ == address(0)) {\n revert RestrictedLockupToken_InvalidFundAddress();\n }\n if (scheduleId_ >= releaseSchedules.length) {\n revert RestrictedLockupToken_InvalidScheduleId();\n }\n if (amount_ < releaseSchedules[scheduleId_].releaseCount) {\n revert RestrictedLockupToken_AmountLessThanReleaseCount();\n }\n if (\n commencementTimestamp_ +\n releaseSchedules[scheduleId_].delayUntilFirstReleaseInSeconds >\n block.timestamp + maxReleaseDelay\n ) {\n revert RestrictedLockupToken_InitialReleaseOutOfRange();\n }\n\n uint256 mintTimestamp = recordMintTimestamp ? \n VestingMath.toMidnightTimestamp(block.timestamp) : \n BitManipulationLib.daysSinceDeployToTimestamp(recordPausedOnDaysAfterDeployment, deploymentDay);\n \n uint256 globalIndex = 0; // Initialize global index\n \n if (isMintReleaseSchedule_) {\n uint256 daysAfterDeployment = BitManipulationLib.calculateDaysAfterDeployment(mintTimestamp, deploymentDay);\n // Get global index from _addToHoldings and store it in timelock\n globalIndex = _addToHoldingsByType(to_, tokenType_, daysAfterDeployment, 0);\n // Verify if the specified token type is allowed for the recipient\n if (!transferRules.tokenTypeAllowed(tokenType_, to_, identityRegistry)) {\n revert RestrictedLockupToken_TokenTypeNotAllowedForRecipient();\n }\n // we mint new tokens so no need to find anything\n // Perform standard ERC20 mint to maintain compatibility\n _mint(to_, amount_);\n } else {\n _enforceTransferRestrictions(funder_, to_, amount_);\n (mintTimestamp, tokenType_) = _updateHolding(funder_, to_, amount_, globalHoldingIdx_);\n // For fundReleaseSchedule, we need to get the global index from the existing holding\n // This will be handled by _updateHolding which should return the global index\n globalIndex = globalHoldingIdx_; // Use the holding index as global index for now\n\n emit TokenTypeTransferred(funder_, to_, amount_, tokenType_);\n super._transfer(funder_, to_, amount_);\n }\n\n Timelock memory _timelock;\n _timelock.scheduleId = scheduleId_;\n _timelock.commencementTimestamp = commencementTimestamp_;\n _timelock.totalAmount = amount_;\n _timelock.funder = funder_;\n _timelock.globalHoldingIndex = globalIndex;\n\n timelocks[to_].push(_timelock);\n return timelockCountOf(to_) - 1;\n }\n\n /**\n * @dev Updates the holding by decreasing the amount from the 'from' wallet's holding, but does NOT update the 'to' wallet.\n * This function only removes the specified amount from the sender's (from) holding for the given tokenType and holdingIdx.\n * The recipient's (to) holdings are not updated here.\n */\n function _updateHolding(\n address from,\n address to,\n uint256 amount,\n uint256 globalHoldingIdx\n ) private returns (uint256, uint256) {\n // Validate that holdingIdx is within bounds\n if (globalHoldingIdx >= mintTimestampCount) {\n revert RestrictedLockupToken_InvalidHoldingIndex();\n }\n\n uint256 packed = globalMintTimestamps[globalHoldingIdx / slotsPerWord];\n uint256 element = BitManipulationLib.getPackedElement(packed, globalHoldingIdx % slotsPerWord, elementBitSize);\n (uint256 _tokenType, uint256 daysAfterDeployment) = BitManipulationLib.unpackElement(element);\n uint256 _mintTimestamp = BitManipulationLib.daysSinceDeployToTimestamp(daysAfterDeployment, deploymentDay);\n _enforceTransferRestrictionsFor(_tokenType, _mintTimestamp, to);\n _removeFromHoldings(\n from,\n amount,\n globalHoldingIdx\n );\n\n return (_mintTimestamp, _tokenType);\n }\n\n /**\n * @dev update hook\n * @notice During a burn, this is invoked by the _burn function (ERC20.sol) which artificially sets `to` to the 0x0 address. Tokens are NOT actually transferred to 0x0 during a burn.\n * @param from_ from address\n * @param to_ to address\n * @param amount_ amount\n */\n function _update(\n address from_,\n address to_,\n uint256 amount_\n ) internal virtual override {\n if (snapshotsEnabled) {\n snapshotPeriods.onUpdate(address(this), from_, to_, amount_);\n }\n super._update(from_, to_, amount_); // Call parent hook\n\n /// @notice when to is 0x0, it's a burn (note - 0x0 is not actually transferred any tokens during burn)\n /// do NOT create a holder in that case\n /// @notice zero-amount transfers must NOT create a holder either - see the matching\n /// guard in RestrictedLockupToken._update\n if (to_ != address(0) && amount_ > 0 && !_addressHasHolder(to_)) {\n _createHolderFromAddress(to_);\n }\n }\n\n /**\n * @notice Batch version of fund cancelable release schedule with explicit parameters including holding index\n * @param params An array of parameters for the release schedule\n * @param cancelableBy_ An array of cancelables\n * @return success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function batchFundReleaseScheduleWithHolding(\n FundReleaseScheduleParams[] memory params,\n address[] memory cancelableBy_\n ) external anyAdmin returns (bool success) {\n for (uint256 i; i < params.length; ++i) {\n _fundReleaseSchedule(\n params[i],\n cancelableBy_\n );\n }\n\n return true;\n }\n\n /**\n * @notice Batch version of fund cancelable release schedule with automatic holding and token type detection\n * @param to_ An array of recipient addresses that will have tokens unlocked on a release schedule\n * @param amounts_ An array of amounts of tokens to transfer in base units (the smallest unit without the decimal point)\n * @param commencementTimestamps_ An array of the times the release schedules will start\n * @param scheduleIds_ An array of the ids of the release schedules that will be used to release the tokens\n * @param cancelableBy_ An array of cancelables\n * @return success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function batchFundReleaseSchedule(\n address[] calldata to_,\n uint256[] calldata amounts_,\n uint256[] calldata commencementTimestamps_,\n uint256[] calldata scheduleIds_,\n address[] calldata cancelableBy_\n ) external anyAdmin returns (bool success) {\n if (\n to_.length != amounts_.length ||\n to_.length != commencementTimestamps_.length ||\n to_.length != scheduleIds_.length\n ) {\n revert RestrictedLockupToken_MismatchedArrayLength();\n }\n\n for (uint256 i; i < to_.length; ++i) {\n fundReleaseSchedule(\n to_[i],\n amounts_[i],\n commencementTimestamps_[i],\n scheduleIds_[i],\n cancelableBy_\n );\n }\n\n return true;\n }\n\n /**\n * @notice Batch version of mint cancelable release schedule with explicit token types\n * @param to An array of recipient address that will have tokens unlocked on a release schedule\n * @param amounts An array of amount of tokens to transfer in base units (the smallest unit without the decimal point)\n * @param commencementTimestamps An array of the time the release schedule will start\n * @param scheduleIds An array of the id of the release schedule that will be used to release the tokens\n * @param tokenTypes An array of token types for each timelock\n * @param cancelableBy An array of cancelables\n * @return success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function batchMintReleaseScheduleTokenType(\n address[] calldata to,\n uint256[] calldata amounts,\n uint256[] calldata commencementTimestamps,\n uint256[] calldata scheduleIds,\n uint256[] calldata tokenTypes,\n address[] calldata cancelableBy\n ) external onlyReserveOrMintAdmin returns (bool success) {\n if (\n to.length != amounts.length ||\n to.length != commencementTimestamps.length ||\n to.length != scheduleIds.length ||\n to.length != tokenTypes.length\n ) {\n revert RestrictedLockupToken_MismatchedArrayLength();\n }\n\n for (uint256 i; i < to.length; ++i) {\n _mintReleaseSchedule(\n to[i],\n amounts[i],\n commencementTimestamps[i],\n scheduleIds[i],\n tokenTypes[i],\n cancelableBy\n );\n }\n\n return true;\n }\n\n /**\n * @notice Batch version of mint cancelable release schedule with automatic token type determination\n * @param to An array of recipient address that will have tokens unlocked on a release schedule\n * @param amounts An array of amount of tokens to transfer in base units (the smallest unit without the decimal point)\n * @param commencementTimestamps An array of the time the release schedule will start\n * @param scheduleIds An array of the id of the release schedule that will be used to release the tokens\n * @param cancelableBy An array of cancelables\n * @return success Always returns true on completion so that a function calling it can revert if the required call did not succeed\n */\n function batchMintReleaseSchedule(\n address[] calldata to,\n uint256[] calldata amounts,\n uint256[] calldata commencementTimestamps,\n uint256[] calldata scheduleIds,\n address[] calldata cancelableBy\n ) external onlyReserveOrMintAdmin returns (bool success) {\n if (\n to.length != amounts.length ||\n to.length != commencementTimestamps.length ||\n to.length != scheduleIds.length\n ) {\n revert RestrictedLockupToken_MismatchedArrayLength();\n }\n\n for (uint256 i; i < to.length; ++i) {\n mintReleaseSchedule(\n to[i],\n amounts[i],\n commencementTimestamps[i],\n scheduleIds[i],\n cancelableBy\n );\n }\n\n return true;\n }\n\n /**\n * @dev safeApprove should only be called when setting an initial allowance,\n * or when resetting it to zero.\n * @param spender_ spending address to approve\n * @param value_ amount to approve\n */\n function safeApprove(address spender_, uint256 value_) external {\n if (value_ != 0 && (allowance(address(_msgSender()), spender_) != 0)) {\n revert RestrictedLockupToken_SafeApprove();\n }\n approve(spender_, value_);\n }\n\n /**\n * @notice Sets a new maximum total supply for the token.\n * @dev Only callable by the reserve admin. The new max total supply must be greater than the current total supply.\n * @param maxTotalSupply_ The new maximum total supply to set.\n */\n function setMaxTotalSupply(\n uint256 maxTotalSupply_\n ) external onlyReserveAdmin {\n if (maxTotalSupply_ > _maxSafeSupply()) {\n revert RestrictedLockupToken_MaxTotalSupplyTooLarge();\n }\n if (maxTotalSupply_ <= totalSupply()) {\n revert RestrictedLockupToken_NewMaxTotalSupplyMustExceedCurrentTotalSupply();\n }\n\n maxTotalSupply = maxTotalSupply_;\n emit SetMaxTotalSupply(_msgSender(), maxTotalSupply_);\n }\n}\n"},"contracts/RestrictedLockupTokenManagementExtension.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {IERC165} from \"@openzeppelin/contracts/utils/introspection/ERC165.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {Storage} from \"./Storage.sol\";\nimport {BitManipulationLib} from \"./libraries/BitManipulationLib.sol\";\nimport {EligibilityCacheLib} from \"./libraries/EligibilityCacheLib.sol\";\nimport {Popcount} from \"@solidity-bits/contracts/Popcount.sol\";\nimport {BitScan} from \"@solidity-bits/contracts/BitScan.sol\";\nimport {ITransferRules} from \"./interfaces/ITransferRules.sol\";\nimport {IIdentityRegistry} from \"./interfaces/IIdentityRegistry.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title RestrictedLockupTokenManagementExtension\n * @notice Extension contract for managing token holders, access control\n * @dev This contract is called via delegatecall from RestrictedLockupToken\n * Following the fallback extension pattern with identical storage layout\n */\ncontract RestrictedLockupTokenManagementExtension is\n Storage,\n Context,\n ERC2771Context,\n ReentrancyGuard\n{\n using BitManipulationLib for uint256;\n using Popcount for uint256;\n using BitScan for uint256;\n\n // ============================================\n // HOLDER MANAGEMENT MODIFIERS\n // ============================================\n \n modifier onlyExistingHolder(uint256 holderId_) {\n if (!holderExists(holderId_)) {\n revert RestrictedLockupToken_HolderDoesNotExist();\n }\n _;\n }\n\n constructor(address trustedForwarder_, uint256 maxTotalSupply_)\n ERC2771Context(trustedForwarder_)\n {\n if (trustedForwarder_ == address(0)) {\n revert RestrictedLockupToken_InvalidTrustedForwarder();\n }\n maxTotalSupply = maxTotalSupply_;\n // Initialize immutable slotsPerWord with the same value as the main contract\n slotsPerWord = BitManipulationLib.calculateSlotsPerWord(maxTotalSupply_);\n maxBalancePerSubIndex = BitManipulationLib.calculateMaxBalancePerSubIndex(slotsPerWord);\n elementBitSize = BitManipulationLib.calculateElementBitSize(slotsPerWord);\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context, Storage)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n // ============================================\n // HOLDER MANAGEMENT VIEW FUNCTIONS\n // ============================================\n \n /**\n * @dev getHolderId\n * @param addr_ address of holder\n * @return holder Id from address\n */\n function getHolderId(address addr_) public view returns (uint256) {\n return holderIdByAddress[addr_];\n }\n\n /**\n * @dev check if address has holder\n * @param addr_ address to check\n * @return true if address has holder\n */\n function addressHasHolder(address addr_) public view returns (bool) {\n return _addressHasHolder(addr_);\n }\n\n /**\n * @dev check if holder exists\n * @param holderId_ id of holder to check\n * @return true if holderId exists\n * */\n function holderExists(uint256 holderId_) public view returns (bool) {\n return walletAddressesByHolderId[holderId_].length > 0;\n }\n\n /**\n * @dev get holder addresses by Id\n * @param holderId_ holder id\n * @return array of addresses\n */\n function getHolderAddresses(\n uint256 holderId_\n ) external view onlyExistingHolder(holderId_) returns (address[] memory) {\n return walletAddressesByHolderId[holderId_];\n }\n\n // ============================================\n // HOLDER MANAGEMENT FUNCTIONS\n // ============================================\n \n /**\n * @dev create Holder from a given Address\n * @param addr_ address of holder\n * @return _holderId id of holder\n */\n function createHolderFromAddress(\n address addr_\n ) public onlyWalletsAdminOrTransferAdmin validAddress(addr_) returns (uint256 _holderId) {\n if (addressHasHolder(addr_)) {\n revert RestrictedLockupToken_HolderAlreadyExists();\n }\n _holderId = _createHolderFromAddress(addr_);\n }\n\n /**\n * @dev Append a new wallet to existing Holder\n * @param addr_ address of holder\n * @param holderId_ id of holder\n */\n function appendHolderAddress(address addr_, uint256 holderId_) public {\n _appendHolderAddress(addr_, holderId_);\n }\n\n /**\n * @dev add a Holder with multiple wallet addresses.\n * @param addresses_ array of addresses\n * @return _holderId id of holder\n */\n function addHolderWithAddresses(\n address[] calldata addresses_\n ) external onlyWalletsAdminOrTransferAdmin returns (uint256 _holderId) {\n uint256 _addrLen = addresses_.length;\n if (_addrLen == 0) {\n revert RestrictedLockupToken_InvalidAddressArray();\n }\n if (addresses_.length > MAX_WALLETS_PER_HOLDER) {\n revert RestrictedLockupToken_MaxHolderAddressesReached();\n }\n\n _holderId = createHolderFromAddress(addresses_[0]);\n // use first entry in array to create Holder id\n for (uint256 i = 1; i < _addrLen; ++i) {\n _appendHolderAddress(addresses_[i], _holderId);\n }\n }\n\n /**\n * @dev set max number of holder addresses\n * @param holderMax_ max number of holder addresses\n */\n function setHolderMax(uint256 holderMax_) external onlyTransferAdmin {\n if (holderMax_ < holderCount) {\n revert RestrictedLockupToken_NewHolderMaxMustExceedCurrentHolderCount();\n }\n holderMax = holderMax_;\n\n emit SetHolderMax(_msgSender(), holderMax_);\n }\n\n /**\n * @dev removeHolder from the system. Delete their wallet address from holder state. Timelock info remains for the wallet address.\n * @notice Holder can only be removed if it has no active timelocks\n * @param holderId_ holder id\n */\n function removeHolder(\n uint256 holderId_\n ) external onlyWalletsAdminOrTransferAdmin onlyExistingHolder(holderId_) {\n address[] storage addresses = walletAddressesByHolderId[holderId_];\n uint256 len = addresses.length;\n for (uint256 i; i < len; ++i) {\n address addr = addresses[i];\n // Check if address has active timelocks by calling main contract function\n if (_queryBalance(\"lockedBalanceOf(address)\", addr) > 0) {\n revert RestrictedLockupToken_CannotRemoveHolderWithActiveTimelocks();\n }\n delete holderIdByAddress[addr];\n }\n delete walletAddressesByHolderId[holderId_];\n holderCount--;\n\n emit RemoveHolder(_msgSender(), holderId_);\n }\n\n /**\n * @dev removeWalletFromHolder Remove wallet from a holderId association.\n * @dev use case - when a wallet is no longer owned by that holder.\n * @param addr_ address to remove\n */\n function removeWalletFromHolder(\n address addr_\n ) public onlyWalletsAdminOrTransferAdmin onlyExistingAddress(addr_) {\n uint256 _holderId = holderIdByAddress[addr_];\n address[] memory _holderWalletAddresses = walletAddressesByHolderId[\n _holderId\n ];\n uint256 _len = _holderWalletAddresses.length;\n if (_len == 1) {\n revert RestrictedLockupToken_CannotRemoveLastWallet();\n }\n\n address[] memory _newHolderWalletAddresses = new address[](_len - 1);\n uint256 _ind;\n for (uint8 i; i < _len; ++i) {\n if (_holderWalletAddresses[i] != addr_) {\n _newHolderWalletAddresses[_ind] = _holderWalletAddresses[i];\n _ind++;\n }\n }\n\n walletAddressesByHolderId[_holderId] = _newHolderWalletAddresses;\n delete holderIdByAddress[addr_];\n\n emit RemoveWalletFromHolder(_msgSender(), _holderId, addr_);\n }\n\n /**\n * @notice batch version of removeWalletFromHolder\n * @param addresses_ array of addresses to remove\n */\n function batchRemoveWalletFromHolder(\n address[] calldata addresses_\n ) external onlyWalletsAdminOrTransferAdmin {\n uint256 _len = addresses_.length;\n for (uint256 i; i < _len; ++i) {\n removeWalletFromHolder(addresses_[i]);\n }\n }\n\n /**\n * @dev Append a new wallet to existing Holder\n * @param addr_ address of holder\n * @param holderId_ id of holder\n */\n function _appendHolderAddress(\n address addr_,\n uint256 holderId_\n ) internal onlyExistingHolder(holderId_) onlyWalletsAdminOrTransferAdmin {\n if (addressHasHolder(addr_)) {\n revert RestrictedLockupToken_AddressAlreadyHasHolder();\n }\n if (walletAddressesByHolderId[holderId_].length >= MAX_WALLETS_PER_HOLDER) {\n revert RestrictedLockupToken_MaxHolderAddressesReached();\n }\n\n holderIdByAddress[addr_] = holderId_;\n walletAddressesByHolderId[holderId_].push(addr_);\n\n emit AppendHolderAddress(addr_, holderId_);\n }\n\n /**\n * @dev Allows the contract admin to pause transfers.\n * @param isPaused_ true to pause\n */\n function pause(bool isPaused_) external onlyContractAdminOrTransferAdmin {\n isPaused = isPaused_;\n emit Pause(_msgSender(), isPaused_);\n }\n\n /**\n * @dev Freezes or unfreezes an address.\n * Tokens in a frozen address cannot be transferred from until the address is unfrozen.\n * @param addr_ The address to be frozen.\n * @param status_ The frozenAddress status of the address. True means frozen false means not frozen.\n */\n function freeze(\n address addr_,\n bool status_\n ) public validAddress(addr_) onlyWalletsAdminOrTransferAdmin {\n frozenAddresses[addr_] = status_;\n emit AddressFrozen(_msgSender(), addr_, status_);\n }\n\n\n /**\n * @dev Count total holdings for an address by counting set bits in bitmap\n * @param who_ the address to get the holding count for\n * @return number of total unique holdings this wallet has across all token types\n */\n function holdingCountOf(address who_) external view returns (uint256) {\n uint256 count = 0;\n \n for (uint256 i = 0; i < walletIndexesByType[who_].length; i++) {\n count += walletIndexesByType[who_][i].popcount256B();\n }\n \n return count;\n }\n\n /**\n * @notice Get the struct details for an address's specific holding (optimized direct bitmap iteration)\n * @param who_ Address to check\n * @param index_ The index of the holding (position among wallet's holdings, not global index)\n * @return Struct with the attributes of the holding (balance, mintTimestamp, tokenType)\n */\n function holdingOf(\n address who_,\n uint256 index_\n ) public view returns (uint256, uint256, uint256) {\n // Find the global index for this wallet's targetIndex_-th holding\n uint256 globalIndex = _findGlobalIndexForWalletHolding(who_, index_);\n \n // If invalid index was provided, return zeros\n if (globalIndex == type(uint256).max) {\n return (0, 0, 0);\n }\n \n // Get element from global registry\n uint256 element = BitManipulationLib.getPackedElement(\n globalMintTimestamps[globalIndex / slotsPerWord],\n globalIndex % slotsPerWord,\n elementBitSize\n );\n (uint256 tokenType, uint256 daysAfterDeployment) = BitManipulationLib.unpackElement(element);\n \n // Get real balance (considering overflow)\n uint256 balance = _getRealBalance(who_, globalIndex);\n \n return (\n balance,\n BitManipulationLib.daysSinceDeployToTimestamp(daysAfterDeployment, deploymentDay),\n tokenType\n );\n }\n\n /**\n * @dev Find the global index for a wallet's Nth holding\n * @param who_ The wallet address\n * @param targetIndex_ The position among wallet's holdings (0-indexed)\n * @return globalIndex The global index, or type(uint256).max if invalid\n */\n function _findGlobalIndexForWalletHolding(\n address who_,\n uint256 targetIndex_\n ) internal view returns (uint256) {\n uint256 foundCount = 0;\n \n // Iterate through bitmap words to find the targetIndex_-th set bit\n for (uint256 wordIndex = 0; wordIndex < walletIndexesByType[who_].length; wordIndex++) {\n uint256 word = walletIndexesByType[who_][wordIndex];\n if (word == 0) continue;\n \n // Count set bits in this word\n uint256 bitsInWord = word.popcount256B();\n \n if (foundCount + bitsInWord > targetIndex_) {\n // The target bit is in this word\n uint256 targetBitInWord = targetIndex_ - foundCount;\n \n // Find the targetBitInWord-th set bit in this word\n for (uint256 bitIndex = 0; bitIndex < 256; bitIndex++) {\n if ((word >> bitIndex) & 1 == 1) {\n if (targetBitInWord == 0) {\n // Found the target bit\n return wordIndex * 256 + bitIndex;\n }\n targetBitInWord--;\n }\n }\n }\n \n foundCount += bitsInWord;\n }\n \n // Index out of bounds\n return type(uint256).max;\n }\n\n /**\n * @notice Sums the holdings of `from` that are transferable to `to`, stopping as soon as\n * `expectedTransferableAmount` is covered.\n * @dev Read-only counterpart of RestrictedLockupToken._processTransfers, and deliberately\n * walks the bitmap the same way: word by word, then bucket-aligned within each word.\n * This keeps the preflight answer consistent with what the transfer will actually do.\n *\n * Replaces the previous holdingCountOf/holdingOf enumeration in TransferRules, which\n * re-scanned the wallet bitmap from word 0 on every iteration to locate the N-th set\n * bit, making the preflight O(holdings * words). This is O(words + holdings).\n *\n * Recipient eligibility is resolved once per token type via Storage._resolveEligibility\n * (ITransferRules.eligibilityThresholdFor behind a small memory cache), instead of one\n * external call per holding.\n * @param from The address whose holdings are being counted\n * @param to The prospective recipient\n * @param expectedTransferableAmount Amount needed; 0 means \"sum everything transferable\"\n * @return transferableAmount Total transferable balance found\n */\n function previewTransferableFromHoldings(\n address from,\n address to,\n uint256 expectedTransferableAmount\n ) external view returns (uint256 transferableAmount) {\n PreviewWalk memory walk = PreviewWalk({\n from: from,\n recipient: _recipientEligibility(to),\n expectedTransferableAmount: expectedTransferableAmount,\n transferableAmount: 0,\n slotsPerWord: slotsPerWord,\n elementBitSize: elementBitSize\n });\n uint256 wordCount = walletIndexesByType[from].length;\n uint256 mintTimestampCount_ = mintTimestampCount;\n\n for (uint256 wordIndex = 0; wordIndex < wordCount; wordIndex++) {\n uint256 word = walletIndexesByType[from][wordIndex];\n\n while (word != 0) {\n uint256 globalIndex = wordIndex * 256 + word.bitScanForward256();\n\n if (globalIndex >= mintTimestampCount_) {\n // Out of range, drop just this bit\n word &= word - 1;\n continue;\n }\n\n // Early termination once the caller has what it asked for\n if (_previewBucket(walk, wordIndex, globalIndex)) {\n return walk.transferableAmount;\n }\n\n word = BitManipulationLib.clearBucketBits(word, globalIndex, wordIndex, walk.slotsPerWord);\n }\n }\n return walk.transferableAmount;\n }\n\n /**\n * @dev Cursor for previewTransferableFromHoldings. Bundled into one memory struct so the\n * walk stays within the EVM stack limit under every compiler mode; the constant fields\n * are also cheaper to read from memory than from storage on every holding.\n */\n struct PreviewWalk {\n address from;\n RecipientEligibility recipient;\n uint256 expectedTransferableAmount;\n uint256 transferableAmount;\n uint256 slotsPerWord;\n uint256 elementBitSize;\n }\n\n /**\n * @dev Adds the transferable balances of the bucket containing `globalIndex` - the part of\n * it that lies inside bitmap word `wordIndex` - to `walk.transferableAmount`. Mirrors\n * RestrictedLockupToken._processTransferAlignedPackedHoldings.\n * @return done True once `walk.expectedTransferableAmount` (when non-zero) is covered\n */\n function _previewBucket(\n PreviewWalk memory walk,\n uint256 wordIndex,\n uint256 globalIndex\n ) private view returns (bool done) {\n uint256 slotsPerWord_ = walk.slotsPerWord;\n uint256 elementBitSize_ = walk.elementBitSize;\n RecipientEligibility memory recipient = walk.recipient;\n uint256 bucketIndex = globalIndex / slotsPerWord_;\n uint256 packedTimestamps = globalMintTimestamps[bucketIndex];\n uint256 packedBalances = packedBalancesByTypeAndTime[walk.from][bucketIndex];\n uint256 found = walk.transferableAmount;\n\n for (uint256 i = 0; i < slotsPerWord_; i++) {\n uint256 currentGlobalIndex = bucketIndex * slotsPerWord_ + i;\n // Buckets can straddle bitmap words; only handle the part in this word\n if (currentGlobalIndex / 256 != wordIndex) continue;\n\n uint256 packedBalance = BitManipulationLib.getPackedBalance(packedBalances, i, elementBitSize_);\n if (packedBalance == 0) continue;\n\n (uint256 tokenType, uint256 daysAfterDeployment) = BitManipulationLib.unpackElement(\n (packedTimestamps >> (i * elementBitSize_)) & ((1 << elementBitSize_) - 1)\n );\n\n // Never eligible for this recipient, or holding period not met yet;\n // see Storage._resolveEligibility for the idiom\n uint256 entry = EligibilityCacheLib.entryFor(recipient.cache, tokenType);\n if (!EligibilityCacheLib.isHit(entry, tokenType)) {\n entry = _resolveEligibility(recipient, tokenType);\n }\n if (!EligibilityCacheLib.admits(entry, daysAfterDeployment)) continue;\n\n found += packedBalance == maxBalancePerSubIndex\n ? overflowBalances[walk.from][currentGlobalIndex]\n : packedBalance;\n\n if (walk.expectedTransferableAmount > 0 && found >= walk.expectedTransferableAmount) {\n done = true;\n break;\n }\n }\n walk.transferableAmount = found;\n }\n\n /**\n * @dev Allows the contract admin to upgrade the transfer rules. onlyContractAdmin access.\n * The upgraded transfer rules must implement the ITransferRules interface which conforms to the ERC-1404 token standard.\n * @param newTransferRules The address of the deployed TransferRules contract.\n */\n function upgradeTransferRules(\n ITransferRules newTransferRules\n ) external onlyContractAdmin {\n address _newTransferRulesAddress = address(newTransferRules);\n if (_newTransferRulesAddress == address(0)) {\n revert RestrictedLockupToken_InvalidTransferRules();\n }\n if (\n !IERC165(_newTransferRulesAddress).supportsInterface(\n _ITRANSFER_RULES_INTERFACE_ID\n )\n ) {\n revert RestrictedLockupToken_NewTransferRulesContractDoesNotImplementITransferRules();\n }\n\n address oldRules = address(transferRules);\n\n transferRules = newTransferRules;\n emit Upgrade(_msgSender(), oldRules, _newTransferRulesAddress);\n }\n\n /**\n * @dev Allows the contract admin to upgrade the identity registry. onlyContractAdmin access.\n * The upgraded identity registry must implement the IIdentityRegistry interface.\n * @param newIdentityRegistry The address of the deployed IdentityRegistry contract.\n */\n function upgradeIdentityRegistry(\n IIdentityRegistry newIdentityRegistry\n ) external onlyContractAdmin {\n address _newIdentityRegistryAddress = address(newIdentityRegistry);\n if (_newIdentityRegistryAddress == address(0)) {\n revert RestrictedLockupToken_InvalidIdentityRegistry();\n }\n if (\n !IERC165(_newIdentityRegistryAddress).supportsInterface(\n _IIDENTITY_REGISTRY_INTERFACE_ID\n )\n ) {\n revert RestrictedLockupToken_NewIdentityRegistryContractDoesNotImplementIIdentityRegistry();\n }\n\n address oldIdentityRegistry = address(identityRegistry);\n\n identityRegistry = newIdentityRegistry;\n emit Upgrade(_msgSender(), oldIdentityRegistry, _newIdentityRegistryAddress);\n }\n\n /**\n * @dev Checks the status of an address to see if its frozen\n * @param addr_ The address to check\n * @return status Returns true if the address is frozen and false if its not frozen.\n */\n function getFrozenStatus(\n address addr_\n ) external view returns (bool status) {\n return frozenAddresses[addr_];\n }\n\n /**\n * @dev Get status of address\n * @param transferRulesAddr_ The address of the new transfer rules contract\n * @return _isValidTransferRules True if the transfer rules contract is valid\n */\n function isValidTransferRules(\n address transferRulesAddr_\n ) external view virtual returns (bool) {\n return\n IERC165(transferRulesAddr_).supportsInterface(\n _ITRANSFER_RULES_INTERFACE_ID\n );\n }\n\n /**\n * @dev Get status of identity registry address\n * @param identityRegistryAddr_ The address of the new identity registry contract\n * @return _isValidIdentityRegistry True if the identity registry contract is valid\n */\n function isValidIdentityRegistry(\n address identityRegistryAddr_\n ) external view virtual returns (bool) {\n return\n IERC165(identityRegistryAddr_).supportsInterface(\n _IIDENTITY_REGISTRY_INTERFACE_ID\n );\n }\n\n /**\n * @dev Get the count of existing token types\n * @return Number of token types that have been used\n */\n function existingTokenTypesCount() external view returns (uint256) {\n return _existingTokenTypes.length;\n }\n\n function existingTokenTypes(uint256 index) external view returns (uint256) {\n return _existingTokenTypes[index];\n }\n\n /**\n * @dev Check if a token type exists in the system\n * @param tokenType The token type to check\n * @return True if the token type has been used\n */\n function tokenTypeExists(uint256 tokenType) external view returns (bool) {\n return _tokenTypeExists[tokenType];\n }\n}\n"},"contracts/RestrictedLockupTokenStandardsExtension.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {ECDSA} from \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\nimport {MessageHashUtils} from \"@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol\";\nimport {Storage} from \"./Storage.sol\";\nimport {BitManipulationLib} from \"./libraries/BitManipulationLib.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title RestrictedLockupTokenStandardsExtension\n * @notice Extension hosting standards-based surface (currently EIP-2612 permit) called\n * via delegatecall from RestrictedLockupToken. Future EIP additions\n * (e.g. EIP-3009, EIP-5267, EIP-1271) live here too so the token doesn't need\n * a separate extension contract per standard.\n * @dev Mirrors the inheritance layout of RestrictedLockupTokenExtension so storage slots\n * align under delegatecall. The EIP-712 domain separator is computed inline using\n * `name()` (which reads ERC20's `_name` from the token's storage under delegatecall)\n * and `address(this)` (the token), so no EIP-712 immutables need to match between\n * the token and this extension.\n */\ncontract RestrictedLockupTokenStandardsExtension is\n Storage,\n ERC20,\n ERC2771Context,\n ReentrancyGuard\n{\n bytes32 private constant PERMIT_TYPEHASH =\n keccak256(\n \"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\"\n );\n bytes32 private constant EIP712_DOMAIN_TYPEHASH =\n keccak256(\n \"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"\n );\n bytes32 private constant EIP712_VERSION_HASH = keccak256(bytes(\"1\"));\n\n error ERC2612ExpiredSignature(uint256 deadline);\n error ERC2612InvalidSigner(address signer, address owner);\n\n constructor(address trustedForwarder_, uint256 maxTotalSupply_)\n ERC20(\"RLT Standards Extension\", \"RLTS\")\n ERC2771Context(trustedForwarder_)\n {\n if (trustedForwarder_ == address(0)) {\n revert RestrictedLockupToken_InvalidTrustedForwarder();\n }\n maxTotalSupply = maxTotalSupply_;\n slotsPerWord = BitManipulationLib.calculateSlotsPerWord(maxTotalSupply_);\n maxBalancePerSubIndex = BitManipulationLib.calculateMaxBalancePerSubIndex(\n slotsPerWord\n );\n elementBitSize = BitManipulationLib.calculateElementBitSize(slotsPerWord);\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context, Storage)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n /**\n * @dev EIP-2612 permit. Sets `value` allowance from `owner_` to `spender` using an\n * off-chain signature. Reverts on expired deadline or signer mismatch. The\n * caller may be any account (e.g. a relayer) — the owner is recovered from\n * the signature, not from msg.sender.\n */\n function permit(\n address owner_,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external {\n if (block.timestamp > deadline) {\n revert ERC2612ExpiredSignature(deadline);\n }\n uint256 currentNonce = _permitNonces[owner_];\n _permitNonces[owner_] = currentNonce + 1;\n\n bytes32 structHash = keccak256(\n abi.encode(\n PERMIT_TYPEHASH,\n owner_,\n spender,\n value,\n currentNonce,\n deadline\n )\n );\n bytes32 digest = MessageHashUtils.toTypedDataHash(\n _domainSeparator(),\n structHash\n );\n address signer = ECDSA.recover(digest, v, r, s);\n if (signer != owner_) {\n revert ERC2612InvalidSigner(signer, owner_);\n }\n _approve(owner_, spender, value);\n }\n\n function nonces(address owner_) external view returns (uint256) {\n return _permitNonces[owner_];\n }\n\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return _domainSeparator();\n }\n\n function _domainSeparator() private view returns (bytes32) {\n return\n keccak256(\n abi.encode(\n EIP712_DOMAIN_TYPEHASH,\n keccak256(bytes(name())),\n EIP712_VERSION_HASH,\n block.chainid,\n address(this)\n )\n );\n }\n}\n"},"contracts/RestrictedSwap.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {IERC165, ERC165} from \"@openzeppelin/contracts/utils/introspection/ERC165.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IERC20Permit} from \"@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {IRestrictedSwap} from \"./interfaces/IRestrictedSwap.sol\";\nimport {RestrictedLockupToken} from \"./RestrictedLockupToken.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport {IERC1404} from \"./interfaces/IERC1404.sol\";\nimport {Pausable} from \"@openzeppelin/contracts/utils/Pausable.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title RestrictedSwap\n * @notice Escrow-free swaps of a RestrictedLockupToken against an ERC-20 quote token, settled\n * atomically from the counterparties' own allowances.\n * @dev Quote-token guarantee — RECIPIENT LEG ONLY. Every settlement path measures the quote\n * RECIPIENT's balance delta around the transfer and reverts\n * `RestrictedSwap_InconsistentQuoteTokenAmount` unless it equals the quoted amount exactly,\n * so a seller (or a buyer taking delivery of quote) is always paid to the wei. The PAYER's\n * debit is deliberately NOT measured: a quote token that surcharges the sender on top of the\n * transferred value (debit `amount + fee`, credit `amount`) settles successfully, and the\n * payer's true cost is whatever that token does.\n *\n * This asymmetry is intentional, not an oversight. Nothing in this contract depends on the\n * payer's leg: no funds are custodied, and `remainingQuoteTokenAmount`, `_requiredAllowance`\n * and `_pendingSells` are all denominated in the quoted amount, which is exactly what the\n * recipient receives. Guarding the payer's side would buy no accounting safety and would make\n * such tokens untradeable outright.\n *\n * INTEGRATOR CONSTRAINT: `quoteTokenAmount` is what the recipient receives, NOT necessarily\n * what the payer is debited. Do not present it to a payer as their cost without checking the\n * quote token's transfer semantics first — for a surcharging token the real price is higher\n * than the ratio this contract advertises. Tokens that carve the fee OUT of the transferred\n * value (the common shape: USDT-style `basisPointsRate`, reflection tokens) are rejected by\n * the recipient-side check and cannot settle here at all, as are rebasing tokens.\n */\ncontract RestrictedSwap is\n IRestrictedSwap,\n ReentrancyGuard,\n ERC165,\n Pausable,\n ERC2771Context\n{\n RestrictedLockupToken public immutable restrictedLockupToken;\n IAccessControl public accessControl;\n\n using SafeERC20 for IERC20;\n\n /// @dev Compile-time hard ceiling on `maxSwapLifetime`. The constructor refuses any value\n /// above this constant. One calendar year is enough headroom for every legitimate\n /// listing while preventing perpetual escrow obligations.\n uint256 public constant MAX_SWAP_LIFETIME_LIMIT = 365 days;\n\n /// @dev Compile-time floor on `maxSwapLifetime`. The constructor refuses any value below\n /// this constant. Prevents deploying with a window so short that configuring any swap\n /// is impractical (e.g. a 1-second cap that no real configure tx could meet).\n uint256 public constant MIN_SWAP_LIFETIME_LIMIT = 1 hours;\n\n string public constant contractVersion = \"1.1.0\";\n\n /// @dev Cap on how far in the future a swap's `deadline` may sit at configure time.\n /// Enforced as `deadline - block.timestamp <= maxSwapLifetime`. Fixed at deployment\n /// within `[MIN_SWAP_LIFETIME_LIMIT, MAX_SWAP_LIFETIME_LIMIT]` and cannot be changed\n /// afterwards. The cap is checked only at configure time.\n uint256 public immutable maxSwapLifetime;\n\n /// @dev swap number\n uint256 public _swapNumber = 0;\n bytes4 public immutable INTERFACE_ID;\n\n /// @dev swap number => swap\n mapping(uint256 => Swap) private _swap;\n\n /// @dev userAddress => tokenAddress => the required allowance of tokens to swapContract by active swaps\n mapping(address => mapping(address => uint256)) private _requiredAllowance; // specific to configurer. Is a requirement!\n\n /// @dev userAddress => restricted tokens this address is obligated to deliver across active\n /// CLOSED buy orders (orders where the seller was named at `configureBuy` time). Open\n /// buy orders (`restrictedTokenSender == address(0)`) are NOT tracked here: the seller is\n /// unknown until `takeOpenBuy` and the transfer settles atomically in the same call, so\n /// there is no observable pending window. Integrators that need total delivery\n /// obligations must also index `OpenSwapFilled` events.\n mapping(address => uint256) private _pendingBuys;\n\n /// @dev userAddress => restricted tokens this address is obligated to deliver across active\n /// sell orders (both closed and OPEN sells: the seller is the configurer and is always\n /// known at `configureSell` time, so open sell orders are tracked here too).\n mapping(address => uint256) private _pendingSells;\n\n error RestrictedSwap_AlreadyCanceled();\n error RestrictedSwap_AlreadyCompleted();\n error RestrictedSwap_InvalidRestrictedTokenAmount();\n error RestrictedSwap_InvalidQuoteTokenAmount();\n error RestrictedSwap_QuoteTokenMustNotSupportIERC1404();\n error RestrictedSwap_InvalidQuoteTokenSender();\n error RestrictedSwap_InvalidQuoteToken();\n error RestrictedSwap_InvalidRestrictedTokenSender();\n error RestrictedSwap_InsufficientQuoteTokenAmount();\n error RestrictedSwap_InsufficientRestrictedTokenAmount();\n error RestrictedSwap_InvalidTokenSender();\n error RestrictedSwap_InvalidSwapStatus();\n error RestrictedSwap_InsufficientRestrictedTokenAllowance();\n error RestrictedSwap_InsufficientQuoteTokenAllowance();\n error RestrictedSwap_InconsistentQuoteTokenAmount();\n error RestrictedSwap_InconsistentRestrictedTokenAmount();\n error RestrictedSwap_SwapNotConfigured();\n error RestrictedSwap_InvalidCanceler();\n error RestrictedSwap_InvalidSwapRecord();\n error RestrictedSwap_InvalidAccessControl();\n error RestrictedSwap_InvalidRestrictedLockupToken();\n error RestrictedSwap_InvalidTrustedForwarder();\n error RestrictedSwap_SwapExpired();\n error RestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermit();\n error RestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermit();\n error RestrictedSwap_NotOpenOrder();\n error RestrictedSwap_InvalidFillAmount();\n error RestrictedSwap_OpenOrderRequiresTake();\n error RestrictedSwap_SelfFillNotAllowed();\n error RestrictedSwap_InvalidMaxSwapLifetime();\n error RestrictedSwap_DeadlineRequired();\n error RestrictedSwap_DeadlineExceedsMaxLifetime();\n error RestrictedSwap_InvalidMinimumFillAmount();\n error RestrictedSwap_FillBelowMinimum();\n error RestrictedSwap_InvalidOrderOwner();\n error RestrictedSwap_AmountNotDivisible();\n error RestrictedSwap_InvalidResizeAmount();\n error RestrictedSwap_ExceedsParentRemaining();\n\n modifier onlyValidSwap(uint256 swapNumber_) {\n Swap memory swap = _swap[swapNumber_];\n _onlyActiveSwap(swap.status);\n // Check if swap has expired (deadline > 0 and current time > deadline)\n _onlyValidDeadline(swap.deadline);\n _;\n }\n\n modifier onlyActiveSwap(uint256 swapNumber_) {\n Swap memory swap = _swap[swapNumber_];\n _onlyActiveSwap(swap.status);\n _;\n }\n function _onlyActiveSwap(SwapStatus status_) internal view {\n if (status_ == SwapStatus.Canceled) {\n revert RestrictedSwap_AlreadyCanceled();\n }\n if (status_ == SwapStatus.Complete) {\n revert RestrictedSwap_AlreadyCompleted();\n }\n }\n\n modifier onlyValidDeadline(uint256 deadline_) {\n _onlyValidDeadline(deadline_);\n _;\n }\n function _onlyValidDeadline(uint256 deadline_) internal view {\n if (deadline_ > 0 && block.timestamp > deadline_) {\n revert RestrictedSwap_SwapExpired();\n }\n }\n\n /// @dev Configure-only deadline validation. Adds two checks on top of `_onlyValidDeadline`:\n /// `deadline > 0` (forbids the legacy \"no deadline\" sentinel) and\n /// `deadline - block.timestamp <= maxSwapLifetime` (deployment-time cap).\n /// Not used on the complete/take paths.\n function _validateConfigureDeadline(uint256 deadline_) internal view {\n if (deadline_ == 0) {\n revert RestrictedSwap_DeadlineRequired();\n }\n // Mirror the existing `_onlyValidDeadline` convention: only strictly past deadlines revert\n // (a deadline equal to `block.timestamp` is treated as \"right at the edge but still valid\",\n // same as in `cancelSwap`'s `isExpired` check).\n if (block.timestamp > deadline_) {\n revert RestrictedSwap_SwapExpired();\n }\n unchecked {\n // safe: `block.timestamp <= deadline_` checked just above.\n if (deadline_ - block.timestamp > maxSwapLifetime) {\n revert RestrictedSwap_DeadlineExceedsMaxLifetime();\n }\n }\n }\n\n constructor(\n address restrictedLockupTokenAddress_,\n address trustedForwarder_,\n address accessControl_,\n uint256 maxSwapLifetime_\n )\n ReentrancyGuard()\n ERC2771Context(trustedForwarder_)\n {\n if (accessControl_ == address(0)) {\n revert RestrictedSwap_InvalidAccessControl();\n }\n if (restrictedLockupTokenAddress_ == address(0)) {\n revert RestrictedSwap_InvalidRestrictedLockupToken();\n }\n if (trustedForwarder_ == address(0)) {\n revert RestrictedSwap_InvalidTrustedForwarder();\n }\n if (\n maxSwapLifetime_ < MIN_SWAP_LIFETIME_LIMIT ||\n maxSwapLifetime_ > MAX_SWAP_LIFETIME_LIMIT\n ) {\n revert RestrictedSwap_InvalidMaxSwapLifetime();\n }\n restrictedLockupToken = RestrictedLockupToken(\n restrictedLockupTokenAddress_\n );\n INTERFACE_ID = type(IRestrictedSwap).interfaceId;\n\n accessControl = IAccessControl(accessControl_);\n maxSwapLifetime = maxSwapLifetime_;\n }\n\n function _msgSender()\n internal\n view\n override(Context, ERC2771Context)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(Context, ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(Context, ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n\n /**\n * Support of ERC165\n * @dev See https://eips.ethereum.org/EIPS/eip-165\n * @param interfaceId The interface identifier, as specified in ERC-165\n */\n function supportsInterface(\n bytes4 interfaceId\n ) public view override(ERC165, IERC165) returns (bool) {\n return\n interfaceId == INTERFACE_ID || super.supportsInterface(interfaceId);\n }\n\n /**\n * @dev pause contract. Only contract admin.\n */\n function pause(bool isPaused_) external onlyContractAdminOrTransferAdmin {\n if (isPaused_) {\n _pause();\n } else {\n _unpause();\n }\n }\n\n modifier onlyContractAdminOrTransferAdmin() {\n _onlyContractAdminOrTransferAdmin();\n _;\n }\n\n function _onlyContractAdminOrTransferAdmin() internal view {\n if (\n !accessControl.hasRole(_msgSender(), accessControl.CONTRACT_ADMIN_ROLE()) &&\n !accessControl.hasRole(_msgSender(), accessControl.TRANSFER_ADMIN_ROLE())\n ) {\n revert EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(_msgSender());\n }\n }\n \n /**\n * @dev Get the swap number counter value\n */\n function swapNumber() external view returns (uint256) {\n return _swapNumber;\n }\n\n /**\n * @dev Get the allowance required for active swaps for the wallet address and token address.\n Can be used by UI client to calculate the required allowance before creating the swap\n * @param addr Wallet address\n * @param token ERC-20 token address\n */\n function requiredAllowance(\n address addr,\n address token\n ) external view returns (uint256) {\n return _requiredAllowance[addr][token];\n }\n\n\n /**\n * @dev Pending restricted-token delivery obligations for `addr` from CLOSED buy orders only.\n * Open buy orders (`configureBuy` called with `restrictedTokenSender == address(0)`) are\n * filled via `takeOpenBuy` with atomic settlement and do NOT contribute to this value —\n * the seller is unknown at configure time and the transfer happens in the same call as\n * the take, so no pending window ever exists for a read to observe. Off-chain accounting\n * that needs full delivery-obligation coverage must also index `OpenSwapFilled` events.\n * @param addr Seller address (the named `restrictedTokenSender` on a `configureBuy` call).\n * @return Pending restricted-token amount owed by `addr` across active closed buy orders.\n */\n function pendingBuys(address addr) external view override returns (uint256) {\n return _pendingBuys[addr];\n }\n\n /**\n * @dev Pending restricted-token delivery obligations for `addr` across active sell orders\n * (both closed and open sells). Unlike `pendingBuys`, the seller is always known at\n * `configureSell` time — they are the configurer — so this value covers open sell\n * orders too. Decremented on completion, cancellation, and each `takeOpenSell` fill.\n * @param addr Seller address.\n * @return Pending restricted-token amount that `addr` is obligated to deliver.\n */\n function pendingSells(address addr) external view override returns (uint256) {\n return _pendingSells[addr];\n }\n\n /**\n * @dev Restricted-token amount on this swap that was never filled. Only OPEN orders decrement\n * it (partial fills, `decreaseOrder` / `increaseOrder`); a closed swap reports its\n * configure-time amount for its whole lifetime.\n * Terminal states: a `Complete` swap always reports zero. A `Canceled` swap retains the\n * amount withdrawn unfilled — kept as history — which is zero only when the order was\n * retired by shrinking to zero via `decreaseOrder` or a parent-offer deduction, where\n * `_shrinkOrder` had already consumed the remainder.\n * @param swapNumber_ swap number\n */\n function remainingRestrictedTokenAmount(\n uint256 swapNumber_\n ) external view returns (uint256) {\n Swap storage swap = _swap[swapNumber_];\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_InvalidSwapRecord();\n }\n return swap.remainingRestrictedTokenAmount;\n }\n\n /**\n * @dev Minimum restricted-token amount a taker must fill on this order via `takeOpenSell` /\n * `takeOpenBuy`, unless taking the entire remaining amount. 0 means no minimum.\n * @param swapNumber_ swap number\n */\n function minimumFillAmount(\n uint256 swapNumber_\n ) external view returns (uint256) {\n Swap storage swap = _swap[swapNumber_];\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_InvalidSwapRecord();\n }\n return swap.minimumFillAmount;\n }\n\n /**\n * @dev True if the swap is an open order (only one of restrictedTokenSender / quoteTokenSender\n * is set and it requires takeOpenSell / takeOpenBuy to fill).\n * @param swapNumber_ swap number\n */\n function isOpenOrder(uint256 swapNumber_) external view returns (bool) {\n Swap storage swap = _swap[swapNumber_];\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_InvalidSwapRecord();\n }\n return\n swap.restrictedTokenSender == address(0) ||\n swap.quoteTokenSender == address(0);\n }\n\n /**\n * @dev Calculate required allowance for counterparty across active swaps (paginated)\n * @param addr Counterparty address\n * @param token Token address\n * @param pageNumber Page number (0-based) to calculate allowance for\n * @param pageSize Number of swaps per page (recommended: 100-1000 for gas efficiency)\n * @return total Required allowance amount for the specified page of swaps\n */\n function calculateCounterpartyAllowance(\n address addr,\n address token,\n uint256 pageNumber,\n uint256 pageSize\n ) external view returns (uint256 total) {\n // Calculate pagination bounds\n if (pageSize == 0) {\n return 0; // Invalid page size\n }\n \n uint256 startSwap = pageNumber * pageSize + 1;\n uint256 endSwap = startSwap + pageSize - 1;\n \n // Ensure we don't exceed the total number of swaps\n if (endSwap > _swapNumber) {\n endSwap = _swapNumber;\n }\n \n // If startSwap is beyond total swaps, return 0\n if (startSwap > _swapNumber) {\n return 0;\n }\n \n // Iterate through the specified page range\n for (uint256 i = startSwap; i <= endSwap; i++) {\n Swap storage swap = _swap[i];\n if (swap.status == SwapStatus.SellConfigured) {\n // Check if addr is counterparty for quote token in sell swaps\n if (swap.quoteTokenSender == addr && swap.quoteToken == token) {\n total += swap.remainingQuoteTokenAmount;\n }\n } else if (swap.status == SwapStatus.BuyConfigured) {\n // Check if addr is counterparty for restricted token in buy swaps\n if (swap.restrictedTokenSender == addr && token == address(restrictedLockupToken)) {\n total += swap.remainingRestrictedTokenAmount;\n }\n }\n }\n }\n\n /**\n * @dev Configure sell and emit an event with new swap number\n * Restricted Tokens should be approved to this contract before calling this function\n * @param restrictedTokenAmount the required amount for the erc1404Sender to send\n * @param quoteToken the address of an erc1404 or erc20 that will be swapped\n * @param quoteTokenSender the address that is approved to fund quoteToken\n * @param quoteTokenAmount the required amount of quoteToken to swap\n * @param deadline absolute UNIX timestamp at which the swap expires. Must be > 0,\n * strictly in the future, and within `maxSwapLifetime` seconds of `block.timestamp`.\n * @param minimumFillAmount minimum restricted-token amount a taker must fill via\n * `takeOpenSell` (unless taking the entire remaining amount). Must be <=\n * `restrictedTokenAmount`. 0 means no minimum. Must be 0 for closed swaps\n * (both counterparties named) — they settle in full and never consult it.\n */\n function configureSell(\n uint256 restrictedTokenAmount,\n address quoteToken,\n address quoteTokenSender,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount\n ) external override whenNotPaused {\n _validateConfigureDeadline(deadline);\n address msgSender = _msgSender();\n _validateConfigureSellInputs(msgSender, quoteToken, restrictedTokenAmount);\n _configureSellInternal(msgSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount);\n }\n\n /**\n * @dev Bring `owner_`'s allowance for `token` up to `required` using a caller-supplied\n * EIP-2612 permit, then verify the result.\n *\n * `permitValue` is the value the owner actually SIGNED. It is forwarded to `permit`\n * verbatim and never recomputed from `_requiredAllowance`: an EIP-2612 signature binds\n * the exact value, while `_requiredAllowance` drifts whenever a third party fills one\n * of the owner's other open orders (`takeOpenSell` / `takeOpenBuy`). Recomputing it\n * would invalidate every signature that was signed more than one block before it was\n * relayed, which is exactly the relayer flow these helpers exist to serve.\n *\n * `required` stays the sole authority on sufficiency: the permit only has to land the\n * allowance at or above it. Callers must therefore size `permitValue` off-chain as\n * `requiredAllowance(owner, token)` plus the operation's own delta, because `permit`\n * OVERWRITES the allowance and a delta-only value would clobber the allowance backing\n * the owner's other active orders. Adding headroom for an active order book is safe;\n * over-signing is the signer's prerogative but forfeits the exact-sizing property this\n * contract otherwise maintains, and under-signing reverts.\n *\n * Skipped entirely when the existing allowance already covers `required`, so a permit\n * signed for exactly `required` cannot trim headroom the owner set deliberately. The\n * `permit` call is wrapped in try/catch and followed by the recheck so a front-runner\n * who replays the signature to consume the nonce cannot grief the call.\n *\n * @param restrictedSide selects which insufficiency error to raise: the restricted-token\n * one when true, the quote-token one when false.\n */\n function _applyPermit(\n address token,\n address owner_,\n uint256 required,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s,\n bool restrictedSide\n ) private {\n if (IERC20(token).allowance(owner_, address(this)) >= required) {\n return;\n }\n\n try\n IERC20Permit(token).permit(\n owner_,\n address(this),\n permitValue,\n permitDeadline,\n v,\n r,\n s\n )\n {} catch {}\n\n if (IERC20(token).allowance(owner_, address(this)) < required) {\n if (restrictedSide) {\n revert RestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermit();\n }\n revert RestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermit();\n }\n }\n\n /**\n * @dev Configure sell using an EIP-2612 permit signature on the restricted token.\n * Lets the seller approve and configure the sell in a single relayer-driven tx.\n * `permitValue` is the value the seller signed; it must cover\n * `requiredAllowance(seller, restrictedLockupToken) + restrictedTokenAmount` as of\n * execution time. See `_applyPermit` for the sizing rules, the front-running defense,\n * and why the value is never recomputed from the live reservation.\n */\n function configureSellWithPermit(\n uint256 restrictedTokenAmount,\n address quoteToken,\n address quoteTokenSender,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override nonReentrant whenNotPaused {\n _validateConfigureDeadline(deadline);\n // quoteTokenSender == address(0) is allowed (creates an open sell order).\n if (quoteToken == address(0)) {\n revert RestrictedSwap_InvalidQuoteToken();\n }\n address msgSender = _msgSender();\n uint256 requiredAllowance = _requiredAllowance[msgSender][address(restrictedLockupToken)]\n + restrictedTokenAmount;\n\n _applyPermit(\n address(restrictedLockupToken),\n msgSender,\n requiredAllowance,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n true\n );\n\n _configureSellInternal(msgSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount);\n }\n\n // Note: quoteTokenSender is intentionally not validated here — address(0) is permitted\n // and creates an open sell order that any compliant buyer can fill via `takeOpenSell`.\n function _validateConfigureSellInputs(\n address msgSender,\n address quoteToken,\n uint256 restrictedTokenAmount\n ) private view {\n if (quoteToken == address(0)) {\n revert RestrictedSwap_InvalidQuoteToken();\n }\n if (\n restrictedLockupToken.allowance(msgSender, address(this)) <\n _requiredAllowance[msgSender][address(restrictedLockupToken)] +\n restrictedTokenAmount\n ) {\n revert RestrictedSwap_InsufficientRestrictedTokenAllowance();\n }\n }\n\n function _configureSellInternal(\n address msgSender,\n uint256 restrictedTokenAmount,\n address quoteToken,\n address quoteTokenSender,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount\n ) private {\n _pendingSells[msgSender] += restrictedTokenAmount;\n\n _configureSwap(\n msgSender,\n quoteTokenSender,\n quoteToken,\n restrictedTokenAmount,\n quoteTokenAmount,\n SwapStatus.SellConfigured,\n deadline,\n minimumFillAmount\n );\n }\n\n /**\n * @dev Configure buy and emit an event with new swap number\n * @param restrictedTokenAmount the required amount for the erc1404Sender to send\n * @param restrictedTokenSender restricted token sender\n * @param quoteToken the address of an erc1404 or erc20 that will be swapped\n * @param quoteTokenAmount the required amount of quoteToken to swap\n * @param deadline absolute UNIX timestamp at which the swap expires. Must be > 0,\n * strictly in the future, and within `maxSwapLifetime` seconds of `block.timestamp`.\n * @param minimumFillAmount minimum restricted-token amount a taker must fill via\n * `takeOpenBuy` (unless taking the entire remaining amount). Must be <=\n * `restrictedTokenAmount`. 0 means no minimum. Must be 0 for closed swaps\n * (both counterparties named) — they settle in full and never consult it.\n */\n function configureBuy(\n uint256 restrictedTokenAmount,\n address restrictedTokenSender,\n address quoteToken,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount\n ) external override whenNotPaused {\n _validateConfigureDeadline(deadline);\n address msgSender = _msgSender();\n _validateConfigureBuyInputs(msgSender, quoteToken, quoteTokenAmount);\n if (\n IERC20(quoteToken).allowance(msgSender, address(this)) <\n _requiredAllowance[msgSender][quoteToken] + quoteTokenAmount\n ) {\n revert RestrictedSwap_InsufficientQuoteTokenAllowance();\n }\n _configureBuyInternal(msgSender, restrictedTokenAmount, restrictedTokenSender, quoteToken, quoteTokenAmount, deadline, minimumFillAmount);\n }\n\n /**\n * @dev Configure buy using an EIP-2612 permit signature on the quote token.\n * Lets the buyer approve and configure the buy in a single relayer-driven tx.\n * Requires the quote token to implement EIP-2612.\n * `permitValue` is the value the buyer signed; it must cover\n * `requiredAllowance(buyer, quoteToken) + quoteTokenAmount` as of execution time.\n * See `_applyPermit` for the sizing rules, the front-running defense, and why the\n * value is never recomputed from the live reservation.\n */\n function configureBuyWithPermit(\n uint256 restrictedTokenAmount,\n address restrictedTokenSender,\n address quoteToken,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override nonReentrant whenNotPaused {\n _validateConfigureDeadline(deadline);\n address msgSender = _msgSender();\n _validateConfigureBuyInputs(msgSender, quoteToken, quoteTokenAmount);\n\n uint256 requiredAllowance = _requiredAllowance[msgSender][quoteToken] + quoteTokenAmount;\n\n _applyPermit(\n quoteToken,\n msgSender,\n requiredAllowance,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n false\n );\n\n _configureBuyInternal(msgSender, restrictedTokenAmount, restrictedTokenSender, quoteToken, quoteTokenAmount, deadline, minimumFillAmount);\n }\n\n // Note: restrictedTokenSender is intentionally not validated here — address(0) is permitted\n // and creates an open buy order that any compliant holder can fill via `takeOpenBuy`.\n function _validateConfigureBuyInputs(\n address msgSender,\n address quoteToken,\n uint256 quoteTokenAmount\n ) private view {\n if (quoteToken == address(0)) {\n revert RestrictedSwap_InvalidQuoteToken();\n }\n if (IERC20(quoteToken).balanceOf(msgSender) < quoteTokenAmount) {\n revert RestrictedSwap_InsufficientQuoteTokenAmount();\n }\n }\n\n function _configureBuyInternal(\n address msgSender,\n uint256 restrictedTokenAmount,\n address restrictedTokenSender,\n address quoteToken,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount\n ) private {\n // For open buy orders (restrictedTokenSender == 0) there is no named seller to track,\n // so `_pendingBuys` is only incremented when a concrete seller is pinned.\n if (restrictedTokenSender != address(0)) {\n _pendingBuys[restrictedTokenSender] += restrictedTokenAmount;\n }\n\n _configureSwap(\n restrictedTokenSender,\n msgSender,\n quoteToken,\n restrictedTokenAmount,\n quoteTokenAmount,\n SwapStatus.BuyConfigured,\n deadline,\n minimumFillAmount\n );\n }\n\n /**\n * @dev Complete swap with quote token\n * @param swapNumber_ swap number\n */\n function completeSwapWithQuoteToken(\n uint256 swapNumber_\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n Swap memory swap = _swap[swapNumber_];\n _validateQuoteTokenCompletion(swap, _msgSender());\n _completeSwapWithQuoteToken(swapNumber_, swap);\n }\n\n /**\n * @dev Complete swap with quote token using an EIP-2612 permit signature.\n * Combines `permit` + complete into one call so the buyer signs a single\n * ERC2771 forward request instead of separate approve + complete txs.\n * `permitValue` must cover `requiredAllowance(buyer, quoteToken) + quoteTokenAmount`\n * as of execution time. The buyer completing a closed sell was never reserved on the\n * quote side (SellConfigured reserves the seller's restricted side only), so this\n * settlement is additional intent on top of whatever the buyer's other active buy\n * orders already reserve. Mirrors the sizing rule of `configureBuyWithPermit`; see\n * `_applyPermit` for the sizing rules and the front-running defense.\n * @param swapNumber_ swap number\n * @param permitValue the EIP-2612 `value` the caller signed (see the sizing rule above)\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function completeSwapWithQuoteTokenPermit(\n uint256 swapNumber_,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n Swap memory swap = _swap[swapNumber_];\n address msgSender = _msgSender();\n\n _validateQuoteTokenCompletion(swap, msgSender);\n\n // The buyer completing a closed sell was never reserved in `_requiredAllowance`\n // (SellConfigured reserves the seller's restricted side only), so the allowance\n // after the permit must cover existing buy-order reservations PLUS this settlement.\n uint256 requiredAfterPermit = _requiredAllowance[msgSender][\n swap.quoteToken\n ] + swap.quoteTokenAmount;\n\n _applyPermit(\n swap.quoteToken,\n msgSender,\n requiredAfterPermit,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n false\n );\n\n _completeSwapWithQuoteToken(swapNumber_, swap);\n }\n\n function _validateQuoteTokenCompletion(\n Swap memory swap,\n address msgSender\n ) private pure {\n // Open sell orders (quoteTokenSender == 0) must be filled via takeOpenSell, not this path.\n if (swap.quoteTokenSender == address(0)) {\n revert RestrictedSwap_OpenOrderRequiresTake();\n }\n if (swap.quoteTokenSender != msgSender) {\n revert RestrictedSwap_InvalidTokenSender();\n }\n if (swap.status != SwapStatus.SellConfigured) {\n revert RestrictedSwap_InvalidSwapStatus();\n }\n }\n\n function _completeSwapWithQuoteToken(\n uint256 swapNumber_,\n Swap memory swap\n ) private {\n if (\n IERC20(swap.quoteToken).balanceOf(swap.quoteTokenSender) <\n swap.quoteTokenAmount\n ) {\n revert RestrictedSwap_InsufficientQuoteTokenAmount();\n }\n if (\n restrictedLockupToken.balanceOf(swap.restrictedTokenSender) <\n swap.restrictedTokenAmount\n ) {\n revert RestrictedSwap_InsufficientRestrictedTokenAmount();\n }\n\n uint256 code = restrictedLockupToken.detectTransferRestriction(\n swap.restrictedTokenSender,\n swap.quoteTokenSender,\n swap.restrictedTokenAmount\n );\n\n require(\n restrictedLockupToken.transferRules().checkSuccess(code),\n restrictedLockupToken.transferRules().messageForTransferRestriction(\n code\n )\n );\n\n _pendingSells[swap.restrictedTokenSender] -= swap.restrictedTokenAmount;\n\n _completeSwap(swapNumber_);\n }\n\n /**\n * @dev Complete swap with restricted token (the seller settling a bid/closed buy order).\n * `sellSwapNumber_` optionally names the seller's own OPEN sell offer the accepted\n * bid was drawn from: the offer is shrunk by the bid amount in the same transaction\n * (emitting `OrderResized`), so it stops advertising inventory this settlement just\n * consumed. Because `takeOpenSell` caps fills at `remainingRestrictedTokenAmount`,\n * an honest remaining makes over-filling the offer impossible at the contract.\n * Pass 0 for a standalone bid that does not draw from any open offer — a bid carries\n * no on-chain reference to a parent offer, so the contract cannot infer or force the\n * linkage; callers that skip it fall back to allowance-sizing for protection.\n * @param swapNumber_ swap number of the bid (closed buy order naming the caller as seller)\n * @param sellSwapNumber_ swap number of the caller's open sell offer to deduct from (0 = none)\n */\n function completeSwapWithRestrictedToken(\n uint256 swapNumber_,\n uint256 sellSwapNumber_\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n _completeSwapWithRestrictedToken(\n swapNumber_,\n sellSwapNumber_,\n _swap[swapNumber_],\n _msgSender()\n );\n }\n\n /**\n * @dev Complete swap with restricted token using an EIP-2612 permit signature on the\n * restricted token — approve + settle in one atomic transaction, mirroring\n * `completeSwapWithQuoteTokenPermit` on the quote side. No half-done state: either\n * the authorisation and the settlement both land, or neither does.\n * `permitValue` must cover the seller's existing restricted-token reservations plus\n * this settlement's delta, as of execution time. With `sellSwapNumber_ == 0` the bid\n * is additional intent and the delta is `restrictedTokenAmount`. With a parent offer\n * named the delta is zero: the parent's reservation already covers the bid (the offer\n * shrinks by it in this same transaction), so signing beyond the current requirement\n * would leave the seller over-authorised relative to their remaining intent.\n * See `_applyPermit` for the sizing rules and the front-running defense.\n * @param swapNumber_ swap number of the bid (closed buy order naming the caller as seller)\n * @param sellSwapNumber_ swap number of the caller's open sell offer to deduct from (0 = none)\n * @param permitValue the EIP-2612 `value` the caller signed (see the sizing rule above)\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function completeSwapWithRestrictedTokenPermit(\n uint256 swapNumber_,\n uint256 sellSwapNumber_,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n Swap memory swap = _swap[swapNumber_];\n address msgSender = _msgSender();\n\n _validateRestrictedTokenCompletion(swap, msgSender);\n\n uint256 requiredAfterPermit = _requiredAllowance[msgSender][\n address(restrictedLockupToken)\n ];\n if (sellSwapNumber_ == 0) {\n requiredAfterPermit += swap.restrictedTokenAmount;\n }\n\n _applyPermit(\n address(restrictedLockupToken),\n msgSender,\n requiredAfterPermit,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n true\n );\n\n _completeSwapWithRestrictedToken(\n swapNumber_,\n sellSwapNumber_,\n swap,\n msgSender\n );\n }\n\n function _validateRestrictedTokenCompletion(\n Swap memory swap,\n address msgSender\n ) private pure {\n // Open buy orders (restrictedTokenSender == 0) must be filled via takeOpenBuy, not this path.\n if (swap.restrictedTokenSender == address(0)) {\n revert RestrictedSwap_OpenOrderRequiresTake();\n }\n if (swap.restrictedTokenSender != msgSender) {\n revert RestrictedSwap_InvalidTokenSender();\n }\n if (swap.status != SwapStatus.BuyConfigured) {\n revert RestrictedSwap_InvalidSwapStatus();\n }\n }\n\n // `swap` is the pre-loaded record for `swapNumber_` — both entry points already hold it,\n // so it is passed in rather than re-read from storage. Validation stays here as the single\n // source of truth for both paths (re-checking a memory struct costs almost nothing).\n function _completeSwapWithRestrictedToken(\n uint256 swapNumber_,\n uint256 sellSwapNumber_,\n Swap memory swap,\n address msgSender\n ) private {\n _validateRestrictedTokenCompletion(swap, msgSender);\n if (\n IERC20(swap.quoteToken).balanceOf(swap.quoteTokenSender) <\n swap.quoteTokenAmount\n ) {\n revert RestrictedSwap_InsufficientQuoteTokenAmount();\n }\n if (\n restrictedLockupToken.balanceOf(swap.restrictedTokenSender) <\n swap.restrictedTokenAmount\n ) {\n revert RestrictedSwap_InsufficientRestrictedTokenAmount();\n }\n\n uint256 code = restrictedLockupToken.detectTransferRestriction(\n swap.restrictedTokenSender,\n swap.quoteTokenSender,\n swap.restrictedTokenAmount\n );\n\n require(\n restrictedLockupToken.transferRules().checkSuccess(code),\n restrictedLockupToken.transferRules().messageForTransferRestriction(\n code\n )\n );\n\n _pendingBuys[swap.restrictedTokenSender] -= swap.restrictedTokenAmount;\n\n // Effects (CEI): the parent-offer shrink precedes the external settlement transfers\n // in _completeSwap.\n if (sellSwapNumber_ != 0) {\n _deductFromParentSell(\n sellSwapNumber_,\n swap.restrictedTokenAmount,\n msgSender\n );\n }\n\n _completeSwap(swapNumber_);\n }\n\n /**\n * @dev Shrink the caller's open sell offer by a restricted-token amount being delivered\n * in this same transaction — either a bid being settled (`completeSwapWithRestrictedToken`)\n * or an open-buy fill (`takeOpenBuy`). The parent must be an OPEN sell (closed swaps\n * settle their original configured amounts and cannot shrink), owned by the caller,\n * still SellConfigured, with remaining size covering the deduction. The amount must\n * divide cleanly against the offer's price ratio (`_computeFillQuote` rule inside\n * `_shrinkOrder`); a deduction equal to the full remaining cancels the offer.\n */\n function _deductFromParentSell(\n uint256 sellSwapNumber_,\n uint256 bidRestrictedAmount,\n address msgSender\n ) private {\n Swap storage sell = _swap[sellSwapNumber_];\n if (sell.restrictedTokenSender != msgSender) {\n revert RestrictedSwap_InvalidTokenSender();\n }\n if (sell.status != SwapStatus.SellConfigured) {\n revert RestrictedSwap_InvalidSwapStatus();\n }\n if (sell.quoteTokenSender != address(0)) {\n revert RestrictedSwap_NotOpenOrder();\n }\n if (bidRestrictedAmount > sell.remainingRestrictedTokenAmount) {\n revert RestrictedSwap_ExceedsParentRemaining();\n }\n _shrinkOrder(\n sellSwapNumber_,\n sell.remainingRestrictedTokenAmount - bidRestrictedAmount\n );\n }\n\n /**\n * @dev Fill an open sell order. The taker pays a pro-rata `fillQuoteAmount` of quote token\n * and receives `fillAmount` of restricted token. Multiple buyers can fill the same\n * open order until `remainingRestrictedTokenAmount` reaches zero.\n * @param swapNumber_ swap number\n * @param fillAmount restricted-token amount to take (must be > 0 and <= remaining)\n */\n function takeOpenSell(\n uint256 swapNumber_,\n uint256 fillAmount\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n _takeOpenSell(swapNumber_, fillAmount, _msgSender());\n }\n\n /**\n * @dev Fill an open sell order using an EIP-2612 permit on the quote token. Bundles\n * approve + take into a single relayer-driven tx so the taker signs only one\n * ERC2771 forward request to close a listing.\n * `permitValue` must cover `requiredAllowance(taker, quoteToken) + fillQuoteAmount`\n * as of execution time. An instant buy is always additional intent on top of the\n * taker's active buy orders — there is no buy-side parent link — so the delta is\n * always the full `fillQuoteAmount`. See `_applyPermit` for the sizing rules and the\n * front-running defense.\n */\n function takeOpenSellWithPermit(\n uint256 swapNumber_,\n uint256 fillAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n address msgSender = _msgSender();\n Swap memory swap = _swap[swapNumber_];\n\n _validateOpenSellTake(swap, fillAmount, msgSender);\n uint256 fillQuoteAmount = _computeFillQuote(swap, fillAmount);\n\n uint256 requiredAfterPermit = _requiredAllowance[msgSender][\n swap.quoteToken\n ] + fillQuoteAmount;\n\n _applyPermit(\n swap.quoteToken,\n msgSender,\n requiredAfterPermit,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n false\n );\n\n _executeOpenSellFill(swapNumber_, swap, fillAmount, fillQuoteAmount, msgSender);\n }\n\n /**\n * @dev Fill an open buy order. The taker delivers `fillAmount` of restricted token\n * and receives the pro-rata quote token from the buyer's escrowed allowance.\n * `sellSwapNumber_` optionally names the taker's own OPEN sell offer the delivered\n * tokens are drawn from: the offer is shrunk by `fillAmount` in the same transaction\n * (emitting `OrderResized`), so it stops advertising inventory this fill just\n * consumed — the same parent linkage `completeSwapWithRestrictedToken` supports for\n * bid accepts. Pass 0 when the fill draws from no open offer. Note the fill amount\n * must divide cleanly against the PARENT's price ratio too (`_shrinkOrder` rule);\n * callers whose fill does not divide must pass 0 and fall back to allowance-sizing.\n * @param swapNumber_ swap number\n * @param fillAmount restricted-token amount to deliver\n * @param sellSwapNumber_ swap number of the taker's open sell offer to deduct from (0 = none)\n */\n function takeOpenBuy(\n uint256 swapNumber_,\n uint256 fillAmount,\n uint256 sellSwapNumber_\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n _takeOpenBuy(swapNumber_, fillAmount, sellSwapNumber_, _msgSender());\n }\n\n /**\n * @dev Fill an open buy order using an EIP-2612 permit on the restricted token.\n * `permitValue` must cover the taker's existing restricted-token reservations plus\n * this fill's delta, as of execution time — same rule as\n * `completeSwapWithRestrictedTokenPermit`. The delta is `fillAmount` with\n * `sellSwapNumber_ == 0`, and zero with a parent offer named, whose reservation\n * already covers the fill (the offer shrinks by it in this same transaction).\n * See `_applyPermit` for the sizing rules and the front-running defense.\n * @param swapNumber_ swap number\n * @param fillAmount restricted-token amount to deliver\n * @param sellSwapNumber_ swap number of the taker's open sell offer to deduct from (0 = none)\n * @param permitValue the EIP-2612 `value` the caller signed (see the sizing rule above)\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function takeOpenBuyWithPermit(\n uint256 swapNumber_,\n uint256 fillAmount,\n uint256 sellSwapNumber_,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n address msgSender = _msgSender();\n Swap memory swap = _swap[swapNumber_];\n\n _validateOpenBuyTake(swap, fillAmount, msgSender);\n\n uint256 requiredAfterPermit = _requiredAllowance[msgSender][\n address(restrictedLockupToken)\n ];\n if (sellSwapNumber_ == 0) {\n requiredAfterPermit += fillAmount;\n }\n\n _applyPermit(\n address(restrictedLockupToken),\n msgSender,\n requiredAfterPermit,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n true\n );\n\n _takeOpenBuy(swapNumber_, fillAmount, sellSwapNumber_, msgSender);\n }\n\n function _takeOpenSell(\n uint256 swapNumber_,\n uint256 fillAmount,\n address taker\n ) private {\n Swap memory swap = _swap[swapNumber_];\n _validateOpenSellTake(swap, fillAmount, taker);\n uint256 fillQuoteAmount = _computeFillQuote(swap, fillAmount);\n _executeOpenSellFill(swapNumber_, swap, fillAmount, fillQuoteAmount, taker);\n }\n\n function _takeOpenBuy(\n uint256 swapNumber_,\n uint256 fillAmount,\n uint256 sellSwapNumber_,\n address taker\n ) private {\n Swap memory swap = _swap[swapNumber_];\n _validateOpenBuyTake(swap, fillAmount, taker);\n uint256 fillQuoteAmount = _computeFillQuote(swap, fillAmount);\n\n // Effects (CEI): the parent-offer shrink precedes the external settlement transfers\n // in _executeOpenBuyFill — same ordering as the bid-accept path.\n if (sellSwapNumber_ != 0) {\n _deductFromParentSell(sellSwapNumber_, fillAmount, taker);\n }\n\n _executeOpenBuyFill(swapNumber_, swap, fillAmount, fillQuoteAmount, taker);\n }\n\n function _validateOpenSellTake(\n Swap memory swap,\n uint256 fillAmount,\n address taker\n ) private pure {\n if (swap.quoteTokenSender != address(0)) {\n revert RestrictedSwap_NotOpenOrder();\n }\n if (swap.status != SwapStatus.SellConfigured) {\n revert RestrictedSwap_InvalidSwapStatus();\n }\n if (fillAmount == 0 || fillAmount > swap.remainingRestrictedTokenAmount) {\n revert RestrictedSwap_InvalidFillAmount();\n }\n // A fill must meet the configured minimum unless it takes the entire remaining amount,\n // so a below-minimum tail left over from prior fills can still be bought in full.\n if (\n fillAmount < swap.minimumFillAmount &&\n fillAmount != swap.remainingRestrictedTokenAmount\n ) {\n revert RestrictedSwap_FillBelowMinimum();\n }\n // The seller is the configurer (swap.restrictedTokenSender) and cannot also be the\n // counterparty on their own listing — wash fills would emit misleading OpenSwapFilled\n // events and let a seller cheaply close a listing without going through cancelSwap.\n if (taker == swap.restrictedTokenSender) {\n revert RestrictedSwap_SelfFillNotAllowed();\n }\n }\n\n function _validateOpenBuyTake(\n Swap memory swap,\n uint256 fillAmount,\n address taker\n ) private pure {\n if (swap.restrictedTokenSender != address(0)) {\n revert RestrictedSwap_NotOpenOrder();\n }\n if (swap.status != SwapStatus.BuyConfigured) {\n revert RestrictedSwap_InvalidSwapStatus();\n }\n if (fillAmount == 0 || fillAmount > swap.remainingRestrictedTokenAmount) {\n revert RestrictedSwap_InvalidFillAmount();\n }\n // A fill must meet the configured minimum unless it takes the entire remaining amount,\n // so a below-minimum tail left over from prior fills can still be bought in full.\n if (\n fillAmount < swap.minimumFillAmount &&\n fillAmount != swap.remainingRestrictedTokenAmount\n ) {\n revert RestrictedSwap_FillBelowMinimum();\n }\n // Symmetric to _validateOpenSellTake: the buyer is the configurer (swap.quoteTokenSender)\n // and cannot self-fill.\n if (taker == swap.quoteTokenSender) {\n revert RestrictedSwap_SelfFillNotAllowed();\n }\n }\n\n function _computeFillQuote(\n Swap memory swap,\n uint256 fillAmount\n ) private pure returns (uint256) {\n // Strict exact-pricing: reject any fillAmount that does not produce an integer\n // pro-rata quote charge. This eliminates rounding artifacts entirely — every fill\n // pays its precise share, total quote collected always equals quoteTokenAmount, and\n // no taker can ever underpay or overpay by even one wei.\n uint256 product = swap.quoteTokenAmount * fillAmount;\n if (product % swap.restrictedTokenAmount != 0) {\n revert RestrictedSwap_AmountNotDivisible();\n }\n return product / swap.restrictedTokenAmount;\n }\n\n function _executeOpenSellFill(\n uint256 swapNumber_,\n Swap memory swap,\n uint256 fillAmount,\n uint256 fillQuoteAmount,\n address taker\n ) private {\n if (\n IERC20(swap.quoteToken).balanceOf(taker) < fillQuoteAmount\n ) {\n revert RestrictedSwap_InsufficientQuoteTokenAmount();\n }\n if (\n restrictedLockupToken.balanceOf(swap.restrictedTokenSender) <\n fillAmount\n ) {\n revert RestrictedSwap_InsufficientRestrictedTokenAmount();\n }\n\n uint256 code = restrictedLockupToken.detectTransferRestriction(\n swap.restrictedTokenSender,\n taker,\n fillAmount\n );\n require(\n restrictedLockupToken.transferRules().checkSuccess(code),\n restrictedLockupToken.transferRules().messageForTransferRestriction(\n code\n )\n );\n\n // Effects (CEI): writes precede external transfers; nonReentrant on the entry function.\n Swap storage s_ = _swap[swapNumber_];\n uint256 newRemainingRestricted = swap.remainingRestrictedTokenAmount - fillAmount;\n s_.remainingRestrictedTokenAmount = newRemainingRestricted;\n s_.remainingQuoteTokenAmount = swap.remainingQuoteTokenAmount - fillQuoteAmount;\n _pendingSells[swap.restrictedTokenSender] -= fillAmount;\n _requiredAllowance[swap.restrictedTokenSender][\n address(restrictedLockupToken)\n ] -= fillAmount;\n bool nowComplete = newRemainingRestricted == 0;\n if (nowComplete) {\n s_.status = SwapStatus.Complete;\n }\n\n // Interactions: settle quote first (taker -> seller), then restricted (seller -> taker).\n uint256 quoteBefore = IERC20(swap.quoteToken).balanceOf(\n swap.restrictedTokenSender\n );\n IERC20(swap.quoteToken).safeTransferFrom(\n taker,\n swap.restrictedTokenSender,\n fillQuoteAmount\n );\n // Recipient leg only: the payer's debit is deliberately unmeasured. See the\n // contract-level `@dev` for why, and for the integrator constraint that follows.\n if (\n IERC20(swap.quoteToken).balanceOf(swap.restrictedTokenSender) -\n quoteBefore !=\n fillQuoteAmount\n ) {\n revert RestrictedSwap_InconsistentQuoteTokenAmount();\n }\n\n uint256 restrictedBefore = restrictedLockupToken.balanceOf(taker);\n restrictedLockupToken.transferFrom(\n swap.restrictedTokenSender,\n taker,\n fillAmount\n );\n if (\n restrictedLockupToken.balanceOf(taker) - restrictedBefore !=\n fillAmount\n ) {\n revert RestrictedSwap_InconsistentRestrictedTokenAmount();\n }\n\n emit OpenSwapFilled(\n swapNumber_,\n taker,\n fillAmount,\n fillQuoteAmount,\n newRemainingRestricted\n );\n\n if (nowComplete) {\n emit SwapComplete(\n swapNumber_,\n swap.restrictedTokenSender,\n swap.restrictedTokenAmount,\n taker,\n swap.quoteToken,\n swap.quoteTokenAmount,\n swap.deadline\n );\n }\n }\n\n function _executeOpenBuyFill(\n uint256 swapNumber_,\n Swap memory swap,\n uint256 fillAmount,\n uint256 fillQuoteAmount,\n address taker\n ) private {\n if (\n IERC20(swap.quoteToken).balanceOf(swap.quoteTokenSender) <\n fillQuoteAmount\n ) {\n revert RestrictedSwap_InsufficientQuoteTokenAmount();\n }\n if (restrictedLockupToken.balanceOf(taker) < fillAmount) {\n revert RestrictedSwap_InsufficientRestrictedTokenAmount();\n }\n\n uint256 code = restrictedLockupToken.detectTransferRestriction(\n taker,\n swap.quoteTokenSender,\n fillAmount\n );\n require(\n restrictedLockupToken.transferRules().checkSuccess(code),\n restrictedLockupToken.transferRules().messageForTransferRestriction(\n code\n )\n );\n\n // Effects\n Swap storage s_ = _swap[swapNumber_];\n uint256 newRemainingRestricted = swap.remainingRestrictedTokenAmount - fillAmount;\n s_.remainingRestrictedTokenAmount = newRemainingRestricted;\n s_.remainingQuoteTokenAmount = swap.remainingQuoteTokenAmount - fillQuoteAmount;\n // _pendingBuys was never incremented for open buys, so it is not decremented here.\n _requiredAllowance[swap.quoteTokenSender][swap.quoteToken] -= fillQuoteAmount;\n bool nowComplete = newRemainingRestricted == 0;\n if (nowComplete) {\n s_.status = SwapStatus.Complete;\n }\n\n // Interactions: settle quote first (buyer -> taker), then restricted (taker -> buyer).\n uint256 quoteBefore = IERC20(swap.quoteToken).balanceOf(taker);\n IERC20(swap.quoteToken).safeTransferFrom(\n swap.quoteTokenSender,\n taker,\n fillQuoteAmount\n );\n // Recipient leg only: the payer's debit is deliberately unmeasured. See the\n // contract-level `@dev` for why, and for the integrator constraint that follows.\n if (\n IERC20(swap.quoteToken).balanceOf(taker) - quoteBefore !=\n fillQuoteAmount\n ) {\n revert RestrictedSwap_InconsistentQuoteTokenAmount();\n }\n\n uint256 restrictedBefore = restrictedLockupToken.balanceOf(\n swap.quoteTokenSender\n );\n restrictedLockupToken.transferFrom(\n taker,\n swap.quoteTokenSender,\n fillAmount\n );\n if (\n restrictedLockupToken.balanceOf(swap.quoteTokenSender) -\n restrictedBefore !=\n fillAmount\n ) {\n revert RestrictedSwap_InconsistentRestrictedTokenAmount();\n }\n\n emit OpenSwapFilled(\n swapNumber_,\n taker,\n fillAmount,\n fillQuoteAmount,\n newRemainingRestricted\n );\n\n if (nowComplete) {\n emit SwapComplete(\n swapNumber_,\n taker,\n swap.restrictedTokenAmount,\n swap.quoteTokenSender,\n swap.quoteToken,\n swap.quoteTokenAmount,\n swap.deadline\n );\n }\n }\n\n /**\n * @dev cancel swap\n * @param swapNumber_ swap number\n */\n function cancelSwap(\n uint256 swapNumber_\n ) external override nonReentrant onlyActiveSwap(swapNumber_) {\n Swap storage swap = _swap[swapNumber_];\n\n // A genuinely uninitialized swap record has BOTH addresses zero. Open orders zero only one\n // side, so the guard now requires both — otherwise legitimate open orders couldn't be canceled.\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_SwapNotConfigured();\n }\n\n // Check if swap has expired\n bool isExpired = swap.deadline > 0 && block.timestamp > swap.deadline;\n\n address msgSender = _msgSender();\n // Refund based on the *remaining* unfilled amounts so partial fills on open orders settle cleanly.\n if (swap.status == SwapStatus.SellConfigured) {\n if (!isExpired && msgSender != swap.restrictedTokenSender) {\n revert RestrictedSwap_InvalidCanceler();\n }\n uint256 remainRestricted = swap.remainingRestrictedTokenAmount;\n _pendingSells[swap.restrictedTokenSender] -= remainRestricted;\n _requiredAllowance[swap.restrictedTokenSender][\n address(restrictedLockupToken)\n ] -= remainRestricted;\n } else if (swap.status == SwapStatus.BuyConfigured) {\n if (!isExpired && msgSender != swap.quoteTokenSender) {\n revert RestrictedSwap_InvalidCanceler();\n }\n // _pendingBuys was only incremented for closed buys (named seller).\n if (swap.restrictedTokenSender != address(0)) {\n _pendingBuys[swap.restrictedTokenSender] -= swap\n .remainingRestrictedTokenAmount;\n }\n _requiredAllowance[swap.quoteTokenSender][swap.quoteToken] -= swap\n .remainingQuoteTokenAmount;\n }\n\n // The `remaining*` amounts are deliberately NOT cleared. The refunds above already\n // consumed them for accounting purposes, but the record keeps the unfilled tail as\n // on-chain history of what was withdrawn rather than filled — otherwise that figure is\n // only recoverable by replaying `OpenSwapFilled` / `OrderResized`. Safe because every\n // reader of `remaining*` requires status `SellConfigured` / `BuyConfigured` first, so a\n // canceled record can never be double-refunded or re-filled. Any new reader MUST keep\n // that status gate. Contrast `_completeSwap`, which does zero them: there the retained\n // value would be a stale configure-time amount, not history.\n swap.status = SwapStatus.Canceled;\n\n emit SwapCanceled(msgSender, swapNumber_);\n }\n\n /**\n * @dev Shrink an active OPEN order's remaining size in place — owner-only, decrease-only.\n * The single-signature alternative to cancel-and-recreate when an investor's intent\n * drops below the advertised size: the order keeps its number and history, and the\n * creator's reservations are released exactly as `cancelSwap` would release the\n * removed portion. Shrinking to zero cancels the order. Closed swaps (both\n * counterparties named) revert with `RestrictedSwap_NotOpenOrder`: they settle their\n * ORIGINAL configured amounts, so an in-place resize would corrupt the reservation\n * accounting (see `_shrinkOrder`).\n * Like `cancelSwap`, intentionally callable while paused and on expired orders —\n * reducing exposure is an exit-type action that must never be blocked.\n * @param swapNumber_ swap number of the caller's open order\n * @param newRemainingRestrictedAmount new remaining restricted-token size; must be strictly\n * less than the current remaining amount (0 cancels the order)\n */\n function decreaseOrder(\n uint256 swapNumber_,\n uint256 newRemainingRestrictedAmount\n ) external override nonReentrant onlyActiveSwap(swapNumber_) {\n Swap memory swap = _swap[swapNumber_];\n\n // Same uninitialized-record guard as cancelSwap: a genuinely missing swap has BOTH\n // addresses zero; open orders zero exactly one side.\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_SwapNotConfigured();\n }\n\n address creator = swap.status == SwapStatus.SellConfigured\n ? swap.restrictedTokenSender\n : swap.quoteTokenSender;\n if (_msgSender() != creator) {\n revert RestrictedSwap_InvalidOrderOwner();\n }\n\n _shrinkOrder(swapNumber_, newRemainingRestrictedAmount);\n }\n\n /**\n * @dev Shrink an open order's remaining size in place and release the creator's matching\n * reservations — the exact accounting `cancelSwap` performs on the removed portion.\n *\n * Only OPEN orders may shrink: closed swaps settle their ORIGINAL amounts\n * (`_completeSwap` transfers `restrictedTokenAmount` / `quoteTokenAmount`, not the\n * remaining), so shrinking one and then completing it would double-release\n * `_pendingSells` / `_pendingBuys` / `_requiredAllowance` — underflow reverts that\n * could brick the owner's other active orders.\n *\n * The removed quote is computed with the same strict exact-pricing rule as partial\n * fills (`_computeFillQuote` on the order's ORIGINAL price ratio), so remaining\n * amounts always stay exactly on-ratio and `remainingQuoteTokenAmount` still reaches\n * zero exactly when `remainingRestrictedTokenAmount` does; a removed amount that does\n * not divide cleanly reverts with `RestrictedSwap_AmountNotDivisible`. This also rules\n * out any under/overflow: on-ratio remainders guarantee `quoteRemoved <=\n * remainingQuoteTokenAmount`.\n *\n * Shrinking to zero marks the order Canceled (the remainder is withdrawn, not\n * filled) and emits `SwapCanceled` alongside `OrderResized`.\n */\n function _shrinkOrder(\n uint256 swapNumber_,\n uint256 newRemainingRestricted\n ) private {\n Swap storage s_ = _swap[swapNumber_];\n Swap memory swap = s_;\n\n // Defense-in-depth: callers already restrict to open orders (exactly one side zero).\n if (\n swap.restrictedTokenSender != address(0) &&\n swap.quoteTokenSender != address(0)\n ) {\n revert RestrictedSwap_NotOpenOrder();\n }\n\n uint256 oldRemaining = swap.remainingRestrictedTokenAmount;\n if (newRemainingRestricted >= oldRemaining) {\n revert RestrictedSwap_InvalidResizeAmount();\n }\n uint256 removed = oldRemaining - newRemainingRestricted;\n uint256 quoteRemoved = _computeFillQuote(swap, removed);\n\n if (swap.status == SwapStatus.SellConfigured) {\n _pendingSells[swap.restrictedTokenSender] -= removed;\n _requiredAllowance[swap.restrictedTokenSender][\n address(restrictedLockupToken)\n ] -= removed;\n } else {\n // Open buy: only the buyer's quote reservation shrinks. `_pendingBuys` is never\n // incremented for open buys (no named seller), so it is not touched here.\n _requiredAllowance[swap.quoteTokenSender][\n swap.quoteToken\n ] -= quoteRemoved;\n }\n\n s_.remainingRestrictedTokenAmount = newRemainingRestricted;\n s_.remainingQuoteTokenAmount =\n swap.remainingQuoteTokenAmount -\n quoteRemoved;\n\n emit OrderResized(\n swapNumber_,\n newRemainingRestricted,\n s_.remainingQuoteTokenAmount\n );\n\n if (newRemainingRestricted == 0) {\n s_.status = SwapStatus.Canceled;\n emit SwapCanceled(_msgSender(), swapNumber_);\n }\n }\n\n /**\n * @dev Grow an active OPEN order's remaining size in place — owner-only, increase-only.\n * The counterpart to `decreaseOrder`, but with a configure-grade guard set: growing\n * an order CREATES new exposure, so unlike the exit-type `decreaseOrder` it is\n * blocked while paused, blocked on expired orders (`onlyValidSwap`), and re-checks\n * the creator's allowance — and, for buy orders, quote balance — exactly like\n * `configureSell` / `configureBuy` do at creation time.\n * The paired quote amount grows exactly pro-rata on the order's ORIGINAL price\n * ratio (same strict divisibility rule as partial fills), so the price cannot\n * change. `remainingRestrictedTokenAmount` may exceed the originally configured\n * `restrictedTokenAmount` after an increase; indexers must derive traded totals\n * from `OpenSwapFilled` events rather than the configured amounts.\n * @param swapNumber_ swap number of the caller's open order\n * @param newRemainingRestrictedAmount new remaining restricted-token size; must be\n * strictly greater than the current remaining amount\n */\n function increaseOrder(\n uint256 swapNumber_,\n uint256 newRemainingRestrictedAmount\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n address msgSender = _msgSender();\n (\n Swap memory swap,\n uint256 added,\n uint256 quoteAdded\n ) = _validateIncreaseOrder(\n swapNumber_,\n newRemainingRestrictedAmount,\n msgSender\n );\n\n if (swap.status == SwapStatus.SellConfigured) {\n if (\n restrictedLockupToken.allowance(msgSender, address(this)) <\n _requiredAllowance[msgSender][address(restrictedLockupToken)] +\n added\n ) {\n revert RestrictedSwap_InsufficientRestrictedTokenAllowance();\n }\n } else {\n _validateBuyIncreaseFunds(msgSender, swap, quoteAdded);\n if (\n IERC20(swap.quoteToken).allowance(msgSender, address(this)) <\n _requiredAllowance[msgSender][swap.quoteToken] + quoteAdded\n ) {\n revert RestrictedSwap_InsufficientQuoteTokenAllowance();\n }\n }\n\n _growOrder(swapNumber_, added, quoteAdded);\n }\n\n /**\n * @dev `increaseOrder` with an EIP-2612 permit bundled in, so the allowance top-up and\n * the size increase land in one atomic transaction (mirrors the configure*WithPermit\n * pattern). The permitted token depends on the order side: the restricted token for\n * an open sell, the order's quote token for an open buy. `permitValue` must cover\n * `requiredAllowance + added` (restricted side) or `requiredAllowance + quoteAdded`\n * (quote side) as of execution time. Only one side's permit is ever consumed, so a\n * single signed value covers either branch. See `_applyPermit` for the sizing rules\n * and the front-running defense.\n */\n function increaseOrderWithPermit(\n uint256 swapNumber_,\n uint256 newRemainingRestrictedAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override nonReentrant onlyValidSwap(swapNumber_) whenNotPaused {\n address msgSender = _msgSender();\n (\n Swap memory swap,\n uint256 added,\n uint256 quoteAdded\n ) = _validateIncreaseOrder(\n swapNumber_,\n newRemainingRestrictedAmount,\n msgSender\n );\n\n if (swap.status == SwapStatus.SellConfigured) {\n uint256 requiredAllowance_ = _requiredAllowance[msgSender][\n address(restrictedLockupToken)\n ] + added;\n\n _applyPermit(\n address(restrictedLockupToken),\n msgSender,\n requiredAllowance_,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n true\n );\n } else {\n _validateBuyIncreaseFunds(msgSender, swap, quoteAdded);\n uint256 requiredAllowance_ = _requiredAllowance[msgSender][\n swap.quoteToken\n ] + quoteAdded;\n\n _applyPermit(\n swap.quoteToken,\n msgSender,\n requiredAllowance_,\n permitValue,\n permitDeadline,\n v,\n r,\n s,\n false\n );\n }\n\n _growOrder(swapNumber_, added, quoteAdded);\n }\n\n /**\n * @dev Shared validation for both increase entry points: the record exists, is an OPEN\n * order, the caller is its creator, the new size is strictly larger, and the added\n * amount divides cleanly against the order's original price ratio.\n */\n function _validateIncreaseOrder(\n uint256 swapNumber_,\n uint256 newRemainingRestricted,\n address msgSender\n ) private view returns (Swap memory swap, uint256 added, uint256 quoteAdded) {\n swap = _swap[swapNumber_];\n\n // Same uninitialized-record guard as decreaseOrder / cancelSwap.\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_SwapNotConfigured();\n }\n // Closed swaps settle their original amounts and cannot be resized (see _shrinkOrder).\n if (\n swap.restrictedTokenSender != address(0) &&\n swap.quoteTokenSender != address(0)\n ) {\n revert RestrictedSwap_NotOpenOrder();\n }\n\n address creator = swap.status == SwapStatus.SellConfigured\n ? swap.restrictedTokenSender\n : swap.quoteTokenSender;\n if (msgSender != creator) {\n revert RestrictedSwap_InvalidOrderOwner();\n }\n\n if (newRemainingRestricted <= swap.remainingRestrictedTokenAmount) {\n revert RestrictedSwap_InvalidResizeAmount();\n }\n added = newRemainingRestricted - swap.remainingRestrictedTokenAmount;\n quoteAdded = _computeFillQuote(swap, added);\n }\n\n /// @dev Mirror of `_validateConfigureBuyInputs`' balance check, applied to the grown\n /// order: the buyer must be able to cover the order's NEW total quote obligation.\n function _validateBuyIncreaseFunds(\n address msgSender,\n Swap memory swap,\n uint256 quoteAdded\n ) private view {\n if (\n IERC20(swap.quoteToken).balanceOf(msgSender) <\n swap.remainingQuoteTokenAmount + quoteAdded\n ) {\n revert RestrictedSwap_InsufficientQuoteTokenAmount();\n }\n }\n\n /// @dev Apply a validated increase: reservations grow by exactly what a fresh configure\n /// of the added amount would have reserved, and remaining amounts stay on-ratio.\n function _growOrder(\n uint256 swapNumber_,\n uint256 added,\n uint256 quoteAdded\n ) private {\n Swap storage s_ = _swap[swapNumber_];\n\n if (s_.status == SwapStatus.SellConfigured) {\n _pendingSells[s_.restrictedTokenSender] += added;\n _requiredAllowance[s_.restrictedTokenSender][\n address(restrictedLockupToken)\n ] += added;\n } else {\n // Open buy: only the buyer's quote reservation grows (`_pendingBuys` is never\n // tracked for open buys — no named seller).\n _requiredAllowance[s_.quoteTokenSender][\n s_.quoteToken\n ] += quoteAdded;\n }\n\n s_.remainingRestrictedTokenAmount += added;\n s_.remainingQuoteTokenAmount += quoteAdded;\n\n emit OrderResized(\n swapNumber_,\n s_.remainingRestrictedTokenAmount,\n s_.remainingQuoteTokenAmount\n );\n }\n\n /**\n * @dev Returns the swap status if exists\n * @param swapNumber_ swap number\n * @return SwapStatus status of the swap record\n */\n function swapStatus(\n uint256 swapNumber_\n ) external view override returns (SwapStatus) {\n Swap storage swap = _swap[swapNumber_];\n // A configured swap always has at least one non-zero counterparty (open orders zero only one side).\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_InvalidSwapRecord();\n }\n return swap.status;\n }\n\n /**\n * @dev Check if a swap has expired based on its deadline\n * @param swapNumber_ swap number\n * @return bool true if the swap has expired, false otherwise\n */\n function isSwapExpired(uint256 swapNumber_) external view override returns (bool) {\n Swap memory swap = _swap[swapNumber_];\n if (\n swap.restrictedTokenSender == address(0) &&\n swap.quoteTokenSender == address(0)\n ) {\n revert RestrictedSwap_InvalidSwapRecord();\n }\n // Check if swap has expired (deadline > 0 and current time > deadline)\n return swap.deadline > 0 && block.timestamp > swap.deadline;\n }\n\n\n /**\n * @dev Configures swap and emits an event. This function does not fund tokens. Only for swap configuration.\n * @param restrictedTokenSender restricted token sender\n * @param quoteTokenSender quote token sender\n * @param quoteToken quote token\n * @param restrictedTokenAmount restricted token amount\n * @param quoteTokenAmount quote token amount\n * @param configuror initial status or configuration type for the swap\n * @param deadline the deadline for swap expiration (0 means no deadline)\n * @param minimumFillAmount minimum restricted-token amount a taker must fill on this order\n * via takeOpenSell/takeOpenBuy, unless taking the entire remaining amount. Must be\n * <= restrictedTokenAmount. 0 means no minimum. Must be 0 for closed swaps (both\n * counterparties named) — they settle in full and never consult it.\n */\n function _configureSwap(\n address restrictedTokenSender,\n address quoteTokenSender,\n address quoteToken,\n uint256 restrictedTokenAmount,\n uint256 quoteTokenAmount,\n SwapStatus configuror,\n uint256 deadline,\n uint256 minimumFillAmount\n ) private {\n if (restrictedTokenAmount == 0) {\n revert RestrictedSwap_InvalidRestrictedTokenAmount();\n }\n if (quoteTokenAmount == 0) {\n revert RestrictedSwap_InvalidQuoteTokenAmount();\n }\n if (minimumFillAmount > restrictedTokenAmount) {\n revert RestrictedSwap_InvalidMinimumFillAmount();\n }\n\n // For open orders one side is address(0) and the actual counterparty is only known at take time,\n // so the transfer-restriction probe is deferred until then.\n if (restrictedTokenSender != address(0) && quoteTokenSender != address(0)) {\n // Closed swaps settle in full via the complete* paths and never reach the take\n // functions, so a non-zero minimum would only advertise (in SwapConfigured and the\n // minimumFillAmount view) a constraint nothing enforces.\n if (minimumFillAmount != 0) {\n revert RestrictedSwap_InvalidMinimumFillAmount();\n }\n uint256 code = restrictedLockupToken.detectTransferRestriction(\n restrictedTokenSender,\n quoteTokenSender,\n restrictedTokenAmount\n );\n\n string memory message = restrictedLockupToken\n .messageForTransferRestriction(code);\n\n require(\n restrictedLockupToken.transferRules().checkSuccess(code),\n message\n );\n }\n\n try\n IERC165(quoteToken).supportsInterface(type(IERC1404).interfaceId)\n returns (bool supported1404) {\n if (supported1404) {\n revert RestrictedSwap_QuoteTokenMustNotSupportIERC1404();\n }\n // happy path, ie ERC1404 is not supported, even if ERC165 is supported\n } catch {\n // happy path, ie ERC165 is not supported. Most ERC-20 tokens do not support ERC-165\n }\n\n _swapNumber += 1;\n\n Swap storage swap = _swap[_swapNumber];\n swap.restrictedTokenSender = restrictedTokenSender;\n swap.restrictedTokenAmount = restrictedTokenAmount;\n swap.quoteTokenSender = quoteTokenSender;\n swap.quoteTokenAmount = quoteTokenAmount;\n swap.quoteToken = quoteToken;\n swap.status = configuror;\n swap.deadline = deadline;\n swap.remainingRestrictedTokenAmount = restrictedTokenAmount;\n swap.remainingQuoteTokenAmount = quoteTokenAmount;\n swap.minimumFillAmount = minimumFillAmount;\n\n if (configuror == SwapStatus.SellConfigured) {\n _requiredAllowance[swap.restrictedTokenSender][\n address(restrictedLockupToken)\n ] += restrictedTokenAmount;\n } else {\n _requiredAllowance[swap.quoteTokenSender][\n quoteToken\n ] += quoteTokenAmount;\n }\n\n emit SwapConfigured(\n _swapNumber,\n restrictedTokenSender,\n restrictedTokenAmount,\n quoteToken,\n quoteTokenSender,\n quoteTokenAmount,\n deadline,\n minimumFillAmount\n );\n }\n\n /**\n * @dev Complete swap and emit an event\n * @param swapNumber_ swap number\n */\n function _completeSwap(uint256 swapNumber_) private {\n Swap memory swap = _swap[swapNumber_];\n\n // Effects (CEI): writes precede external transfers. nonReentrant on the entry function\n // already blocks reentry, but the effects-first ordering matches `_executeOpenSellFill`\n // / `_executeOpenBuyFill` and removes a footgun for future callers without nonReentrant.\n if (swap.status == SwapStatus.SellConfigured) {\n _requiredAllowance[swap.restrictedTokenSender][\n address(restrictedLockupToken)\n ] -= swap.restrictedTokenAmount;\n } else if (swap.status == SwapStatus.BuyConfigured) {\n _requiredAllowance[swap.quoteTokenSender][swap.quoteToken] -= swap\n .quoteTokenAmount;\n }\n Swap storage s_ = _swap[swapNumber_];\n s_.status = SwapStatus.Complete;\n // Upholds the invariant that a `Complete` swap always reports zero remaining, matching\n // `_executeOpenSellFill` / `_executeOpenBuyFill` where a full fill reaches zero\n // arithmetically. This write is needed because a closed swap never decrements\n // `remaining*` during its lifetime — it settles the ORIGINAL `restrictedTokenAmount` /\n // `quoteTokenAmount` in full — so leaving the field alone would report the entire amount\n // as unfilled: a stale configure-time value, not history. Unlike `cancelSwap`, nothing\n // is lost by clearing here.\n s_.remainingRestrictedTokenAmount = 0;\n s_.remainingQuoteTokenAmount = 0;\n\n // Interactions: balance probes around each transfer remain as defense\n // against fee-on-transfer / non-standard ERC20 tokens.\n uint256 balanceBeforeTransfer = IERC20(swap.quoteToken).balanceOf(\n swap.restrictedTokenSender\n );\n\n IERC20(swap.quoteToken).safeTransferFrom(\n swap.quoteTokenSender,\n swap.restrictedTokenSender,\n swap.quoteTokenAmount\n );\n\n uint256 balanceAfterTransfer = IERC20(swap.quoteToken).balanceOf(\n swap.restrictedTokenSender\n );\n\n // Recipient leg only: the payer's debit is deliberately unmeasured. See the\n // contract-level `@dev` for why, and for the integrator constraint that follows.\n if (\n balanceAfterTransfer - balanceBeforeTransfer !=\n swap.quoteTokenAmount\n ) {\n revert RestrictedSwap_InconsistentQuoteTokenAmount();\n }\n\n uint256 _restrictedTokenBalanceBefore = restrictedLockupToken.balanceOf(\n swap.quoteTokenSender\n );\n restrictedLockupToken.transferFrom(\n swap.restrictedTokenSender,\n swap.quoteTokenSender,\n swap.restrictedTokenAmount\n );\n\n uint256 _restrictedTokenBalanceAfter = restrictedLockupToken.balanceOf(\n swap.quoteTokenSender\n );\n if (\n _restrictedTokenBalanceAfter - _restrictedTokenBalanceBefore !=\n swap.restrictedTokenAmount\n ) {\n revert RestrictedSwap_InconsistentRestrictedTokenAmount();\n }\n\n emit SwapComplete(\n swapNumber_,\n swap.restrictedTokenSender,\n swap.restrictedTokenAmount,\n swap.quoteTokenSender,\n swap.quoteToken,\n swap.quoteTokenAmount,\n swap.deadline\n );\n }\n}\n"},"contracts/SnapshotPeriods.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\nimport {RestrictedLockupToken} from \"./RestrictedLockupToken.sol\";\nimport {ISnapshotPeriods} from \"./interfaces/ISnapshotPeriods.sol\";\nimport {ERC165} from \"@openzeppelin/contracts/utils/introspection/ERC165.sol\";\n\ncontract SnapshotPeriods is ISnapshotPeriods, ERC165 {\n bytes4 public immutable INTERFACE_ID;\n\n mapping(address token => mapping(address account => WalletInfo))\n public walletInfo;\n mapping(address token => mapping(address account => Period[]))\n public walletPeriods;\n\n // total supply periods\n mapping(address token => Period[]) public totalSupplyPeriods;\n // total supply info\n mapping(address token => WalletInfo) public totalSupplyInfo;\n\n error SnapshotPeriods_InvalidToken();\n error SnapshotPeriods_UnauthorizedCaller();\n error SnapshotPeriods_TimestampInFuture();\n constructor() {\n INTERFACE_ID = type(ISnapshotPeriods).interfaceId;\n }\n\n /**\n * Support of ERC165\n * @dev See https://eips.ethereum.org/EIPS/eip-165\n * @param interfaceId The interface identifier, as specified in ERC-165\n */\n function supportsInterface(\n bytes4 interfaceId\n ) public view override returns (bool) {\n return\n interfaceId == INTERFACE_ID || super.supportsInterface(interfaceId);\n }\n\n /// @inheritdoc ISnapshotPeriods\n function getPastBalanceOf(\n address token,\n address account,\n uint256 timestamp\n ) public view returns (uint256) {\n if (timestamp >= block.timestamp) {\n revert SnapshotPeriods_TimestampInFuture();\n }\n return ownershipForPeriod(token, account, timestamp, timestamp + 1);\n }\n\n /// @inheritdoc ISnapshotPeriods\n function getPastTotalSupply(\n address token,\n uint256 timestamp\n ) public view returns (uint256) {\n if (timestamp >= block.timestamp) {\n revert SnapshotPeriods_TimestampInFuture();\n }\n return totalOwnershipForPeriod(token, timestamp, timestamp + 1);\n }\n\n /// @inheritdoc ISnapshotPeriods\n function onUpdate(\n address tokenAddress,\n address from,\n address to,\n uint256 amount\n ) external {\n if (msg.sender != tokenAddress) {\n revert SnapshotPeriods_UnauthorizedCaller();\n }\n if (amount == 0) {\n return;\n }\n\n if (from != address(0)) {\n _updatePeriods(tokenAddress, from, amount);\n }\n if (to != address(0)) {\n _updatePeriods(\n tokenAddress,\n to,\n 0 // for recipient, we don't need to validate if new balance is 0\n );\n }\n // update total supply periods only for mint or burn when total supply is changed\n if (from == address(0) || to == address(0)) {\n _updateTotalSupplyPeriods(\n tokenAddress,\n to == address(0) ? amount : 0 // for burn, we need to clear latestAccrualTimestamp if new total supply is 0\n );\n }\n }\n\n /**\n * @dev This function calculates the ownership of `amount` for a period of time. The period\n * starts at `periodStart` and ends at `periodEnd`.\n * @notice This multiplication has no local overflow guard. The sums in\n * `totalAccruedOwnership` have no local overflow guard. The\n * `accruedOwnership * portion` calculation in `ownershipAt` and `totalOwnershipAt`\n * also has no local overflow guard.\n *\n * A limit on the total supply prevents an overflow of these three products. The\n * function `Storage._maxSafeSupply()` sets this limit to 2^192 - 1. The limit keeps\n * the largest of the three products at 2^255 or less.\n *\n * Read the description of `_maxSafeSupply()` before you add an overflow guard to\n * this function. These code paths operate for each transfer and for each claim of\n * interest.\n */\n function calculateOwnershipForPeriod(\n uint256 amount,\n uint256 periodStart,\n uint256 periodEnd\n ) public pure returns (uint256) {\n return amount * (periodEnd - periodStart);\n }\n\n function _updateTotalSupplyPeriods(\n address tokenAddress,\n uint256 amount\n ) internal {\n uint256 totalAccruedOwnership = totalSupplyInfo[tokenAddress]\n .totalAccruedOwnership;\n RestrictedLockupToken token = RestrictedLockupToken(tokenAddress);\n uint256 currentTimestamp = block.timestamp;\n if (\n totalSupplyInfo[tokenAddress].latestAccrualTimestamp >=\n currentTimestamp\n ) {\n return; // not need to update periods\n }\n\n uint256 curentTotalSupply = token.totalSupply();\n if (totalSupplyInfo[tokenAddress].latestAccrualTimestamp > 0) {\n uint256 periodOwnership = calculateOwnershipForPeriod(\n curentTotalSupply,\n totalSupplyInfo[tokenAddress].latestAccrualTimestamp,\n currentTimestamp\n );\n totalAccruedOwnership += periodOwnership;\n totalSupplyPeriods[tokenAddress].push(\n Period({\n start: totalSupplyInfo[tokenAddress].latestAccrualTimestamp,\n end: currentTimestamp,\n totalAccruedOwnership: totalAccruedOwnership,\n accruedOwnership: periodOwnership\n })\n );\n totalSupplyInfo[tokenAddress].periodCount++;\n totalSupplyInfo[tokenAddress]\n .totalAccruedOwnership = totalAccruedOwnership;\n }\n\n if (amount != 0 && curentTotalSupply == amount) {\n totalSupplyInfo[tokenAddress].latestAccrualTimestamp = 0;\n } else {\n totalSupplyInfo[tokenAddress]\n .latestAccrualTimestamp = currentTimestamp;\n }\n }\n\n function _updatePeriods(\n address tokenAddress,\n address account,\n uint256 amount\n ) internal {\n WalletInfo memory accountInfo = walletInfo[tokenAddress][account];\n\n uint256 totalAccruedOwnership = accountInfo.totalAccruedOwnership;\n RestrictedLockupToken token = RestrictedLockupToken(tokenAddress);\n uint256 currentTimestamp = block.timestamp;\n if (accountInfo.latestAccrualTimestamp >= currentTimestamp) {\n return; // not need to update periods\n }\n\n uint256 currentBalance = token.balanceOf(account);\n if (accountInfo.latestAccrualTimestamp > 0) {\n uint256 periodOwnership = calculateOwnershipForPeriod(\n currentBalance,\n accountInfo.latestAccrualTimestamp,\n currentTimestamp\n );\n totalAccruedOwnership += periodOwnership;\n walletPeriods[tokenAddress][account].push(\n Period({\n start: accountInfo.latestAccrualTimestamp,\n end: currentTimestamp,\n totalAccruedOwnership: totalAccruedOwnership,\n accruedOwnership: periodOwnership\n })\n );\n accountInfo.periodCount++;\n accountInfo.totalAccruedOwnership = totalAccruedOwnership;\n }\n\n if (amount != 0 && currentBalance == amount) {\n accountInfo.latestAccrualTimestamp = 0;\n } else {\n accountInfo.latestAccrualTimestamp = currentTimestamp;\n }\n\n walletInfo[tokenAddress][account] = accountInfo;\n }\n\n // @dev calculate total ownership at timestamp\n // @param token The token address\n // @param timestamp The timestamp to calculate the ownership at\n // @return ownership The ownership at the timestamp\n function totalOwnershipAt(\n address token,\n uint256 timestamp\n ) public view returns (uint256) {\n WalletInfo memory totalSupplyInfoNow = supplyInfo(token);\n if (\n totalSupplyInfoNow.periodCount == 0 &&\n totalSupplyInfoNow.latestAccrualTimestamp == 0\n ) {\n return 0;\n }\n // end timestamp can be before last account accrual timestamp\n // in this case, we should find period which includes timestamp and use previous period\n uint256 periodIdx = type(uint256).max;\n uint256 totalSupplyAccruedOwnership = totalSupplyInfoNow\n .totalAccruedOwnership;\n timestamp = timestamp > block.timestamp ? block.timestamp : timestamp;\n if (\n (timestamp <= totalSupplyInfoNow.latestAccrualTimestamp ||\n totalSupplyInfoNow.latestAccrualTimestamp == 0) &&\n totalSupplyInfoNow.periodCount > 0\n ) {\n periodIdx = findTotalSupplyPeriodIndexForTimestamp(\n token,\n timestamp\n );\n if (periodIdx == type(uint256).max) {\n // no previous period found -> no accrued ownership\n totalSupplyAccruedOwnership = 0;\n }\n if (periodIdx != type(uint256).max) {\n // we found previous period\n Period memory period = totalSupplyPeriodByIndex(\n token,\n periodIdx\n );\n totalSupplyAccruedOwnership = period.totalAccruedOwnership;\n }\n }\n\n bool isIncludedPeriodExists = false;\n // if included period already cached\n if (\n (periodIdx != type(uint256).max &&\n totalSupplyInfoNow.periodCount > periodIdx + 1) ||\n (periodIdx == type(uint256).max &&\n totalSupplyInfoNow.periodCount > 0)\n ) {\n Period memory period = totalSupplyPeriodByIndex(\n token,\n periodIdx == type(uint256).max ? 0 : periodIdx + 1\n );\n isIncludedPeriodExists =\n period.start < timestamp &&\n timestamp <= period.end;\n if (isIncludedPeriodExists) {\n uint256 portion = timestamp - period.start;\n uint256 portionOfOwnership = (period.accruedOwnership *\n portion) / (period.end - period.start);\n totalSupplyAccruedOwnership += portionOfOwnership;\n }\n }\n\n // ( ]( ]( timestamp ..... now\n // | latestAccrualTimestamp\n // if included period not cached - still ongoing\n if (\n totalSupplyInfoNow.latestAccrualTimestamp != 0 &&\n totalSupplyInfoNow.latestAccrualTimestamp < timestamp &&\n periodIdx == type(uint256).max\n ) {\n RestrictedLockupToken restrictedLockupToken = RestrictedLockupToken(\n token\n );\n uint256 ownershipAccrued = calculateOwnershipForPeriod(\n restrictedLockupToken.totalSupply(),\n totalSupplyInfoNow.latestAccrualTimestamp,\n timestamp\n );\n totalSupplyAccruedOwnership += ownershipAccrued;\n }\n\n return totalSupplyAccruedOwnership;\n }\n\n // @dev calculate ownership at timestamp\n // @param token The token address\n // @param account The account address\n // @param timestamp The timestamp to calculate the ownership at\n // @return ownership The ownership at the timestamp\n function ownershipAt(\n address token,\n address account,\n uint256 timestamp\n ) public view returns (uint256) {\n RestrictedLockupToken restrictedLockupToken = RestrictedLockupToken(\n token\n );\n WalletInfo memory accountInfo = addressInfo(token, account);\n if (\n accountInfo.periodCount == 0 &&\n accountInfo.latestAccrualTimestamp == 0\n ) {\n return 0;\n }\n // end timestamp can be before last account accrual timestamp\n // in this case, we should find period which includes timestamp and use previous period\n uint256 periodIdx = type(uint256).max;\n uint256 accountAccruedOwnership = accountInfo.totalAccruedOwnership;\n timestamp = timestamp > block.timestamp ? block.timestamp : timestamp;\n if (\n (timestamp <= accountInfo.latestAccrualTimestamp ||\n accountInfo.latestAccrualTimestamp == 0) &&\n accountInfo.periodCount > 0\n ) {\n periodIdx = findPeriodIndexForTimestamp(token, account, timestamp);\n if (periodIdx == type(uint256).max) {\n // no previous period found -> no accrued ownership\n accountAccruedOwnership = 0;\n }\n if (periodIdx != type(uint256).max) {\n // we found previous period\n Period memory period = walletPeriodByIndex(\n token,\n account,\n periodIdx\n );\n accountAccruedOwnership = period.totalAccruedOwnership;\n }\n }\n\n bool isIncludedPeriodExists = false;\n // if included period already cached\n if (\n (periodIdx != type(uint256).max &&\n accountInfo.periodCount > periodIdx + 1) ||\n (periodIdx == type(uint256).max && accountInfo.periodCount > 0)\n ) {\n Period memory period = walletPeriodByIndex(\n token,\n account,\n periodIdx == type(uint256).max ? 0 : periodIdx + 1\n );\n isIncludedPeriodExists =\n period.start < timestamp &&\n timestamp <= period.end;\n if (isIncludedPeriodExists) {\n uint256 portion = timestamp - period.start;\n uint256 portionOfOwnership = (period.accruedOwnership *\n portion) / (period.end - period.start);\n accountAccruedOwnership += portionOfOwnership;\n }\n }\n\n // ( ]( ]( timestamp ..... now\n // | latestAccrualTimestamp\n // if included period not cached - still ongoing\n if (\n accountInfo.latestAccrualTimestamp != 0 &&\n accountInfo.latestAccrualTimestamp < timestamp &&\n (periodIdx == type(uint256).max)\n ) {\n uint256 ownershipAccrued = calculateOwnershipForPeriod(\n restrictedLockupToken.balanceOf(account),\n accountInfo.latestAccrualTimestamp,\n timestamp\n );\n accountAccruedOwnership += ownershipAccrued;\n }\n\n return accountAccruedOwnership;\n }\n\n function totalOwnershipForPeriod(\n address token,\n uint256 startTimestamp,\n uint256 endTimestamp\n ) public view returns (uint256) {\n if (startTimestamp >= endTimestamp) {\n return 0;\n }\n\n return\n totalOwnershipAt(token, endTimestamp) -\n totalOwnershipAt(token, startTimestamp);\n }\n\n function ownershipForPeriod(\n address token,\n address account,\n uint256 startTimestamp,\n uint256 endTimestamp\n ) public view returns (uint256) {\n if (startTimestamp >= endTimestamp) {\n return 0;\n }\n\n return\n ownershipAt(token, account, endTimestamp) -\n ownershipAt(token, account, startTimestamp);\n }\n\n function addressInfo(\n address token,\n address account\n ) public view returns (WalletInfo memory) {\n return walletInfo[token][account];\n }\n\n function supplyInfo(address token) public view returns (WalletInfo memory) {\n return totalSupplyInfo[token];\n }\n\n function walletPeriodByIndex(\n address token,\n address account,\n uint256 index\n ) public view returns (Period memory) {\n return walletPeriods[token][account][index];\n }\n\n function totalSupplyPeriodByIndex(\n address token,\n uint256 index\n ) public view returns (Period memory) {\n return totalSupplyPeriods[token][index];\n }\n\n /**\n * @dev Find the period index for a wallet address that the nearest period that ends before the timestamp\n * if no period includes it\n * @param token The token address\n * @param account The wallet address to check\n * @param timestamp The timestamp to find the period for\n * @return periodIndex The index of the period that nearest to the timestamp,\n * or type(uint256).max if no suitable period is found\n */\n function findPeriodIndexForTimestamp(\n address token,\n address account,\n uint256 timestamp\n ) public view returns (uint256 periodIndex) {\n WalletInfo memory accountWalletInfo = addressInfo(token, account);\n\n // If no periods exist for this wallet, return max value to indicate not found\n if (accountWalletInfo.periodCount == 0) {\n return type(uint256).max;\n }\n\n // Binary search to efficiently find the period\n uint256 low = 0;\n uint256 high = accountWalletInfo.periodCount - 1;\n uint256 nearestLeftPeriod = type(uint256).max;\n\n while (low <= high) {\n uint256 mid = (low + high) / 2;\n Period memory currentPeriod = walletPeriods[token][account][mid];\n\n // Check if timestamp is in this period (timestamp > start && timestamp <= end)\n if (\n timestamp > currentPeriod.start &&\n timestamp <= currentPeriod.end\n ) {\n return mid > 0 ? mid - 1 : type(uint256).max; // return previous period\n }\n\n // If timestamp is before this period's start, search in the lower half\n if (timestamp <= currentPeriod.start) {\n // If we're at index 0, we can't go lower\n if (mid == 0) {\n break;\n }\n high = mid - 1;\n }\n // If timestamp is after this period's end, search in the upper half\n // and update the nearest left period if this period ends before the timestamp\n else {\n // This period ends before the timestamp, so it's a candidate for nearest left period\n if (\n currentPeriod.end < timestamp &&\n (nearestLeftPeriod == type(uint256).max ||\n currentPeriod.end >\n walletPeriods[token][account][nearestLeftPeriod].end)\n ) {\n nearestLeftPeriod = mid;\n }\n low = mid + 1;\n }\n }\n\n // If we found a nearest left period, return it\n if (nearestLeftPeriod != type(uint256).max) {\n return nearestLeftPeriod;\n }\n\n // If we get here, no period includes the timestamp and no nearest left period was found\n return type(uint256).max;\n }\n\n /**\n * @dev Find the period index for the total supply that ends before the timestamp\n * @param token The token address\n * @param timestamp The timestamp to find the period for\n * @return periodIndex The index of the previous period if timestamp is within a period,\n * or the nearest period that ends before the timestamp if no period includes it,\n * or type(uint256).max if no suitable period is found\n */\n function findTotalSupplyPeriodIndexForTimestamp(\n address token,\n uint256 timestamp\n ) public view returns (uint256 periodIndex) {\n // If no periods exist for total supply, return max value to indicate not found\n if (totalSupplyInfo[token].periodCount == 0) {\n return type(uint256).max;\n }\n\n // Binary search to efficiently find the period\n uint256 low = 0;\n uint256 high = totalSupplyInfo[token].periodCount - 1;\n uint256 nearestLeftPeriod = type(uint256).max;\n\n while (low <= high) {\n uint256 mid = (low + high) / 2;\n Period memory currentPeriod = totalSupplyPeriods[token][mid];\n\n // Check if timestamp is in this period (timestamp > start && timestamp <= end)\n if (\n timestamp > currentPeriod.start &&\n timestamp <= currentPeriod.end\n ) {\n return mid > 0 ? mid - 1 : type(uint256).max; // return previous period\n }\n\n // If timestamp is before this period's start, search in the lower half\n if (timestamp <= currentPeriod.start) {\n // If we're at index 0, we can't go lower\n if (mid == 0) {\n break;\n }\n high = mid - 1;\n }\n // If timestamp is after this period's end, search in the upper half\n // and update the nearest left period if this period ends before the timestamp\n else {\n // This period ends before the timestamp, so it's a candidate for nearest left period\n if (\n currentPeriod.end < timestamp &&\n (nearestLeftPeriod == type(uint256).max ||\n currentPeriod.end >\n totalSupplyPeriods[token][nearestLeftPeriod].end)\n ) {\n nearestLeftPeriod = mid;\n }\n low = mid + 1;\n }\n }\n\n // If we found a nearest left period, return it\n if (nearestLeftPeriod != type(uint256).max) {\n return nearestLeftPeriod;\n }\n\n // If we get here, no period includes the timestamp and no nearest left period was found\n return type(uint256).max;\n }\n\n function walletPeriodCount(\n address token,\n address account\n ) public view returns (uint256) {\n return walletInfo[token][account].periodCount;\n }\n\n function totalSupplyPeriodCount(\n address token\n ) public view returns (uint256) {\n return totalSupplyInfo[token].periodCount;\n }\n\n function totalSupplyPeriodAccruedOwnership(\n address token,\n uint256 index\n ) public view returns (uint256) {\n return totalSupplyPeriods[token][index].totalAccruedOwnership;\n }\n\n function walletPeriodAccruedOwnership(\n address token,\n address account,\n uint256 index\n ) public view returns (uint256) {\n return walletPeriods[token][account][index].totalAccruedOwnership;\n }\n}\n"},"contracts/Storage.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {ITransferRules} from \"./interfaces/ITransferRules.sol\";\nimport {ISnapshotPeriods} from \"./interfaces/ISnapshotPeriods.sol\";\nimport {IIdentityRegistry} from \"./interfaces/IIdentityRegistry.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport {BitManipulationLib} from \"./libraries/BitManipulationLib.sol\";\nimport {EligibilityCacheLib, ELIGIBILITY_CACHE_SLOTS} from \"./libraries/EligibilityCacheLib.sol\";\nimport \"./AccessControlErrors.sol\";\n\n/**\n * @title Storage\n * @notice Shared storage contract for RestrictedLockupToken and its extensions\n * @dev Both main contract and extensions inherit from this to ensure identical storage layout\n */\ncontract Storage {\n using BitManipulationLib for uint256;\n\n struct ReleaseSchedule {\n uint256 releaseCount;\n uint256 delayUntilFirstReleaseInSeconds;\n uint256 initialReleasePortionInBips;\n uint256 periodBetweenReleasesInSeconds;\n }\n\n struct Timelock {\n address funder;\n uint256 scheduleId;\n uint256 commencementTimestamp;\n uint256 tokensTransferred;\n uint256 totalAmount;\n uint256 globalHoldingIndex; // Global index for token type + daysAfterDeployment combination\n address[] cancelableBy; // not cancelable unless set at the time of funding\n }\n\n /**\n * @dev Everything a loop needs to decide whether holdings may move to one recipient: the\n * recipient's identity, its AML/KYC status, and a per-token-type cache of the collapsed\n * transfer rules (see EligibilityCacheLib). Bundled so the loop carries one stack slot.\n * Build with _recipientEligibility, query as described on _resolveEligibility.\n */\n struct RecipientEligibility {\n IIdentityRegistry.IdentityInfo identity;\n bool isAmlKycPassed;\n uint256[ELIGIBILITY_CACHE_SLOTS] cache;\n }\n\n // ============================================\n // CORE CONTRACT REFERENCES\n // ============================================\n ITransferRules public transferRules;\n ISnapshotPeriods public snapshotPeriods;\n IIdentityRegistry public identityRegistry;\n address public restrictedLockupTokenManagementExtension;\n address public restrictedLockupTokenExtension;\n IAccessControl public accessControl; // external access control contract\n\n // ============================================\n // RELEASE SCHEDULES AND TIMELOCKS\n // ============================================\n ReleaseSchedule[] public releaseSchedules;\n // wallet address => timelock details\n mapping(address => Timelock[]) internal timelocks;\n uint256 public minTimelockAmount;\n bool public snapshotsEnabled;\n bool public recordMintTimestamp;\n uint256 public recordPausedOnDaysAfterDeployment;\n\n // ============================================\n // HOLDER MANAGEMENT STATE\n // ============================================\n uint256 internal holderIds; // starts at 1 for 1st holder\n uint256 public holderMax = 2 ** 255 - 1; // Maximum uint256 value - walletsMax\n uint256 public holderCount;\n // wallet address => holderId\n mapping(address => uint256) internal holderIdByAddress;\n // holderId => wallet addresses\n mapping(uint256 => address[]) internal walletAddressesByHolderId;\n\n // ============================================\n // TOKEN STATE\n // ============================================\n bool public isPaused;\n // address => isFrozen\n mapping(address => bool) internal frozenAddresses;\n uint256 public maxTotalSupply;\n \n // ============================================\n // SLOT CONFIGURATION\n // ============================================\n /// @dev Number of slots per 256-bit word, calculated based on maxTotalSupply and decimals\n /// @dev Determines how many balance values can fit in a single storage slot\n uint256 public immutable slotsPerWord;\n \n /// @dev Number of bits per element (tokenType + daysAfterDeployment) or balance\n /// @dev Calculated as 256 / slotsPerWord\n uint256 public immutable elementBitSize;\n\n uint256 public immutable maxBalancePerSubIndex;\n\n // ============================================\n // MULTI-TYPE TOKEN FUNCTIONALITY (OPTIMIZED)\n // ============================================\n \n // Deployment day rounded up to midnight for calculating days since deploy\n uint256 public deploymentDay;\n \n // Global registry of packed 256-bit structures, each containing slotsPerWord elements\n // Each uint256 stores slotsPerWord elements of: [8 bits: tokenType][elementBitSize-8 bits: daysAfterDeployment]\n // This creates direct 1:1 mapping to packedBalancesByTypeAndTime storage slots\n uint256[] public globalMintTimestamps;\n \n // Count of elements in globalMintTimestamps (for efficient validation)\n uint256 public mintTimestampCount;\n \n // Wallet ownership bitmaps: address => uint256[] (each uint256 = 256 bits)\n // Each bit represents ownership of a global TokenTypeMintTimestamp combination\n mapping(address => uint256[]) internal walletIndexesByType;\n \n // ============================================\n // PACKED BALANCE STORAGE OPTIMIZATION\n // ============================================\n\n // Packed balance storage: address => packed TokenTypeMintTimestamp (without subIndex) => packed balances\n // Each uint256 stores slotsPerWord balances for example for 8 elements\n // (uint32 each): [bal7][bal6][bal5][bal4][bal3][bal2][bal1][bal0]\n mapping(address => mapping(uint256 => uint256)) internal packedBalancesByTypeAndTime;\n \n // Overflow balance storage for amounts > uint32..64 max\n // Used when packed balance == maxBalancePerSubIndex (uint32..64 max)\n // Mapping: address => globalIndex => actual overflow amount\n mapping(address => mapping(uint256 => uint256)) internal overflowBalances;\n\n // Global registry of all token types that have been used\n uint256[] internal _existingTokenTypes;\n mapping(uint256 => bool) internal _tokenTypeExists;\n mapping(uint256 => uint256) internal _tokenTypeIndex; // tokenType => index in _existingTokenTypes array\n\n // ============================================\n // FALLBACK PATTERN STATE\n // ============================================\n\n uint256 public maxReleaseDelay;\n\n // ============================================\n // STANDARDS EXTENSION STATE (EIP-2612 and future standards)\n // ============================================\n // Delegated extension that implements permit/nonces/DOMAIN_SEPARATOR (and future\n // EIP standard surfaces) via the fallback.\n address public restrictedLockupTokenStandardsExtension;\n // Per-owner EIP-2612 permit nonce (read & post-incremented inside the standards extension).\n mapping(address => uint256) internal _permitNonces;\n\n // ============================================\n // GLOBAL REGISTRY ORDERING STATE\n // ============================================\n // Highest daysAfterDeployment ever appended to globalMintTimestamps.\n uint256 internal highestAppendedDays;\n // False while globalMintTimestamps is sorted non-decreasing by daysAfterDeployment,\n // which lets _findOrCreateOptimizedGlobalIndex stop its backward search at the first\n // older element. Minting is inherently chronological, but updateHoldingTokenType and\n // updateTimelockTokenType re-insert an existing holding's older day at the tail; once\n // that happens the ordering assumption no longer holds and the search must be\n // exhaustive, otherwise an existing combination is missed and a duplicate global index\n // is allocated. Latches true and is never cleared.\n // Public because this is a one-way switch that permanently raises the gas cost of\n // allocating a new (tokenType, day) index: operators need to see that it happened.\n bool public registryUnordered;\n\n uint8 public constant MAX_CANCELABLE_BY = 10;\n uint256 internal constant MAX_TIMELOCKS = 10_000;\n uint256 public constant MAX_WALLETS_PER_HOLDER = 10;\n // 5.1.3: ITransferRules gained eligibilityThresholdFor, so its ERC165 interface ID changed and\n // transfer-rules contracts built against 5.1.2 are rejected by upgradeTransferRules.\n string public constant contractVersion = \"5.1.3\";\n bytes4 public constant _ITRANSFER_RULES_INTERFACE_ID =\n type(ITransferRules).interfaceId;\n bytes4 public constant _IIDENTITY_REGISTRY_INTERFACE_ID =\n type(IIdentityRegistry).interfaceId;\n\n // ============================================\n // EVENTS\n // ============================================\n\n // Multi-type token events\n event TokenTypeMinted(\n address indexed to,\n uint256 amount,\n uint256 indexed tokenType,\n uint256 timestamp\n );\n\n event TokenTypeTransferred(\n address indexed from,\n address indexed to,\n uint256 amount,\n uint256 indexed tokenType\n );\n\n event TokenTypeBurned(\n address indexed from,\n uint256 amount,\n uint256 indexed tokenType\n );\n\n /// @notice Emitted once, when an element is appended to globalMintTimestamps out of\n /// chronological order. From this point index allocation scans the registry\n /// exhaustively, which permanently increases the gas cost of registering a new\n /// (tokenType, daysAfterDeployment) combination.\n /// @param appendedDays The out-of-order daysAfterDeployment that was appended\n /// @param highestDays The highest daysAfterDeployment appended before this one\n event GlobalRegistryBecameUnordered(uint256 appendedDays, uint256 highestDays);\n\n event TokenHoldingsMerged(\n address indexed wallet,\n uint256 indexed tokenType,\n uint256 holdingsMerged,\n uint256 totalAmount,\n uint256 newMintTimestamp\n );\n\n /// @notice This event is emitted when a schedule is funded\n /// @param from The address of the funder\n /// @param to The address of the recipient of the tokens when schedule is released\n /// @param scheduleId The id of the release schedule\n /// @param amount The amount of tokens to be released\n /// @param commencementTimestamp The timestamp when the release schedule starts\n /// @param timelockId The id of the timelock\n /// @param cancelableBy The addresses that can cancel the timelock\n event ScheduleFunded(\n address indexed from,\n address indexed to,\n uint256 indexed scheduleId,\n uint256 amount,\n uint256 commencementTimestamp,\n uint256 timelockId,\n address[] cancelableBy\n );\n\n /// @notice This event is emitted when a timelock is canceled\n /// @param canceledBy The address of the canceller\n /// @param target The address of the recipient of the unlocked tokens\n /// @param timelockIndex The index of the timelock in the timelocks array\n /// @param reclaimTokenTo The address of the recipient of the locked tokens\n /// @param canceledAmount The total amount of canceled tokens\n /// @param paidAmount The amount of paid tokens to target\n event TimelockCanceled(\n address indexed canceledBy,\n address indexed target,\n uint256 indexed timelockIndex,\n address reclaimTokenTo,\n uint256 canceledAmount,\n uint256 paidAmount\n );\n\n /// @notice This event is emitted when a transfer rules is updated\n /// @param admin The address of the admin\n /// @param oldRules The old transfer rules contract address\n /// @param newRules The new transfer rules contract address\n event Upgrade(\n address indexed admin,\n address indexed oldRules,\n address indexed newRules\n );\n\n /// @notice This event is emitted when a force transfer between wallets is executed by admin\n /// @param admin The address of the admin\n /// @param from The address of the sender\n /// @param to The address of the recipient\n /// @param amount The amount of tokens to transfer\n event ForceTransferBetween(\n address indexed admin,\n address indexed from,\n address indexed to,\n uint256 amount\n );\n\n /// @notice This event is emitted when an updated max total supply\n /// @param admin The address of the admin\n /// @param maxTotalSupply The new max total supply\n event SetMaxTotalSupply(address indexed admin, uint256 maxTotalSupply);\n\n /// @notice This event is emitted when a holder is created\n /// @param holderId The id of the holder\n /// @param addr The primary address of the holder\n event HolderCreated(uint256 indexed holderId, address indexed addr);\n\n /// @notice This event is emitted when an append new wallet address to existing holder\n /// @param addr The address of the wallet\n /// @param holderId The id of the holder\n event AppendHolderAddress(address indexed addr, uint256 indexed holderId);\n\n /// @notice This event is emitted when an updated holder max limit\n /// @param admin The address of the admin\n /// @param holderMax The new holder max limit\n event SetHolderMax(address indexed admin, uint256 holderMax);\n\n /// @notice This event is emitted when a holder is removed\n /// @param admin The address of the admin\n /// @param holderId The id of the holder\n event RemoveHolder(address indexed admin, uint256 indexed holderId);\n\n /// @notice This event is emitted when a wallet address is removed from a given holderId\n /// @param admin The address of the tx sender\n /// @param holderId The id of the holder\n /// @param addr The primary address of the holder to be removed\n event RemoveWalletFromHolder(\n address indexed admin,\n uint256 indexed holderId,\n address indexed addr\n );\n\n /// @notice This event is emitted when a new release schedule is created\n /// @param from The address of the schedule creator\n /// @param scheduleId The id of the new release schedule\n event ScheduleCreated(address indexed from, uint256 indexed scheduleId);\n\n /// @notice This event is emitted when a holding's token type is updated\n /// @param admin The address of the admin performing the update\n /// @param wallet The wallet address whose holding is being updated\n /// @param oldTokenType The previous token type\n /// @param newTokenType The new token type\n /// @param mintTimestamp The mint timestamp of the holding\n /// @param amount The amount of tokens in the holding\n event HoldingTokenTypeUpdated(\n address indexed admin,\n address indexed wallet,\n uint256 indexed oldTokenType,\n uint256 newTokenType,\n uint256 mintTimestamp,\n uint256 amount\n );\n\n /// @notice This event is emitted when a timelock's token type is updated\n /// @param admin The address of the admin performing the update\n /// @param wallet The wallet address whose timelock is being updated\n /// @param timelockIndex The index of the timelock\n /// @param oldTokenType The previous token type\n /// @param newTokenType The new token type\n /// @param amount The total amount of tokens in the timelock\n event TimelockTokenTypeUpdated(\n address indexed admin,\n address indexed wallet,\n uint256 indexed timelockIndex,\n uint256 oldTokenType,\n uint256 newTokenType,\n uint256 amount\n );\n\n /// @notice This event is emitted when a address is frozen or unfrozen\n /// @param admin The address of the admin\n /// @param addr The (un)frozen address\n /// @param status true - frozen, false - unfrozen\n event AddressFrozen(\n address indexed admin,\n address indexed addr,\n bool indexed status\n );\n\n /// @notice This event is emitted when a contract is paused or unpaused\n /// @param admin The address of the admin\n /// @param status true - paused, false - unpaused\n event Pause(address indexed admin, bool indexed status);\n\n // ============================================\n // TOKENTYPE-TIMESTAMP PACKING UTILITIES\n // ============================================\n \n // Constants moved to BitManipulationLib - keeping references for compatibility\n uint256 internal constant TOKEN_TYPE_BITS = BitManipulationLib.TOKEN_TYPE_BITS;\n uint256 internal constant TOKEN_TYPE_MASK = BitManipulationLib.TOKEN_TYPE_MASK;\n \n // ============================================\n // WRAPPER FUNCTIONS REMOVED - USE BitManipulationLib DIRECTLY\n // ============================================\n \n /**\n * @dev Get token type from global index\n * @param globalIndex The global index\n * @return tokenType The token type\n */\n function _getTokenTypeFromGlobalIndex(uint256 globalIndex) internal view returns (uint256 tokenType) {\n uint256 packed = globalMintTimestamps[globalIndex / slotsPerWord];\n uint256 element = BitManipulationLib.getPackedElement(packed, globalIndex % slotsPerWord, elementBitSize);\n (tokenType, ) = BitManipulationLib.unpackElement(element);\n }\n \n /**\n * @dev Get token type and days-after-deployment from global index. Holdings are keyed by\n * day, so callers that only need to compare against a day threshold use this and skip\n * the timestamp conversion.\n * @param globalIndex The global index\n * @return tokenType The token type\n * @return daysAfterDeployment The recorded day, relative to deploymentDay\n */\n function _getTokenTypeAndDaysFromGlobalIndex(uint256 globalIndex) internal view returns (uint256 tokenType, uint256 daysAfterDeployment) {\n uint256 packed = globalMintTimestamps[globalIndex / slotsPerWord];\n uint256 element = BitManipulationLib.getPackedElement(packed, globalIndex % slotsPerWord, elementBitSize);\n (tokenType, daysAfterDeployment) = BitManipulationLib.unpackElement(element);\n }\n\n /**\n * @dev Get token type and mint timestamp from global index\n * @param globalIndex The global index\n * @return tokenType The token type\n * @return mintTimestamp The mint timestamp\n */\n function _getTokenTypeAndMintTimestampFromGlobalIndex(uint256 globalIndex) internal view returns (uint256 tokenType, uint256 mintTimestamp) {\n uint256 packed = globalMintTimestamps[globalIndex / slotsPerWord];\n uint256 element = BitManipulationLib.getPackedElement(packed, globalIndex % slotsPerWord, elementBitSize);\n uint256 daysAfterDeployment;\n (tokenType, daysAfterDeployment) = BitManipulationLib.unpackElement(element);\n mintTimestamp = BitManipulationLib.daysSinceDeployToTimestamp(daysAfterDeployment, deploymentDay);\n }\n \n // ============================================\n // RESTRICTED LOCKUP TOKEN ERRORS\n // ============================================\n \n error RestrictedLockupToken_HolderAlreadyExists();\n error RestrictedLockupToken_AddressAlreadyHasHolder();\n error RestrictedLockupToken_MaxHolderAddressesReached();\n error RestrictedLockupToken_NewHolderMaxMustExceedCurrentHolderCount();\n error RestrictedLockupToken_MaxHolderCountReached();\n error RestrictedLockupToken_HolderAddressDoesNotExist();\n error RestrictedLockupToken_HolderDoesNotExist();\n error RestrictedLockupToken_InvalidName();\n error RestrictedLockupToken_InvalidSymbol();\n error RestrictedLockupToken_InvalidTransferRules();\n error RestrictedLockupToken_InvalidAccessControl();\n error RestrictedLockupToken_InvalidMinTimelockAmount();\n error RestrictedLockupToken_InvalidNumberOfRelases();\n error RestrictedLockupToken_InvalidRInitialReleaseBIPS();\n error RestrictedLockupToken_InvalidReleasePeriod();\n error RestrictedLockupToken_InvalidReleasePortionForSingleRelease();\n error RestrictedLockupToken_RecipientAndAmountLengthsShouldMatch();\n error RestrictedLockupToken_NewTransferRulesContractDoesNotImplementITransferRules();\n error RestrictedLockupToken_NewIdentityRegistryContractDoesNotImplementIIdentityRegistry();\n error RestrictedLockupToken_MismatchedArrayLength();\n error RestrictedLockupToken_SenderCannotBeRecipient(address sender);\n error RestrictedLockupToken_InvalidAmount();\n error RestrictedLockupToken_InvalidZeroAddress();\n error RestrictedLockupToken_CannotExceedMaxTotalSupply();\n error RestrictedLockupToken_MaxCancelersExceeded();\n error RestrictedLockupToken_InvalidAddressArray();\n error RestrictedLockupToken_NewMaxTotalSupplyMustExceedCurrentTotalSupply();\n error RestrictedLockupToken_SafeApprove();\n error RestrictedLockupToken_InvalidFirstRelease();\n error RestrictedLockupToken_InvalidTimelock();\n error RestrictedLockupToken_InvalidReclaimTo();\n error RestrictedLockupToken_ReclaimToFrozen(address reclaimTokenTo);\n error RestrictedLockupToken_TransfersPaused();\n error RestrictedLockupToken_TimelockCannotBeCanceled();\n error RestrictedLockupToken_ScheduleIdDoesNotMatch();\n error RestrictedLockupToken_CommencementTimestampDoesNotMatch();\n error RestrictedLockupToken_TotalAmountDoesNotMatch();\n error RestrictedLockupToken_TimelockHasNoValueRemaining();\n error RestrictedLockupToken_AmountExceedsUnlockedBalance();\n error RestrictedLockupToken_InsufficientTotalBalanceOf();\n error RestrictedLockupToken_NoHoldingWithEnoughBalance();\n error RestrictedLockupToken_MaxTimelocksExceeded();\n error RestrictedLockupToken_InvalidFundAmount();\n error RestrictedLockupToken_InvalidFundAddress();\n error RestrictedLockupToken_InvalidScheduleId();\n error RestrictedLockupToken_AmountLessThanReleaseCount();\n error RestrictedLockupToken_InitialReleaseOutOfRange();\n error RestrictedLockupToken_CannotRemoveLastWallet();\n error RestrictedLockupToken_InvalidTrustedForwarder();\n error RestrictedLockupToken_InvalidIdentityRegistry();\n error RestrictedLockupToken_ExtensionSlotsPerWordMismatch(uint256 expected, uint256 actual);\n error RestrictedLockupToken_InvalidRestrictedLockupTokenExtension();\n error RestrictedLockupToken_InvalidRestrictedLockupTokenManagementExtension();\n error RestrictedLockupToken_InvalidRestrictedLockupTokenStandardsExtension();\n error RestrictedLockupToken_CannotRemoveHolderWithActiveTimelocks();\n error RestrictedLockupToken_InsufficientBurnableBalance();\n error RestrictedLockupToken_CantVestAllForMultipleReleases();\n error RestrictedLockupToken_InvalidTokenType();\n error RestrictedLockupToken_NoItemWithEnoughBalance();\n error RestrictedLockupToken_HoldingIndexOutOfBound();\n error RestrictedLockupToken_InvalidHolding();\n error RestrictedLockupToken_InvalidHoldingIndex();\n error RestrictedLockupToken_InvalidZeroAmount();\n error RestrictedLockupToken_MaxTotalSupplyTooLarge();\n error RestrictedLockupToken_TokenTypeNotAllowedForRecipient();\n error RestrictedLockupToken_AlreadySet();\n error RestrictedLockupToken_BalanceCheckFailed();\n\n // ============================================\n // INTERNAL FUNCTIONS\n // ============================================\n \n /**\n * @dev This function gives the maximum permitted value of `maxTotalSupply` in base units.\n * The limit keeps each product in SnapshotPeriods less than 2^256.\n *\n * SnapshotPeriods multiplies a quantity of tokens by a time. In this description, S is\n * the total supply and D is the length of a period in seconds. The balance of a wallet\n * is never more than S. SnapshotPeriods calculates these three products:\n *\n * - S * D. The function `calculateOwnershipForPeriod` calculates this product. The\n * branches that accrue to the current time also calculate this product.\n * - The sum of the S * D products for the full life of the token. The field\n * `totalAccruedOwnership` holds this sum. The sum is not more than S * now.\n * - S * D^2. The functions `ownershipAt` and `totalOwnershipAt` calculate this product.\n * They multiply the `accruedOwnership` of a period, which is S * D, by a portion of\n * that period. The portion is not more than D. Then they divide the result by D.\n *\n * D and `now` are unix timestamps. Therefore both values stay less than 2^32 until\n * 2106-02-07. The third product is the largest product. S * 2^64 must stay less than\n * 2^256. Therefore S must stay less than 2^192.\n *\n * At the limit of 2^192 - 1, S * 2^32 is 2^223 and S * 2^64 is 2^255. Both values are\n * less than 2^256.\n *\n * The constructor and `setMaxTotalSupply` apply this limit. Each deployment does this\n * one time. Therefore the limit adds no gas to `onUpdate` and no gas to `ownershipAt`.\n * These two functions operate for each transfer and for each claim of interest. An\n * overflow guard inside SnapshotPeriods adds gas to each of these operations.\n *\n * The limit of 2^192 - 1 is equal to 6.3e39 tokens at 18 decimals. A security token does\n * not need a supply of this size.\n */\n function _maxSafeSupply() internal pure returns (uint256) {\n return type(uint256).max >> 64; // 2^192 - 1\n }\n\n /**\n * @dev Find or create optimized global index for TokenType + DaysAfterDeployment\n * @param tokenType The token type (0-255)\n * @param daysAfterDeployment The days after deployment\n * @return globalIndex The global index for this combination\n */\n function _findOrCreateOptimizedGlobalIndex(uint256 tokenType, uint256 daysAfterDeployment) internal returns (uint256) {\n // Create element for this token type and days\n uint256 element = BitManipulationLib.packElement(tokenType, daysAfterDeployment, elementBitSize);\n \n // Search from end to beginning to find same token type and daysAfterDeployment.\n // While the registry is still sorted by day (registryUnordered == false) the scan can\n // stop at the first chronologically older element. That shortcut is only valid under\n // that invariant: updateHoldingTokenType / updateTimelockTokenType re-insert an\n // existing holding's older day at the tail, and terminating early past such an\n // element would miss an already-registered combination and allocate a duplicate\n // global index for it.\n // Use direct bit manipulation instead of unpacking to save gas\n bool canBreakEarly = !registryUnordered;\n for (uint256 bucketIndex = globalMintTimestamps.length; bucketIndex > 0; bucketIndex--) {\n uint256 actualBucketIndex = bucketIndex - 1; // Convert to 0-based index\n uint256 packed = globalMintTimestamps[actualBucketIndex];\n \n // Check each element in this bucket (reverse order for better efficiency)\n // Work directly with packed bits instead of unpacking\n for (uint256 subIndex = slotsPerWord; subIndex > 0; subIndex--) {\n uint256 actualSubIndex = subIndex - 1; // Convert to 0-based index\n \n // Extract element directly from packed data using bit shifting\n uint256 currentElement = (packed >> (actualSubIndex * elementBitSize)) & ((1 << elementBitSize) - 1);\n \n if (currentElement == 0) {\n continue; // Skip empty slots\n }\n \n if (currentElement == element) {\n // Found existing combination - return its global index\n return actualBucketIndex * slotsPerWord + actualSubIndex;\n }\n\n // Extract daysAfterDeployment directly from current element using bit\n // manipulation. Days are stored with a +1 offset, so compare against the\n // encoded form of the target.\n uint256 currentDays = currentElement >> TOKEN_TYPE_BITS;\n\n // Entries older than the target cannot match, and while the registry is\n // ordered every remaining entry is older still.\n if (canBreakEarly && currentDays < (daysAfterDeployment + 1)) {\n bucketIndex = 1; // Set to 1 so that outer for loop will terminate after decrement\n break; // No point searching further, older entries won't match\n }\n }\n }\n\n // Not found, so a new element is about to be appended. Maintain the ordering\n // invariant that the early-break above depends on.\n if (daysAfterDeployment < highestAppendedDays) {\n if (!registryUnordered) {\n registryUnordered = true;\n emit GlobalRegistryBecameUnordered(daysAfterDeployment, highestAppendedDays);\n }\n } else if (daysAfterDeployment > highestAppendedDays) {\n highestAppendedDays = daysAfterDeployment;\n }\n\n // Element not found, add new item to the last available subindex or create new bucket\n if (globalMintTimestamps.length > 0) {\n // Try to add to the last bucket first\n uint256 lastBucketIndex = globalMintTimestamps.length - 1;\n uint256 packed = globalMintTimestamps[lastBucketIndex];\n \n // Find the first available slot in the last bucket using direct bit manipulation\n for (uint256 subIndex = 0; subIndex < slotsPerWord; subIndex++) {\n // Extract element directly from packed data\n uint256 currentElement = (packed >> (subIndex * elementBitSize)) & ((1 << elementBitSize) - 1);\n \n if (currentElement == 0) {\n // Found empty slot in last bucket, use it\n // Set the element directly in the packed data using bit manipulation\n uint256 updatedPacked = packed | (element << (subIndex * elementBitSize));\n globalMintTimestamps[lastBucketIndex] = updatedPacked;\n mintTimestampCount++;\n return lastBucketIndex * slotsPerWord + subIndex;\n }\n }\n }\n \n // Last bucket is full or no buckets exist, create new bucket with subindex 0\n // Use direct bit manipulation instead of array creation\n uint256 newBucketIndex = globalMintTimestamps.length;\n uint256 newPacked = element; // element goes to subIndex 0, all other slots are 0\n globalMintTimestamps.push(newPacked);\n mintTimestampCount++;\n \n return newBucketIndex * slotsPerWord; // subIndex = 0 for first element\n }\n \n /**\n * @dev Set a bit in the wallet's bitmap\n * @param account The wallet address\n * @param globalIndex The global index to set\n */\n function _setBitmapBit(address account, uint256 globalIndex) internal {\n uint256 wordIndex = globalIndex / 256;\n uint256 bitIndex = globalIndex % 256;\n \n // Ensure bitmap array is large enough\n while (walletIndexesByType[account].length <= wordIndex) {\n walletIndexesByType[account].push(0);\n }\n \n // Set the bit\n walletIndexesByType[account][wordIndex] |= (1 << bitIndex);\n }\n\n /**\n * @dev Register a token type in the global registry if it doesn't exist\n * @param tokenType The token type to register\n */\n function _registerTokenType(uint256 tokenType) internal {\n if (!_tokenTypeExists[tokenType]) {\n _tokenTypeIndex[tokenType] = _existingTokenTypes.length;\n _existingTokenTypes.push(tokenType);\n _tokenTypeExists[tokenType] = true;\n }\n }\n /**\n * @dev Adds a specified amount to the holdings of a given account at a specific global holding index.\n * Handles packed storage and overflow for balances exceeding the maximum per sub-index.\n * @param account The address of the account to update holdings for\n * @param globalHoldingIndex The global index representing the token type and mint timestamp\n * @param amount The amount to add to the holdings\n */\n function _addToHoldingsByIndex(\n address account,\n uint256 globalHoldingIndex,\n uint256 amount\n ) internal {\n // Calculate subIndex from globalHoldingIndex\n uint256 subIndex = globalHoldingIndex % slotsPerWord;\n \n // Calculate bucket index (which globalMintTimestamps entry to use)\n uint256 bucketIndex = globalHoldingIndex / slotsPerWord;\n \n // Get current packed balances for this bucket\n uint256 currentPackedBalances = packedBalancesByTypeAndTime[account][bucketIndex];\n \n // Get current balance for this subIndex\n uint256 currentBalance = BitManipulationLib.getPackedBalance(currentPackedBalances, subIndex, elementBitSize);\n \n // Handle overflow balances: if current balance is max uint32, get real balance from overflow mapping\n uint256 realCurrentBalance = currentBalance;\n if (currentBalance == maxBalancePerSubIndex) {\n realCurrentBalance = overflowBalances[account][globalHoldingIndex];\n }\n \n // If this combination doesn't exist for this wallet yet, set bitmap bit\n if (currentBalance == 0) {\n // Set bitmap bit for this wallet\n _setBitmapBit(account, globalHoldingIndex);\n }\n \n // Calculate new total balance\n uint256 newTotalBalance = realCurrentBalance + amount;\n \n // Update balance in packed storage and overflow mapping as needed\n if (newTotalBalance < maxBalancePerSubIndex) {\n // Balance fits in uint32, store in packed storage\n packedBalancesByTypeAndTime[account][bucketIndex] = BitManipulationLib.setPackedBalance(\n currentPackedBalances,\n subIndex,\n newTotalBalance,\n elementBitSize\n );\n // Clear any existing overflow balance\n if (currentBalance == maxBalancePerSubIndex) {\n delete overflowBalances[account][globalHoldingIndex];\n }\n } else {\n // Balance exceeds uint32 max, store max in packed storage and real amount in overflow mapping\n packedBalancesByTypeAndTime[account][bucketIndex] = BitManipulationLib.setPackedBalance(\n currentPackedBalances,\n subIndex,\n maxBalancePerSubIndex,\n elementBitSize\n );\n overflowBalances[account][globalHoldingIndex] = newTotalBalance;\n }\n }\n\n /**\n * @dev Add tokens to holdings structure using optimized packed balance system\n * @param account The account to add tokens to\n * @param tokenType The type of tokens (0-255, uses 8 bits)\n * @param daysAfterDeployment The days after deployment of the tokens\n * @param amount The amount to add\n * @return globalIndex The global index for this token type + daysAfterDeployment combination\n */\n function _addToHoldingsByType(\n address account,\n uint256 tokenType,\n uint256 daysAfterDeployment,\n uint256 amount\n ) internal returns (uint256 globalIndex) {\n // Register token type globally if it doesn't exist\n _registerTokenType(tokenType);\n \n // Find or create the global index for this combination\n globalIndex = _findOrCreateOptimizedGlobalIndex(\n tokenType,\n daysAfterDeployment\n );\n \n _addToHoldingsByIndex(account, globalIndex, amount);\n return globalIndex;\n }\n\n /**\n * @dev Get the real balance for a specific holding, considering overflow storage\n * @param account The account to check\n * @param globalIndex The global index of the holding\n * @return balance The real balance (from packed storage or overflow mapping)\n */\n function _getRealBalance(address account, uint256 globalIndex) internal view returns (uint256) {\n uint256 subIndex = globalIndex % slotsPerWord;\n uint256 bucketIndex = globalIndex / slotsPerWord;\n \n uint256 currentPackedBalances = packedBalancesByTypeAndTime[account][bucketIndex];\n uint256 packedBalance = BitManipulationLib.getPackedBalance(currentPackedBalances, subIndex, elementBitSize);\n \n // If packed balance is max uint32, get real balance from overflow mapping\n if (packedBalance == maxBalancePerSubIndex) {\n return overflowBalances[account][globalIndex];\n }\n \n return packedBalance;\n }\n\n /**\n * @dev Clear a bit in the wallet's bitmap for a token type\n * @param account The wallet address\n * @param globalIndex The global index to clear\n */\n function _clearBitmapBit(address account, uint256 globalIndex) internal {\n uint256 wordIndex = globalIndex / 256;\n uint256 bitIndex = globalIndex % 256;\n \n if (wordIndex < walletIndexesByType[account].length) {\n walletIndexesByType[account][wordIndex] &= ~(1 << bitIndex);\n }\n }\n\n\n /**\n * @dev Enforces transfer restrictions managed using the ERC-1404 standard functions.\n * The TransferRules contract defines what the rules are. The data inputs to those rules remains in the RestrictedToken contract.\n * TransferRules is a separate contract so its logic can be upgraded.\n * @param from_ The address the tokens are transferred from\n * @param to_ The address the tokens would be transferred to\n * @param value_ the quantity of tokens to be transferred\n */\n function _enforceTransferRestrictions(\n address from_,\n address to_,\n uint256 value_\n ) internal view {\n uint256 _restrictionCode = transferRules.detectTransferRestrictionBasic(\n address(this),\n from_,\n to_,\n value_\n );\n // if (!transferRules.checkSuccess(_restrictionCode)) revert TransferRestricted(_restrictionCode);\n require(\n transferRules.checkSuccess(_restrictionCode),\n transferRules.messageForTransferRestriction(_restrictionCode)\n );\n }\n\n function _enforceTransferRestrictionsFor(\n uint256 tokenType_,\n uint256 mintTimestamp_,\n address recipient_\n ) internal view {\n IIdentityRegistry.IdentityInfo memory recipientIdentity = identityRegistry.identity(recipient_);\n bool isAmlKycPassed_ = identityRegistry.isAmlKycPassed(recipient_);\n uint256 _restrictionCode = transferRules.detectTransferRestrictionForHolding(\n tokenType_,\n mintTimestamp_,\n recipientIdentity,\n isAmlKycPassed_\n );\n require(\n transferRules.checkSuccess(_restrictionCode),\n transferRules.messageForTransferRestriction(_restrictionCode)\n );\n }\n\n /**\n * @dev Loads the recipient-side inputs for eligibility checks. The cache starts empty and is\n * filled lazily by _resolveEligibility.\n */\n function _recipientEligibility(address recipient_) internal view returns (RecipientEligibility memory recipient) {\n recipient.identity = identityRegistry.identity(recipient_);\n recipient.isAmlKycPassed = identityRegistry.isAmlKycPassed(recipient_);\n }\n\n /**\n * @dev Resolves whether holdings of `tokenType_` may move to `recipient` and caches the\n * answer in `recipient.cache`, returning the encoded cache entry.\n *\n * This is the cache-miss half of the per-holding eligibility check. The hit half is\n * written out at each hot loop (transfer engine, ERC-1404 preview, findHoldingFor,\n * timelock walk) - directly, or in a single-call-site helper the optimizer inlines -\n * because a shared helper call per holding costs more than the check itself:\n *\n * uint256 entry = EligibilityCacheLib.entryFor(recipient.cache, tokenType);\n * if (!EligibilityCacheLib.isHit(entry, tokenType)) entry = _resolveEligibility(recipient, tokenType);\n * if (!EligibilityCacheLib.admits(entry, daysAfterDeployment)) continue;\n *\n * `admits(entry, day)` is then exactly \"transferRules.detectTransferRestrictionForHolding\n * would return SUCCESS for this holding\". ITransferRules.eligibilityThresholdFor collapses\n * the recipient's per-region rules into one mint-timestamp threshold; a holding recorded\n * on day `d` was minted at `deploymentDay + d * 1 days`, so it satisfies\n * `mintTimestamp <= maxEligibleMintTimestamp` iff\n * `d <= (maxEligibleMintTimestamp - deploymentDay) / 1 days`, and a threshold before\n * the deployment day admits no day at all. Storing the last eligible *day* makes the hit\n * path a single comparison with no timestamp arithmetic and no storage reads.\n *\n * The write to the caller's memory cache is the function's one side effect; it touches\n * no storage. Loops therefore pay one external call per distinct token type instead of\n * one per item, which is what keeps transfers, the preview, findHoldingFor and the\n * timelock walk inside the block gas limit for wallets with thousands of holdings.\n */\n function _resolveEligibility(\n RecipientEligibility memory recipient,\n uint256 tokenType_\n ) internal view returns (uint256 entry) {\n (bool allowed, uint256 maxEligibleMintTimestamp, ) = transferRules.eligibilityThresholdFor(\n tokenType_,\n recipient.identity,\n recipient.isAmlKycPassed\n );\n uint256 lastEligibleDay;\n uint256 deploymentDay_ = deploymentDay;\n if (allowed && maxEligibleMintTimestamp >= deploymentDay_) {\n lastEligibleDay = (maxEligibleMintTimestamp - deploymentDay_) / 1 days;\n } else {\n allowed = false;\n }\n entry = EligibilityCacheLib.store(recipient.cache, tokenType_, allowed, lastEligibleDay);\n }\n\n /**\n * @dev Apply batched bitmap clearing to a specific word\n * @dev This replaces multiple _clearBitmapBit calls with a single AND operation\n */\n function _applyBitmaskToClear(\n address account,\n uint256 wordIndex,\n uint256 bitmaskToClear\n ) internal {\n if (wordIndex < walletIndexesByType[account].length) {\n // Clear all bitmap bits for this word at once (major gas optimization)\n walletIndexesByType[account][wordIndex] &= ~bitmaskToClear;\n }\n }\n\n /**\n * @dev check if address has holder\n * @param addr_ address to check\n * @return true if address has holder\n */\n function _addressHasHolder(address addr_) internal view returns (bool) {\n return holderIdByAddress[addr_] > 0;\n }\n\n /**\n * @param addr_ address to associate with holderId. First holder has holderId = 1.\n * @return _holderId holder id\n */\n function _createHolderFromAddress(\n address addr_\n ) internal returns (uint256 _holderId) {\n if (holderCount >= holderMax) {\n revert RestrictedLockupToken_MaxHolderCountReached();\n }\n holderCount++;\n holderIds += 1;\n _holderId = holderIds;\n\n holderIdByAddress[addr_] = _holderId;\n walletAddressesByHolderId[_holderId].push(addr_);\n\n emit HolderCreated(_holderId, addr_);\n }\n\n /**\n * @dev Generic helper for balance query staticcalls with consistent error handling\n * @param functionSignature The function signature to call (e.g., \"superBalanceOf(address)\")\n * @param account The account to query\n * @return balance The balance result\n */\n function _queryBalance(string memory functionSignature, address account) internal view returns (uint256 balance) {\n (bool success, bytes memory data) = address(this).staticcall(\n abi.encodeWithSignature(functionSignature, account)\n );\n if (!success) {\n revert RestrictedLockupToken_BalanceCheckFailed();\n }\n if (data.length < 32) {\n revert RestrictedLockupToken_BalanceCheckFailed();\n }\n return abi.decode(data, (uint256));\n }\n\n modifier validAddress(address addr_) {\n if (addr_ == address(0)) {\n revert EasyAccessControl_InvalidZeroAddress();\n }\n _;\n }\n\n modifier onlyExistingAddress(address addr_) {\n if (!_addressHasHolder(addr_)) {\n revert RestrictedLockupToken_HolderAddressDoesNotExist();\n }\n _;\n }\n\n modifier onlyReserveAdmin() {\n _onlyReserveAdmin();\n _;\n }\n\n modifier onlyTransferAdmin() {\n _onlyTransferAdmin();\n _;\n }\n\n modifier onlyReserveOrMintAdmin() {\n _onlyReserveOrMintAdmin();\n _;\n }\n\n modifier onlySoftBurnAdmin() {\n _onlySoftBurnAdmin();\n _;\n }\n\n modifier anyAdmin() {\n _anyAdmin();\n _;\n }\n\n modifier onlyWalletsAdminOrTransferAdmin() {\n _onlyWalletsAdminOrTransferAdmin();\n _;\n }\n\n modifier onlyContractAdminOrTransferAdmin() {\n _onlyContractAdminOrTransferAdmin();\n _;\n }\n\n modifier onlyContractAdmin() {\n _onlyContractAdmin();\n _;\n }\n\n function _anyAdmin() internal view {\n if (\n !accessControl.hasRole(_msgSender(), accessControl.RESERVE_ADMIN_ROLE()) &&\n !accessControl.hasRole(_msgSender(), accessControl.WALLETS_ADMIN_ROLE()) &&\n !accessControl.hasRole(_msgSender(), accessControl.TRANSFER_ADMIN_ROLE()) &&\n !accessControl.hasRole(_msgSender(), accessControl.CONTRACT_ADMIN_ROLE())\n ) {\n revert EasyAccessControl_DoesNotHaveAdminRole(_msgSender());\n }\n }\n\n function _onlyReserveAdmin() internal view {\n if (!accessControl.hasRole(_msgSender(), accessControl.RESERVE_ADMIN_ROLE())) {\n revert EasyAccessControl_DoesNotHaveReserveAdminRole(_msgSender());\n }\n }\n\n function _onlySoftBurnAdmin() internal view {\n if (!accessControl.hasRole(_msgSender(), accessControl.SOFT_BURN_ADMIN_ROLE())) {\n revert EasyAccessControl_DoesNotHaveSoftBurnAdminRole(_msgSender());\n }\n }\n\n function _onlyReserveOrMintAdmin() internal view {\n if (\n !accessControl.hasRole(_msgSender(), accessControl.RESERVE_ADMIN_ROLE()) &&\n !accessControl.hasRole(_msgSender(), accessControl.MINT_ADMIN_ROLE())\n ) {\n revert EasyAccessControl_DoesNotHaveAdminRole(_msgSender());\n }\n }\n\n function _onlyTransferAdmin() internal view {\n if (!accessControl.hasRole(_msgSender(), accessControl.TRANSFER_ADMIN_ROLE())) {\n revert EasyAccessControl_DoesNotHaveTransferAdminRole(_msgSender());\n }\n }\n\n function _onlyWalletsAdminOrTransferAdmin() internal view {\n if (\n !accessControl.hasRole(_msgSender(), accessControl.WALLETS_ADMIN_ROLE()) &&\n !accessControl.hasRole(_msgSender(), accessControl.TRANSFER_ADMIN_ROLE())\n ) {\n revert EasyAccessControl_DoesNotHaveAdminRole(_msgSender());\n }\n }\n\n function _onlyContractAdminOrTransferAdmin() internal view {\n if (\n !accessControl.hasRole(_msgSender(), accessControl.CONTRACT_ADMIN_ROLE()) &&\n !accessControl.hasRole(_msgSender(), accessControl.TRANSFER_ADMIN_ROLE())\n ) {\n revert EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(_msgSender());\n }\n }\n\n function _onlyContractAdmin() internal view {\n if (!accessControl.hasRole(_msgSender(), accessControl.CONTRACT_ADMIN_ROLE())) {\n revert EasyAccessControl_DoesNotHaveContractAdminRole(_msgSender());\n }\n }\n\n /// @dev This function is intended to be overridden in child contracts (e.g., for ERC2771Context support)\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n}\n"},"contracts/TransferRules.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\nimport {ERC165} from \"@openzeppelin/contracts/utils/introspection/ERC165.sol\";\nimport {ITransferRules} from \"./interfaces/ITransferRules.sol\";\nimport {IIdentityRegistry} from \"./interfaces/IIdentityRegistry.sol\";\nimport {IRestrictedLockupToken} from \"./interfaces/IRestrictedLockupToken.sol\";\nimport {IAccessControl} from \"./interfaces/IAccessControl.sol\";\nimport {Context} from \"@openzeppelin/contracts/utils/Context.sol\";\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {IERC1404} from \"./interfaces/IERC1404.sol\";\nimport {EligibilityCacheLib, ELIGIBILITY_CACHE_SLOTS} from \"./libraries/EligibilityCacheLib.sol\";\nimport {AccessControl} from \"./AccessControl.sol\";\nimport \"./AccessControlErrors.sol\";\n\ncontract TransferRules is ITransferRules, ERC165, ERC2771Context {\n // ============================================\n // ORIGINAL TRANSFER RULES CONSTANTS & STORAGE\n // ============================================\n \n mapping(uint256 => string) internal errorMessage;\n\n uint256 public constant SUCCESS = 0;\n uint256 public constant GREATER_THAN_RECIPIENT_MAX_BALANCE = 1;\n uint256 public constant SENDER_TOKENS_TIME_LOCKED = 2;\n uint256 public constant DO_NOT_SEND_TO_TOKEN_CONTRACT = 3;\n uint256 public constant DO_NOT_SEND_TO_EMPTY_ADDRESS = 4;\n uint256 public constant SENDER_ADDRESS_FROZEN = 5;\n uint256 public constant ALL_TRANSFERS_PAUSED = 6;\n uint256 public constant RECIPIENT_ADDRESS_FROZEN = 7;\n uint256 public constant LOWER_THAN_RECIPIENT_MIN_BALANCE = 8;\n uint256 public constant INSUFFICIENT_BALANCE_OF_SENDER = 9;\n uint256 public constant SENDER_NOT_AMLKYCPASSED = 10;\n uint256 public constant RECIPIENT_NOT_AMLKYCPASSED = 11;\n uint256 public constant HOLDING_PERIOD_NOT_MET = 12;\n uint256 public constant NO_RULE_FOR_RECIPIENT = 13;\n uint256 public constant RECIPIENT_NOT_QUALIFIED = 14;\n\n uint256 public constant MAX_RESTRICTION_CODE = 14;\n\n /// @dev Sentinel returned by getUnlockTimestamp when no active rule can unlock the holding\n /// for the recipient at their current AML/KYC status. Not a real timestamp.\n uint256 public constant NEVER_UNLOCKS = type(uint256).max;\n\n bytes4 public immutable INTERFACE_ID;\n\n // ============================================\n // TOKEN TYPE RULES ENGINE STORAGE\n // ============================================\n \n // Constants for predefined token types\n uint256 public constant TOKEN_TYPE_GENERIC = 0; // Generic/Default\n\n // Efficient lockup table: region -> accreditation -> TokenTypeRule\n mapping(uint256 => mapping(uint256 => TokenTypeRule)) private _tokenTypeRules;\n TokenTypeRule private _defaultTokenTypeRule;\n\n // ============================================\n // TOKEN TYPE TRANSFER RULES STORAGE\n // ============================================\n \n // Bit manipulation constants for packed TransferRule\n uint256 private constant IS_ACTIVE_MASK = 0x1; // 0000...0001\n uint256 private constant REQUIRES_AML_KYC_MASK = 0x2; // 0000...0010\n uint256 private constant FLAGS_MASK = 0x3; // 0000...0011\n uint256 private constant LOCK_DURATION_SHIFT = 2;\n\n /// @dev Largest lockDurationSeconds that survives packing: bits 0-1 are reserved for the\n /// flags, leaving 254 bits for the duration. Enforced by _packRule.\n uint256 public constant MAX_LOCK_DURATION = type(uint256).max >> LOCK_DURATION_SHIFT;\n \n // Packed storage: tokenType -> region -> accreditation -> uint256 (packed TransferRule)\n // Bit layout: bits 0-1 = flags (isActive, requiresAmlKyc), bits 2-255 = lockDurationSeconds\n // (so lockDurationSeconds must be <= MAX_LOCK_DURATION)\n mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) private _packedRules;\n\n\n modifier onlyTransferAdmin() {\n _onlyTransferAdmin();\n _;\n }\n function _onlyTransferAdmin() internal view {\n if (!accessControl.hasRole(_msgSender(), accessControl.TRANSFER_ADMIN_ROLE())) {\n revert EasyAccessControl_DoesNotHaveTransferAdminRole(_msgSender());\n }\n }\n\n // ============================================\n // PACKED TRANSFERRULE HELPER FUNCTIONS\n // ============================================\n \n /**\n * @dev Pack a TransferRule into a single uint256\n * @param lockDurationSeconds The lock duration in seconds, at most MAX_LOCK_DURATION\n * @param requiresAmlKyc Whether AML/KYC is required\n * @param isActive Whether the rule is active\n * @return packed The packed uint256 value\n *\n * Reverts with TransferRules_LockDurationTooLarge if lockDurationSeconds does not fit in the\n * 254 bits left by the flags. Solidity does not overflow-check `<<`, so without this check the\n * shift below would silently discard the high bits, storing and enforcing a duration the caller\n * never asked for while the caller's value is what gets emitted.\n */\n function _packRule(\n uint256 lockDurationSeconds,\n bool requiresAmlKyc,\n bool isActive\n ) private pure returns (uint256 packed) {\n if (lockDurationSeconds > MAX_LOCK_DURATION) {\n revert TransferRules_LockDurationTooLarge();\n }\n packed = lockDurationSeconds << LOCK_DURATION_SHIFT;\n if (isActive) packed |= IS_ACTIVE_MASK;\n if (requiresAmlKyc) packed |= REQUIRES_AML_KYC_MASK;\n }\n \n /**\n * @dev Unpack a uint256 into TransferRule components\n * @param packed The packed uint256 value\n * @return lockDurationSeconds The lock duration in seconds\n * @return requiresAmlKyc Whether AML/KYC is required\n * @return isActive Whether the rule is active\n */\n function _unpackRule(uint256 packed) private pure returns (\n uint256 lockDurationSeconds,\n bool requiresAmlKyc,\n bool isActive\n ) {\n lockDurationSeconds = packed >> LOCK_DURATION_SHIFT;\n requiresAmlKyc = (packed & REQUIRES_AML_KYC_MASK) != 0;\n isActive = (packed & IS_ACTIVE_MASK) != 0;\n }\n \n // ============================================\n // ERRORS\n // ============================================\n \n error TransferRules_BadRestrictionCode();\n error TransferRules_InvalidAccessControl();\n error TransferRules_InvalidTrustedForwarder();\n error TransferRules_TokenTypeRuleNotFound();\n error TransferRules_RecipientNotAmlKycPassed();\n error TransferRules_InvalidTokenType();\n error TransferRules_InvalidArrayLengths();\n error TransferRules_LockDurationTooLarge();\n\n IAccessControl public accessControl;\n\n constructor(\n address trustedForwarder_,\n address accessControl_\n ) ERC2771Context(trustedForwarder_) {\n if (accessControl_ == address(0)) {\n revert TransferRules_InvalidAccessControl();\n } \n if (trustedForwarder_ == address(0)) {\n revert TransferRules_InvalidTrustedForwarder();\n }\n // Initialize error messages\n errorMessage[SUCCESS] = \"SUCCESS\";\n errorMessage[\n LOWER_THAN_RECIPIENT_MIN_BALANCE\n ] = \"LOWER THAN RECIPIENT MIN BALANCE\";\n errorMessage[\n GREATER_THAN_RECIPIENT_MAX_BALANCE\n ] = \"GREATER THAN RECIPIENT MAX BALANCE\";\n errorMessage[SENDER_TOKENS_TIME_LOCKED] = \"SENDER TOKENS LOCKED\";\n errorMessage[\n DO_NOT_SEND_TO_TOKEN_CONTRACT\n ] = \"DO NOT SEND TO TOKEN CONTRACT\";\n errorMessage[\n DO_NOT_SEND_TO_EMPTY_ADDRESS\n ] = \"DO NOT SEND TO EMPTY ADDRESS\";\n errorMessage[SENDER_ADDRESS_FROZEN] = \"SENDER ADDRESS IS FROZEN\";\n errorMessage[ALL_TRANSFERS_PAUSED] = \"ALL TRANSFERS PAUSED\";\n errorMessage[RECIPIENT_ADDRESS_FROZEN] = \"RECIPIENT ADDRESS IS FROZEN\";\n errorMessage[\n INSUFFICIENT_BALANCE_OF_SENDER\n ] = \"INSUFFICIENT BALANCE OF SENDER\";\n errorMessage[SENDER_NOT_AMLKYCPASSED] = \"SENDER NOT AMLKYCPASSED\";\n errorMessage[RECIPIENT_NOT_AMLKYCPASSED] = \"RECIPIENT NOT AMLKYCPASSED\";\n errorMessage[HOLDING_PERIOD_NOT_MET] = \"HOLDING PERIOD NOT MET\";\n errorMessage[NO_RULE_FOR_RECIPIENT] = \"NO RULE FOR RECIPIENT\";\n errorMessage[RECIPIENT_NOT_QUALIFIED] = \"RECIPIENT NOT QUALIFIED\";\n\n INTERFACE_ID = type(ITransferRules).interfaceId;\n \n accessControl = IAccessControl(accessControl_);\n }\n\n /**\n * Support of ERC165\n * @dev See https://eips.ethereum.org/EIPS/eip-165\n * @param interfaceId The interface identifier, as specified in ERC-165\n */\n function supportsInterface(\n bytes4 interfaceId\n ) public view override returns (bool) {\n return\n interfaceId == INTERFACE_ID || super.supportsInterface(interfaceId);\n }\n\n /// @inheritdoc ITransferRules\n function detectTransferRestrictionBasic(\n address tokenAddr,\n address from,\n address to,\n uint256 value\n ) public view override returns (uint256) {\n IRestrictedLockupToken token = IRestrictedLockupToken(tokenAddr);\n\n // ============================================\n // TRADITIONAL TRANSFER RESTRICTION CHECKS\n // ============================================\n if (token.isPaused()) return ALL_TRANSFERS_PAUSED;\n if (to == address(0)) return DO_NOT_SEND_TO_EMPTY_ADDRESS;\n if (to == address(token)) return DO_NOT_SEND_TO_TOKEN_CONTRACT;\n if (token.getFrozenStatus(from)) return SENDER_ADDRESS_FROZEN;\n if (token.getFrozenStatus(to)) return RECIPIENT_ADDRESS_FROZEN;\n\n if (!token.isAmlKycPassed(from)) return SENDER_NOT_AMLKYCPASSED;\n // recipient is aml kyc passed is checked on holdingtransfer rule level\n\n if (token.balanceOf(from) < value)\n return INSUFFICIENT_BALANCE_OF_SENDER;\n if (token.unlockedTotalBalanceOf(from) < value)\n return SENDER_TOKENS_TIME_LOCKED;\n\n return SUCCESS;\n }\n\n /// @inheritdoc ITransferRules\n function detectTransferRestriction(\n address tokenAddr,\n address from,\n address to,\n uint256 value\n ) external view override returns (uint256) {\n uint256 restrictionCode = detectTransferRestrictionBasic(tokenAddr, from, to, value);\n if (restrictionCode != SUCCESS) return restrictionCode;\n\n IRestrictedLockupToken token = IRestrictedLockupToken(tokenAddr);\n IIdentityRegistry identityRegistry = token.identityRegistry();\n uint256 transferableAmount = _calculateTransferableFromTimelocks(\n token,\n from,\n to,\n identityRegistry,\n value\n );\n if (transferableAmount >= value) return SUCCESS;\n\n uint256 amountToTransferFromHoldings = value - transferableAmount;\n // The token owns the holdings bitmap and walks it once, word by word and bucket-aligned,\n // exactly like its transfer engine. Enumerating here via holdingCountOf/holdingOf\n // re-scanned that bitmap from word 0 for every holding - O(holdings * words) with two\n // external calls per holding - and pushed a RestrictedSwap settlement past the block gas\n // limit at a few hundred holdings.\n uint256 transferableAmountFromHoldings = token.previewTransferableFromHoldings(\n from,\n to,\n amountToTransferFromHoldings\n );\n\n if (transferableAmountFromHoldings < amountToTransferFromHoldings) return HOLDING_PERIOD_NOT_MET;\n\n return SUCCESS;\n }\n \n\n /**\n * @dev Calculates the transferable amount from timelocks\n * @param token The token contract\n * @param from The sender address\n * @param expectedTransferableAmount The expected amount needed (0 = calculate all available)\n * @return transferableAmount The amount that can be transferred from timelocks\n */\n function _calculateTransferableFromTimelocks(\n IRestrictedLockupToken token,\n address from,\n address to,\n IIdentityRegistry identityRegistry,\n uint256 expectedTransferableAmount\n ) internal view returns (uint256 transferableAmount) {\n // Check timelocks for unlocked tokens\n IIdentityRegistry.IdentityInfo memory recipientIdentity = identityRegistry.identity(to);\n bool isAmlKycPassed = token.isAmlKycPassed(to);\n // Rules are collapsed once per token type and reused for every timelock of that type.\n uint256[ELIGIBILITY_CACHE_SLOTS] memory eligibilityCache;\n uint256 totalTimelocks = token.timelockCountOf(from);\n for (uint256 i = 0; i < totalTimelocks; i++) {\n IRestrictedLockupToken.Timelock memory timelock = token.timelockOf(from, i);\n if (timelock.totalAmount == timelock.tokensTransferred) continue;\n\n // Get token type and mint timestamp from global holding index\n (uint256 tokenType, uint256 mintTimestamp) = token.getTokenTypeAndMintTimestampFromGlobalIndex(timelock.globalHoldingIndex);\n if (!_cachedHoldingEligibility(eligibilityCache, tokenType, mintTimestamp, recipientIdentity, isAmlKycPassed)) {\n continue; // skip this timelock\n }\n transferableAmount += token.unlockedBalanceOfTimelock(from, i);\n \n // Early termination if we have enough and expectedTransferableAmount > 0\n if (expectedTransferableAmount > 0 && transferableAmount >= expectedTransferableAmount) {\n break;\n }\n }\n }\n\n /**\n * @dev `detectTransferRestrictionForHolding(...) == SUCCESS`, memoized per token type in\n * `cache`. Same encoding the token side uses (EligibilityCacheLib, see\n * Storage._resolveEligibility), keyed on the mint timestamp itself since that is what\n * timelocks expose; the resolver here is the internal eligibilityThresholdFor rather\n * than an external call.\n */\n function _cachedHoldingEligibility(\n uint256[ELIGIBILITY_CACHE_SLOTS] memory cache,\n uint256 tokenType,\n uint256 mintTimestamp,\n IIdentityRegistry.IdentityInfo memory recipientIdentity,\n bool isAmlKycPassed\n ) private view returns (bool) {\n uint256 entry = EligibilityCacheLib.entryFor(cache, tokenType);\n if (!EligibilityCacheLib.isHit(entry, tokenType)) {\n (bool allowed, uint256 threshold, ) = eligibilityThresholdFor(tokenType, recipientIdentity, isAmlKycPassed);\n entry = EligibilityCacheLib.store(cache, tokenType, allowed, threshold);\n }\n return EligibilityCacheLib.admits(entry, mintTimestamp);\n }\n\n /// @inheritdoc ITransferRules\n function messageForTransferRestriction(\n uint256 restrictionCode\n ) external view override returns (string memory) {\n if (restrictionCode > MAX_RESTRICTION_CODE) {\n revert TransferRules_BadRestrictionCode();\n }\n return errorMessage[restrictionCode];\n }\n\n /// @notice a method for checking a response code to determine if a transfer was succesful.\n /// Defining this separately from the token contract allows it to be upgraded.\n /// For instance this method would need to be upgraded if the SUCCESS code was changed to 1\n /// as specified in ERC-1066 instead of 0 as specified in ERC-1404.\n /// @param restrictionCode The code to check.\n /// @return isSuccess A boolean indicating if the code is the SUCCESS code.\n function checkSuccess(\n uint256 restrictionCode\n ) external pure override returns (bool isSuccess) {\n return restrictionCode == SUCCESS;\n }\n\n // ============================================\n // TOKEN TYPE RULES ENGINE FUNCTIONALITY\n // ============================================\n\n /// @inheritdoc ITransferRules\n function determineTokenType(\n address wallet,\n IIdentityRegistry identityRegistry\n ) external view override returns (uint256 tokenType) {\n // Get wallet identity\n IIdentityRegistry.IdentityInfo memory identity = identityRegistry.identity(wallet);\n\n // Check each region in the array to find the first matching rule\n for (uint256 i = 0; i < identity.regions.length; i++) {\n TokenTypeRule memory rule = _tokenTypeRules[identity.regions[i]][identity.accreditationType];\n \n if (rule.isActive) {\n if (rule.requiresAmlKyc && !identityRegistry.isAmlKycPassed(wallet)) {\n continue;\n }\n\n return rule.tokenType;\n }\n }\n \n if (_defaultTokenTypeRule.isActive) {\n if (_defaultTokenTypeRule.requiresAmlKyc && !identityRegistry.isAmlKycPassed(wallet)) {\n revert TransferRules_RecipientNotAmlKycPassed();\n }\n\n return _defaultTokenTypeRule.tokenType;\n }\n\n revert TransferRules_TokenTypeRuleNotFound();\n }\n\n /// @inheritdoc ITransferRules\n function tokenTypeAllowed(\n uint256 tokenType,\n address wallet,\n IIdentityRegistry identityRegistry\n ) external view override returns (bool) {\n IIdentityRegistry.IdentityInfo memory identity = identityRegistry.identity(wallet);\n for (uint256 i = 0; i < identity.regions.length; i++) {\n TokenTypeRule memory rule = _tokenTypeRules[identity.regions[i]][identity.accreditationType];\n if (rule.isActive && rule.tokenType == tokenType) {\n if (!rule.requiresAmlKyc) {\n return true;\n }\n if (identityRegistry.isAmlKycPassed(wallet)) {\n return true;\n }\n }\n }\n\n if (_defaultTokenTypeRule.isActive && _defaultTokenTypeRule.tokenType == tokenType) {\n if (!_defaultTokenTypeRule.requiresAmlKyc) {\n return true;\n }\n\n return identityRegistry.isAmlKycPassed(wallet);\n }\n\n return false;\n }\n\n /// @inheritdoc ITransferRules\n function setTokenTypeRule(\n uint256 region,\n uint256 accreditation,\n uint256 tokenType,\n bool requiresAmlKyc,\n bool isActive\n ) public override onlyTransferAdmin {\n _tokenTypeRules[region][accreditation] = TokenTypeRule({\n tokenType: tokenType,\n requiresAmlKyc: requiresAmlKyc,\n isActive: isActive\n });\n\n emit TokenTypeRuleSet(\n region,\n accreditation,\n tokenType,\n requiresAmlKyc,\n isActive\n );\n }\n\n function setDefaultTokenTypeRule(\n uint256 tokenType,\n bool requiresAmlKyc,\n bool isActive\n ) public override onlyTransferAdmin {\n _defaultTokenTypeRule = TokenTypeRule({\n tokenType: tokenType,\n requiresAmlKyc: requiresAmlKyc,\n isActive: isActive\n });\n\n emit DefaultTokenTypeRuleSet(tokenType, requiresAmlKyc, isActive);\n }\n\n function resetDefaultTokenTypeRule() public override onlyTransferAdmin {\n _defaultTokenTypeRule = TokenTypeRule({\n tokenType: TOKEN_TYPE_GENERIC,\n requiresAmlKyc: false,\n isActive: false\n });\n\n emit DefaultTokenTypeRuleSet(TOKEN_TYPE_GENERIC, false, false);\n }\n\n function defaultTokenTypeRule() public view override returns (TokenTypeRule memory) {\n return _defaultTokenTypeRule;\n }\n\n /// @inheritdoc ITransferRules\n function removeTokenTypeRule(\n uint256 region,\n uint256 accreditation\n ) public override onlyTransferAdmin {\n delete _tokenTypeRules[region][accreditation];\n\n emit TokenTypeRuleRemoved(region, accreditation);\n }\n\n /// @inheritdoc ITransferRules\n function getTokenTypeRule(\n uint256 region,\n uint256 accreditation\n ) external view override returns (TokenTypeRule memory) {\n return _tokenTypeRules[region][accreditation];\n }\n\n /// @inheritdoc ITransferRules\n function batchSetTokenTypeRules(\n uint256[] calldata regions,\n uint256[] calldata accreditations,\n uint256[] calldata tokenTypes,\n bool[] calldata requiresAmlKycFlags,\n bool[] calldata isActiveFlags\n ) external override onlyTransferAdmin {\n if (\n regions.length != accreditations.length ||\n regions.length != tokenTypes.length ||\n regions.length != requiresAmlKycFlags.length ||\n regions.length != isActiveFlags.length\n ) {\n revert TransferRules_InvalidArrayLengths();\n }\n\n for (uint256 i = 0; i < regions.length; i++) {\n setTokenTypeRule(regions[i], accreditations[i], tokenTypes[i], requiresAmlKycFlags[i], isActiveFlags[i]);\n }\n }\n\n // ============================================\n // TOKEN TYPE TRANSFER RULES FUNCTIONALITY\n // ============================================\n\n /// @inheritdoc ITransferRules\n function detectTransferRestrictionForHolding(\n uint256 tokenType,\n uint256 mintTimestamp,\n IIdentityRegistry.IdentityInfo memory recipientIdentity,\n bool isAmlKycPassed\n ) public view override returns (uint256 restrictionCode) {\n (bool allowed, uint256 maxEligibleMintTimestamp, uint256 failureCode) = eligibilityThresholdFor(\n tokenType,\n recipientIdentity,\n isAmlKycPassed\n );\n return (allowed && mintTimestamp <= maxEligibleMintTimestamp) ? SUCCESS : failureCode;\n }\n\n /// @inheritdoc ITransferRules\n function eligibilityThresholdFor(\n uint256 tokenType,\n IIdentityRegistry.IdentityInfo memory recipientIdentity,\n bool isAmlKycPassed\n ) public view override returns (bool allowed, uint256 maxEligibleMintTimestamp, uint256 failureCode) {\n // For a fixed (tokenType, recipient, amlKyc) the per-region checks collapse to a single\n // threshold on mintTimestamp, because the only mintTimestamp-dependent term is the lock\n // duration comparison. A holding is transferable iff some active, AML-satisfying region\n // has `mintTimestamp + lockDuration <= block.timestamp`, i.e. iff\n // `mintTimestamp <= max(block.timestamp - lockDuration)` over those regions.\n //\n // The failure code is likewise constant: when every region fails, the code reported by\n // the original per-region loop is the one from the last *active* region, and for an\n // AML-satisfying region \"failing\" can only mean the lock check failed.\n //\n // Every region is visited on purpose. The original loop returned SUCCESS at the first\n // region that admitted the given mintTimestamp; a threshold that must stand in for *all*\n // holdings of the type has to be the most permissive one, so it cannot stop early. The\n // callers that evaluate many holdings amortise this through EligibilityCacheLib, paying\n // for it once per token type.\n failureCode = NO_RULE_FOR_RECIPIENT;\n\n for (uint256 i = 0; i < recipientIdentity.regions.length; i++) {\n // Single SLOAD - get packed rule (major gas optimization)\n uint256 packedRule = _packedRules[tokenType][recipientIdentity.regions[i]][recipientIdentity.accreditationType];\n\n // Quick check: is rule active? (single bitwise operation)\n if ((packedRule & IS_ACTIVE_MASK) == 0) continue;\n\n // Check AML/KYC requirement (single bitwise operation)\n if ((packedRule & REQUIRES_AML_KYC_MASK) != 0 && !isAmlKycPassed) {\n failureCode = RECIPIENT_NOT_AMLKYCPASSED;\n continue; // This region requires AML/KYC but recipient doesn't have it, try next region\n }\n\n failureCode = HOLDING_PERIOD_NOT_MET;\n\n // Check holding period (single shift operation)\n uint256 lockDuration = (packedRule >> LOCK_DURATION_SHIFT);\n if (lockDuration == 0) {\n // No holding period for this region: every mintTimestamp qualifies.\n return (true, type(uint256).max, SUCCESS);\n }\n if (lockDuration <= block.timestamp) {\n uint256 threshold = block.timestamp - lockDuration;\n if (!allowed || threshold > maxEligibleMintTimestamp) {\n allowed = true;\n maxEligibleMintTimestamp = threshold;\n }\n }\n // lockDuration > block.timestamp: no mintTimestamp can satisfy this region, so it\n // contributes no threshold and leaves `allowed` untouched.\n }\n\n // `failureCode` now holds the code from the last *active* region, which is exactly what\n // the original per-region loop reports when every region fails.\n //\n // `allowed` stays false when no region can ever qualify. It is tracked separately rather\n // than folded into the threshold because a 0 threshold would otherwise admit a holding\n // with mintTimestamp == 0.\n }\n\n /// @inheritdoc ITransferRules\n function setTransferRule(\n uint256 tokenType,\n uint256 recipientRegion,\n uint256 recipientAccreditation,\n TransferRule memory _rule\n ) public override onlyTransferAdmin {\n // Pack the rule into single uint256 for gas-efficient storage\n uint256 packed = _packRule(\n _rule.lockDurationSeconds,\n _rule.requiresAmlKyc,\n _rule.isActive\n );\n \n // Single SSTORE operation (major gas optimization)\n _packedRules[tokenType][recipientRegion][recipientAccreditation] = packed;\n\n emit TransferRuleSet(\n tokenType,\n recipientRegion,\n recipientAccreditation,\n _rule.lockDurationSeconds,\n _rule.requiresAmlKyc,\n _rule.isActive\n );\n }\n\n /// @inheritdoc ITransferRules\n function removeTransferRule(\n uint256 tokenType, \n uint256 recipientRegion, \n uint256 recipientAccreditation\n ) external override onlyTransferAdmin {\n delete _packedRules[tokenType][recipientRegion][recipientAccreditation];\n\n emit TransferRuleRemoved(tokenType, recipientRegion, recipientAccreditation);\n }\n\n /// @inheritdoc ITransferRules\n function getUnlockTimestamp(\n uint256 tokenType,\n uint256 mintTimestamp,\n address recipient,\n IIdentityRegistry identityRegistry\n ) external view override returns (uint256 unlockTimestamp) {\n IIdentityRegistry.IdentityInfo memory recipientIdentity = identityRegistry.identity(recipient);\n bool isAmlKycPassed = identityRegistry.isAmlKycPassed(recipient);\n\n // Start at NEVER_UNLOCKS and let a region prove otherwise. This mirrors\n // detectTransferRestrictionForHolding, which blocks with NO_RULE_FOR_RECIPIENT when no\n // active rule matches the recipient - returning 0 here would read as \"no restriction\".\n unlockTimestamp = NEVER_UNLOCKS;\n\n // Check each region in the recipient's regions array and return the earliest unlock timestamp\n for (uint256 i = 0; i < recipientIdentity.regions.length; i++) {\n // Direct packed storage access for better gas efficiency\n uint256 packedRule = _packedRules[tokenType][recipientIdentity.regions[i]][recipientIdentity.accreditationType];\n\n // Quick check: is rule active? (single bitwise operation)\n if ((packedRule & IS_ACTIVE_MASK) == 0) continue;\n\n // This region requires AML/KYC the recipient does not have, so waiting out its holding\n // period would not make the transfer succeed. It contributes no candidate timestamp.\n if ((packedRule & REQUIRES_AML_KYC_MASK) != 0 && !isAmlKycPassed) continue;\n\n uint256 regionUnlockTimestamp = mintTimestamp + (packedRule >> LOCK_DURATION_SHIFT);\n if (regionUnlockTimestamp < unlockTimestamp) {\n unlockTimestamp = regionUnlockTimestamp;\n }\n }\n }\n\n /// @inheritdoc ITransferRules\n function batchSetTransferRules(\n uint256[] calldata tokenType,\n uint256[] calldata recipientRegion,\n uint256[] calldata recipientAccreditation,\n TransferRule[] calldata rules\n ) external override onlyTransferAdmin {\n if (\n tokenType.length != recipientRegion.length ||\n tokenType.length != recipientAccreditation.length ||\n tokenType.length != rules.length\n ) {\n revert TransferRules_InvalidArrayLengths();\n }\n\n for (uint256 i = 0; i < rules.length; i++) {\n setTransferRule(tokenType[i], recipientRegion[i], recipientAccreditation[i], rules[i]);\n }\n }\n\n /// @inheritdoc ITransferRules\n function transferRuleFor(\n uint256 tokenType,\n uint256 recipientRegion,\n uint256 recipientAccreditation\n ) public view override returns (TransferRule memory rule) {\n uint256 packed = _packedRules[tokenType][recipientRegion][recipientAccreditation];\n (rule.lockDurationSeconds, rule.requiresAmlKyc, rule.isActive) = _unpackRule(packed);\n }\n\n function _msgSender()\n internal\n view\n override(ERC2771Context)\n returns (address sender)\n {\n return ERC2771Context._msgSender();\n }\n\n function _msgData()\n internal\n view\n override(ERC2771Context)\n returns (bytes calldata)\n {\n return ERC2771Context._msgData();\n }\n\n function _contextSuffixLength()\n internal\n view\n override(ERC2771Context)\n returns (uint256)\n {\n return ERC2771Context._contextSuffixLength();\n }\n}\n"},"contracts/interfaces/IAccessControl.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\ninterface IAccessControl {\n // ============================================\n // EVENTS\n // ============================================\n \n /// @notice This event is emitted when a new role is granted or revoked to a user\n /// @param grantor The address of admin who granted the role\n /// @param grantee The address of user who received the role\n /// @param role The role that was granted as a bitmask\n /// @param status The status of the role (true = granted, false = revoked)\n event RoleChange(\n address indexed grantor,\n address indexed grantee,\n uint8 role,\n bool indexed status\n );\n\n // Note: Errors are defined in AccessControlErrors.sol to avoid duplicate declarations\n\n // ============================================\n // VIEW FUNCTIONS\n // ============================================\n \n // View role checks\n function hasRole(address addr, uint8 role) external view returns (bool);\n function roles(address addr) external view returns (uint8);\n function contractAdminCount() external view returns (uint8);\n\n // Role constants\n function CONTRACT_ADMIN_ROLE() external view returns (uint8);\n function TRANSFER_ADMIN_ROLE() external view returns (uint8);\n function RESERVE_ADMIN_ROLE() external view returns (uint8);\n function WALLETS_ADMIN_ROLE() external view returns (uint8);\n function SOFT_BURN_ADMIN_ROLE() external view returns (uint8);\n function MINT_ADMIN_ROLE() external view returns (uint8);\n\n // ============================================\n // STATE-CHANGING FUNCTIONS\n // ============================================\n \n // Role management\n function grantRole(address addr, uint8 role) external;\n function batchGrantRoles(address[] calldata addresses, uint8[] calldata roles_) external;\n function revokeRole(address addr, uint8 role) external;\n function batchRevokeRoles(address[] calldata addresses, uint8[] calldata roles_) external;\n}"},"contracts/interfaces/IDividends.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\n/**\n * @title IDividends\n * @dev Use only standard ERC-20 tokens that keep a constant balance.\n *\n * Do not use these types of token:\n * - a token that subtracts a fee from each transfer;\n * - a token that changes account balances automatically (a rebase token);\n * - an ERC-777 token that calls back into this contract.\n *\n * The fund functions read the contract balance before the transfer and after the transfer. If the\n * increase is not the same as the amount, the function fails. Only the fund functions do this\n * check.\n *\n * The claim functions and the reclaim functions do not do this check. They send the amount from\n * the internal records. They do not compare that amount with the true token balance. If you use a\n * different type of token, these functions can send too few tokens, or they can fail. The contract\n * has no function that recovers tokens that stay in it.\n */\ninterface IDividends {\n /**\n * @title Functions\n */\n\n /// @dev Get unused ERC-20 tokens on timestamp\n /// @param token ERC-20 token address\n /// @param timestamp Timestamp for historical data\n /// @return amount of ERC-20 tokens\n function tokensAt(\n address token,\n uint256 timestamp\n ) external view returns (uint256);\n\n /**\n * @dev Fund any ERC-20 tokens into current contract\n * Tokens can be claimed by holders of RestrictedLockup Token uses claimDividends method.\n * Carries no explicit unlock time. If this deposit auto-starts the distribution and no\n * unlock was pinned by a prior 4-arg {IRecallablePayment-fundDividend} overload, claiming\n * opens at `block.timestamp` (i.e. immediately). If a future unlock was already pinned, this\n * deposit funds freely and claiming opens at that pinned time (this call never overrides it).\n * @param token ERC-20 token address\n * @param amount amount of tokens to fund\n * @param timestamp timestamp for dividend distribution\n */\n function fundDividend(\n address token,\n uint256 amount,\n uint256 timestamp\n ) external;\n\n /**\n * @dev Reclaim ERC-20 tokens from a specific dividend snapshot\n * Can only be done if no dividends have been claimed or reclaimed for this snapshot\n * If amount is 0, reclaims all available funds\n * @param token ERC-20 token address\n * @param amount amount of tokens to reclaim (0 = reclaim all)\n * @param timestamp timestamp for dividend distribution\n */\n function reclaimTotalDividend(\n address token,\n uint256 amount,\n uint256 timestamp\n ) external;\n\n /**\n * @dev Reclaim dividends for a specific target address and send to contract reclaimer\n * Can only be called by transfer admin\n * @param token ERC-20 token address\n * @param targetAddress Address to reclaim dividends for\n * @param timestamp timestamp for dividend distribution\n * @param amount amount of tokens to reclaim (0 = reclaim all)\n */\n function reclaimDividend(\n address token,\n address targetAddress,\n uint256 timestamp,\n uint256 amount\n ) external;\n\n /**\n * @dev Get balance of ERC-20 tokens funded at timestamp\n * @param token ERC-20 token address\n * @param timestamp timestamp for historical data\n * @return amount of ERC-20 tokens\n */\n function fundsAt(\n address token,\n uint256 timestamp\n ) external view returns (uint256);\n\n /**\n * @dev Amount of ERC-20 tokens distributed to the holder of RestrictedLockup Token at timestamp\n * @param token ERC-20 token address\n * @param receiver RestrictedLockup Token's holder address\n * @param timestamp timestamp for historical data\n * @return amount of total ERC-20 tokens distributed to the receiver\n */\n function totalAwardedBalanceAt(\n address token,\n address receiver,\n uint256 timestamp\n ) external view returns (uint256);\n\n /**\n * @dev Amount of ERC-20 tokens claimed by the holder of RestrictedLockup Token at timestamp\n * @param token ERC-20 token address\n * @param receiver RestrictedLockup Token's holder address\n * @param timestamp timestamp for historical data\n * @return amount of claimed ERC-20 tokens\n */\n function claimedBalanceAt(\n address token,\n address receiver,\n uint256 timestamp\n ) external view returns (uint256);\n\n /**\n * @dev Amount of ERC-20 tokens that can be claimed by the holder of RestrictedLockup Token at timestamp\n * @param token ERC-20 token address\n * @param receiver RestrictedLockup Token's holder address\n * @param timestamp timestamp for historical data\n * @return amount of can be claimed ERC-20 tokens\n */\n function unclaimedBalanceAt(\n address token,\n address receiver,\n uint256 timestamp\n ) external view returns (uint256);\n\n /**\n * @dev Claim ERC-20 tokens (dividends) by RestrictedLockup Tokens holder\n * Tokens can be claimed when its allowed by unclaimedBalanceAt\n * @param token ERC-20 token address\n * @param timestamp timestamp for dividend claim\n * @param amount amount of tokens to reclaim (0 = reclaim all)\n */\n function claimDividend(address token, uint256 timestamp, uint256 amount) external;\n\n /**\n * @dev Claim ERC-20 tokens (dividends) by RestrictedLockup Tokens holder, across multiple timestamps\n * Tokens can be claimed when its allowed by unclaimedBalanceAt\n * @param token ERC-20 token address\n * @param timestamps timestamps for dividend claims\n * @param amounts amounts of tokens to reclaim (0 = reclaim all)\n */\n function batchClaimDividend(\n address token,\n uint256[] calldata timestamps,\n uint256[] calldata amounts\n ) external;\n\n /**\n * @title Events\n */\n\n /// @notice This event is emitted when a timestamp is funded with ERC-20 tokens\n /// @param payer address of the payer\n /// @param token ERC-20 token address\n /// @param amount amount of ERC-20 tokens\n /// @param timestamp timestamp for dividend distribution\n event DividendFunded(\n address indexed payer,\n address indexed token,\n uint256 amount,\n uint256 indexed timestamp\n );\n\n /// @notice This event is emitted when a holder of RestrictedLockup Token claims ERC-20 tokens\n /// @param payee address of the receiver\n /// @param token ERC-20 token address\n /// @param amount amount of ERC-20 tokens\n /// @param timestamp timestamp for dividend claim\n event DividendClaimed(\n address indexed payee,\n address indexed token,\n uint256 amount,\n uint256 indexed timestamp\n );\n\n /// @notice This event is emitted when an admin reclaims ERC-20 tokens from a dividend\n /// distribution\n /// @dev The reclaimed funds are transferred to the emitting contract's current\n /// `reclaimerAddress`, which is intentionally NOT a field of this event (the\n /// signature is kept identical across `InterestPayment` and `RecallablePayment`\n /// for ABI compatibility). Indexers can recover the destination of any reclaim\n /// from the `ReclaimerAddressChanged` history: reclaiming requires a non-zero\n /// reclaimer, so at least one `ReclaimerAddressChanged` always precedes this\n /// event, and the destination is the value it carried at the time of the reclaim.\n /// @param payee the transfer admin that initiated the reclaim (`_msgSender()`) — NOT\n /// the fund destination\n /// @param target the recipient whose unclaimed allocation was reclaimed\n /// (`reclaimDividend`), or the emitting contract's own address for pool-level\n /// reclaims (`reclaimTotalDividend`, and `reclaimSurplus` in\n /// `RecallablePayment`)\n /// @param token ERC-20 token address\n /// @param amount amount of ERC-20 tokens reclaimed\n /// @param timestamp timestamp for dividend distribution\n event DividendReclaimed(\n address indexed payee,\n address indexed target,\n address indexed token,\n uint256 amount,\n uint256 timestamp\n );\n}"},"contracts/interfaces/IERC1404.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface IERC1404 is IERC20 {\n function detectTransferRestriction(\n address from,\n address to,\n uint256 value\n ) external view returns (uint8);\n\n function messageForTransferRestriction(\n uint8 restrictionCode\n ) external view returns (string memory);\n}\n"},"contracts/interfaces/IERC20Decimals.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\n/// @notice enhanced ERC20 interface with decimals\ninterface IERC20Decimals is IERC20 {\n function decimals() external view returns (uint8);\n}\n"},"contracts/interfaces/IIdentityRegistry.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\nimport \"@openzeppelin/contracts/utils/introspection/IERC165.sol\";\n\ninterface IIdentityRegistry {\n struct IdentityInfo {\n uint256[] regions;\n uint256 accreditationType;\n uint256 lastAmlKycChangeTimestamp;\n uint256 lastAccreditationChangeTimestamp;\n bool amlKycPassed;\n }\n\n /**\n * @dev Sets the validity duration for AML/KYC status in seconds.\n * @param amlKycValidityDuration The duration in seconds for which AML/KYC status remains valid\n */\n function setAmlKycValidityDuration(uint256 amlKycValidityDuration) external;\n\n /**\n * @dev Sets or updates the complete identity information for a wallet address.\n * @param owner The wallet address to set identity for\n * @param info The complete identity information structure\n */\n function setIdentity(address owner, IdentityInfo memory info) external;\n\n /**\n * @dev Sets or updates the complete identity information for a wallet address.\n * @param owners The wallet addresses to set identity for\n * @param info The complete identity information structure\n */\n function batchSetIdentity(address[] memory owners, IdentityInfo memory info) external;\n\n /**\n * @dev Removes all identity information for a wallet address.\n * @param owner The wallet address to remove identity from\n */\n function removeIdentity(address owner) external;\n\n /**\n * @dev Retrieves the complete identity information for a wallet address.\n * @param owner The wallet address to query\n * @return The complete identity information structure\n */\n function identity(\n address owner\n ) external view returns (IdentityInfo memory);\n\n /**\n * @dev Sets the regions for a wallet address.\n * @param owner The wallet address to set regions for\n * @param regions The array of region identifiers\n */\n function setRegions(address owner, uint256[] memory regions) external;\n\n /**\n * @dev Adds a region to a wallet address.\n * @param owner The wallet address to add region to\n * @param region The region identifier to add\n */\n function addRegion(address owner, uint256 region) external;\n\n /**\n * @dev Removes a region from a wallet address.\n * @param owner The wallet address to remove region from\n * @param region The region identifier to remove\n */\n function removeRegion(address owner, uint256 region) external;\n\n /**\n * @dev Grants AML/KYC approval for a wallet address.\n * @param owner The wallet address to grant AML/KYC for\n * @param amlKycTimestamp The timestamp when AML/KYC was granted (0 for current timestamp)\n */\n function grantAmlKyc(address owner, uint256 amlKycTimestamp) external;\n\n /**\n * @dev Revokes AML/KYC approval for a wallet address.\n * @param owner The wallet address to revoke AML/KYC from\n * @param amlKycTimestamp The timestamp when AML/KYC was revoked (0 for current timestamp)\n */\n function revokeAmlKyc(address owner, uint256 amlKycTimestamp) external;\n\n /**\n * @dev Grants accreditation level for a wallet address.\n * @param owner The wallet address to grant accreditation for\n * @param accreditationType The type/level of accreditation to grant\n * @param accreditationTimestamp The timestamp when accreditation was granted (0 for current timestamp)\n */\n function grantAccreditation(address owner, uint256 accreditationType, uint256 accreditationTimestamp) external;\n\n /**\n * @dev Revokes accreditation for a wallet address.\n * @param owner The wallet address to revoke accreditation from\n * @param accreditationTimestamp The timestamp when accreditation was revoked (0 for current timestamp)\n */\n function revokeAccreditation(address owner, uint256 accreditationTimestamp) external;\n\n /**\n * @dev Retrieves the regions for a wallet address.\n * @param owner The wallet address to query\n * @return The array of region identifiers\n */\n function regions(address owner) external view returns (uint256[] memory);\n\n /**\n * @dev Checks if a wallet address has a specific region.\n * @param owner The wallet address to check\n * @param region The region identifier to check\n * @return True if the wallet has the region, false otherwise\n */\n function hasRegion(address owner, uint256 region) external view returns (bool);\n\n /**\n * @dev Retrieves the accreditation type for a wallet address.\n * @param owner The wallet address to query\n * @return The accreditation type/level\n */\n function accreditationType(address owner) external view returns (uint256);\n\n /**\n * @dev Checks if a wallet address has valid AML/KYC approval.\n * @param owner The wallet address to check\n * @return True if AML/KYC is passed and still valid, false otherwise\n */\n function isAmlKycPassed(address owner) external view returns (bool);\n\n /**\n * @dev Retrieves the current AML/KYC validity duration in seconds.\n * @return The validity duration in seconds\n */\n function amlKycValidityDuration() external view returns (uint256);\n\n // Events\n event IdentityCreated(address indexed authority, address indexed owner);\n event IdentityRemoved(address indexed authority, address indexed owner);\n event RegionsSet(address indexed authority, address indexed owner, uint256[] regions);\n event RegionAdded(address indexed authority, address indexed owner, uint256 region);\n event RegionRemoved(address indexed authority, address indexed owner, uint256 region);\n event AmlKycPassed(address indexed authority, address indexed owner, uint256 amlKycTimestamp);\n event AmlKycFailed(address indexed authority, address indexed owner, uint256 amlKycTimestamp);\n event AccreditationGranted(address indexed authority, address indexed owner, uint256 newAccreditationLevel, uint256 accreditationTimestamp);\n event AccreditationRevoked(address indexed authority, address indexed owner, uint256 revokedAccreditationLevel, uint256 accreditationTimestamp);\n event AmlKycValidityDurationSet(address indexed authority, uint256 oldAmlKycValidityDuration, uint256 newAmlKycValidityDuration);\n}\n"},"contracts/interfaces/IInterestPayment.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\n/**\n * @title IInterestPayment\n * @notice Interface for managing interest payments for loans backed by RestrictedLockupToken\n * @dev Defines functions for tracking interest accrual based on token holdings and claiming interest payments\n */\ninterface IInterestPayment {\n /// @notice This event is emitted when a payment interest period is funded with ERC-20 tokens\n /// @param payer address of the payer who funded the interest\n /// @param amount amount of ERC-20 tokens funded\n event Funded(address indexed payer, uint256 amount);\n\n /// @notice This event is emitted when a payment period is created\n /// @param creator address of the creator who created the payment period\n /// @param paymentPeriodIdx payment period index\n /// @param startTimestamp start timestamp\n /// @param endTimestamp end timestamp\n /// @param interestRate interest rate\n event PaymentPeriodCreated(\n address indexed creator,\n uint256 indexed paymentPeriodIdx,\n uint256 startTimestamp,\n uint256 endTimestamp,\n uint256 interestRate\n );\n\n /// @notice This event is emitted when a interest rate is updated\n /// @param creator address of the creator who updated the interest rate\n /// @param paymentPeriodIdx payment period index\n /// @param interestRate interest rate\n event InterestRateUpdated(\n address indexed creator,\n uint256 indexed paymentPeriodIdx,\n uint256 interestRate\n );\n\n /// @notice This event is emitted when a holder of RestrictedLockup Token claims interests\n /// @param payee address of the receiver who claimed the interest\n /// @param amount amount of ERC-20 tokens claimed\n /// @param periodIdx period index for which interest was claimed\n event Claimed(\n address indexed payee,\n uint256 amount,\n uint256 indexed periodIdx\n );\n\n /// @notice This event is emitted when interest is reclaimed from a specific wallet\n /// @param payee address that reclaimed the interest\n /// @param target address from which interest was reclaimed\n /// @param amount amount of ERC-20 tokens reclaimed\n /// @param periodIdx period index for which interest was reclaimed\n event Reclaimed(\n address indexed payee,\n address indexed target,\n uint256 amount,\n uint256 periodIdx\n );\n\n /// @notice This event is emitted when all unclaimed interest for a period is reclaimed\n /// @param payee address that reclaimed the interest\n /// @param amount total amount of ERC-20 tokens reclaimed\n /// @param periodIdx period index for which all interest was reclaimed\n event ReclaimedAll(\n address indexed payee,\n uint256 amount,\n uint256 indexed periodIdx\n );\n\n /// @notice This event is emitted when principal amount is reclaimed\n /// @param payee address that reclaimed the principal\n /// @param amount amount of ERC-20 tokens reclaimed as principal\n event PrincipalReclaimed(address indexed payee, uint256 amount);\n\n /// @notice This event is emitted when principal amount is funded\n /// @param payer address of the payer who funded the principal\n /// @param amount amount of ERC-20 tokens funded as principal\n event PrincipalFunded(address indexed payer, uint256 amount);\n\n /// @notice This event is emitted by the `fundPrincipal` call that brings the principal pool\n /// up to the full entitlement of circulating supply\n /// @dev Fires once per completion, not only the first: minting new supply or reclaiming from\n /// the pool takes it back below the entitlement, and funding it up again emits this again.\n /// Nothing marks the pool going short, so `principalRedemptionOpen()` — not this event — is\n /// the source of truth for whether claims can succeed.\n /// @param total the unused principal total that reached `requiredPrincipalFunding()`\n event PrincipalFullyFunded(uint256 total);\n\n /// @notice This event is emitted when principal amount is claimed\n /// @param payee address of the receiver who claimed the principal\n /// @param amount amount of ERC-20 tokens claimed as principal\n event PrincipalClaimed(address indexed payee, uint256 amount);\n\n /// @notice This event is emitted when the reclaimer address is changed\n /// @param admin address of the admin who changed the reclaimer\n /// @param newReclaimer address of the new reclaimer\n event ReclaimerAddressChanged(\n address indexed admin,\n address indexed newReclaimer\n );\n\n /// @notice This event is emitted when the interest accrual end timestamp is changed\n /// @param admin address of the admin who changed the interest accrual end timestamp\n /// @param newInterestAccrualEndTimestamp new timestamp for the interest accrual end timestamp\n event InterestAccrualEndShifted(\n address indexed admin,\n uint256 newInterestAccrualEndTimestamp\n );\n\n /// @notice This event is emitted when a payment period is paused\n /// @param admin address of the admin who paused the period\n /// @param periodIdx period index that was paused\n event PeriodPaused(address indexed admin, uint256 indexed periodIdx);\n\n /// @notice This event is emitted when a payment period is unpaused\n /// @param admin address of the admin who unpaused the period\n /// @param periodIdx period index that was unpaused\n event PeriodUnpaused(address indexed admin, uint256 indexed periodIdx);\n\n /// @notice This event is emitted when a payment period is paused after a specific timestamp\n /// @param timestamp The timestamp after which payments are paused\n event PaymentPausedAfter(uint256 timestamp);\n\n /// @notice This event is emitted when a payments is unpaused\n event PaymentUnpausedAfter();\n\n /// @notice This event is emitted when a payments is paused after a specific timestamp\n /// @param admin address of the admin who paused the payments\n /// @param timestamp The timestamp after which payments are paused\n event EarlyRepayment(address indexed admin, uint256 timestamp);\n\n /// @notice This event is emitted when an admin force claims interest for a wallet\n /// @param admin address of the admin who force claimed\n /// @param wallet address of the wallet for which interest was claimed\n /// @param amount amount of ERC-20 tokens claimed\n /// @param periodIdx period index for which interest was claimed\n event ForceClaimed(\n address indexed admin,\n address indexed wallet,\n uint256 amount,\n uint256 indexed periodIdx\n );\n\n /// @notice This event is emitted when the max interest rate is set\n /// @param admin address of the admin who set the max interest rate\n /// @param maxInterestRate the new max interest rate\n event SetMaxInterestRate(address indexed admin, uint256 maxInterestRate);\n\n /// @notice Checks if a specific payment period is paused\n /// @param periodIdx The period index to check\n /// @return A boolean indicating if the period is paused (true) or active (false)\n function paymentPeriodPaused(\n uint256 periodIdx\n ) external view returns (bool);\n\n /// @notice Pauses payments for a specific period\n /// @param periodIdx The period index to pause\n function pausePaymentPeriod(uint256 periodIdx) external;\n\n /// @notice Unpauses payments for a specific period\n /// @param periodIdx The period index to unpause\n function unpausePaymentPeriod(uint256 periodIdx) external;\n\n /// @notice Pauses or unpauses the entire contract\n /// @param isPaused_ Whether to pause (true) or unpause (false) the contract\n function pause(bool isPaused_) external;\n\n /// @notice Pauses payment after a specific timestamp\n /// @param timestamp The timestamp after which payments will be paused\n function pausePaymentAfter(uint256 timestamp) external;\n\n /// @notice Unpauses payment after being paused with pausePaymentAfter\n function unpausePaymentAfter() external;\n\n /// @notice Sets the address that can reclaim unclaimed interest\n /// @dev Reverts with InterestPayment_InvalidReclaimerAddress when the new reclaimer is\n /// the InterestPayment contract itself - reclaims debit accounting and then\n /// transfer to the reclaimer, so a self-reclaimer would strand the funds.\n /// address(0) is permitted and disables reclaiming: every reclaim entrypoint\n /// rejects an unset reclaimer at call time.\n /// @param reclaimerAddress_ The new reclaimer address\n function setReclaimerAddress(address reclaimerAddress_) external;\n\n /// @notice Shifts the interest accrual end timestamp\n /// @param newInterestAccrualEndTimestamp The new interest accrual end timestamp\n function shiftInterestAccrualEnd(\n uint256 newInterestAccrualEndTimestamp\n ) external;\n\n /// @notice Calculates accrued interest for an account at the current time\n /// @param account The account address to check interest for\n /// @return The amount of accrued interest in the specified token\n function accruedInterest(address account) external view returns (uint256);\n\n /// @notice Calculates accrued interest for an account at a specific timestamp\n /// @param account The account address to check interest for\n /// @param timestamp The timestamp at which to calculate the accrued interest\n /// @return The amount of accrued interest at the given timestamp\n function accruedInterestAt(\n address account,\n uint256 timestamp\n ) external view returns (uint256);\n\n /// @notice Shows total funded interest\n /// @return The total funded interest\n function totalInterestAmountFunded() external view returns (uint256);\n\n /// @notice Shows total claimed interest\n /// @return The total claimed interest\n function totalInterestAmountClaimed() external view returns (uint256);\n\n /// @notice Shows total reclaimed interest\n /// @return The total reclaimed interest\n function totalInterestAmountReclaimed() external view returns (uint256);\n\n /// @notice Shows total unused interest\n /// @return The total unused interest\n function totalInterestAmountUnused() external view returns (uint256);\n\n /// @notice Gets the nearest interest payment timestamp at a given timestamp\n /// @param timestamp The reference timestamp\n /// @return The nearest interest payment timestamp (rounded to period boundaries)\n function nearestInterestPaymentTimestampAt(\n uint256 timestamp\n ) external view returns (uint256);\n\n /// @notice Finds the index of a funded payment period that covers the specified time range\n /// @param startTimestamp The start timestamp of the period to find\n /// @param endTimestamp The end timestamp of the period to find\n /// @return The period index or max uint256 if no matching period is found\n function findPaymentPeriodIndex(\n uint256 startTimestamp,\n uint256 endTimestamp\n ) external view returns (uint256);\n\n /// @notice Creates a payment period\n /// @param startTimestamp The start timestamp of the period\n /// @param endTimestamp The end timestamp of the period\n /// @param interestRate The interest rate for the period\n /// @param interestRatePeriodDuration The duration of the interest rate period\n function createPaymentPeriod(\n uint256 startTimestamp,\n uint256 endTimestamp,\n uint256 interestRate,\n uint256 interestRatePeriodDuration\n ) external;\n\n /// @notice Funds interest\n /// @dev The total amount that you supply during the life of the contract must be\n /// 2^128-1 or less. This limit is necessary because the period fields\n /// totalClaimedInterest and totalReclaimedInterest are uint128. If the total\n /// amount becomes more than 2^128-1, this function reverts with\n /// InterestPayment_FieldOverflow. The total amount includes each amount that you\n /// supply again after a claim. The total amount is not the contract balance.\n /// @param amount The amount of tokens to fund\n function fundInterest(uint256 amount) external;\n\n /// @notice Claims interest for a specific period\n /// @param paymentPeriodIdx The payment period index to claim interest for\n /// @param amount The amount to claim, capped at available claimable amount\n function claimInterestForPeriod(\n uint256 paymentPeriodIdx,\n uint256 amount\n ) external;\n\n /// @notice Claims interest for multiple periods\n /// @dev Strict: every listed period must have something to claim, or the whole batch reverts.\n /// Unlike the aggregate paths this settles each period as it goes, so the unused pool\n /// shrinks between iterations and a later period is paid partially rather than refused.\n /// @param paymentPeriodIdxs The payment period indexes to claim interest for\n /// @param amount The amount to claim, capped at available claimable amount\n function batchClaimInterestForPeriods(\n uint256[] memory paymentPeriodIdxs,\n uint256 amount\n ) external;\n\n /// @notice Claims interest for all available periods\n /// @dev All-or-nothing: every accrued period's claimable amount is summed and the call\n /// reverts with `InterestPayment_NoFundsToClaim` if the total exceeds\n /// `totalInterestAmountUnused()`, rather than settling the holder short. For a partial\n /// payout pass a positive `amount`, or use `claimInterestForPeriod`.\n /// @param amount The amount to claim, capped at available claimable amount\n function claimInterest(uint256 amount) external;\n\n /// @notice Force claim interest for an account for a specific period\n /// @param wallet The wallet address to claim interest for\n /// @param paymentPeriodIdx The payment period index to claim interest for\n /// @param amount The amount to claim, capped at available claimable amount\n function forceClaimForPeriod(\n address wallet,\n uint256 paymentPeriodIdx,\n uint256 amount\n ) external;\n\n /// @notice Batch force claim interest for specific periods for a wallet\n /// @dev All-or-nothing, as in `claimInterest`; `forceClaimForPeriod` settles one period.\n /// @param wallet The wallet address to claim interest for\n /// @param paymentPeriodIdxs Array of payment period indices to claim interest from\n /// @param amount The maximum amount to claim, or 0 for all available funds\n function batchForceClaimInterest(\n address wallet,\n uint256[] memory paymentPeriodIdxs,\n uint256 amount\n ) external;\n\n /// @notice Gets the total number of payment periods\n /// @return The number of payment periods\n function paymentPeriodsCount() external view returns (uint256);\n\n /// @notice Gets the available interest for a specific period\n /// @param periodIdx The period index to check\n /// @return The available interest for the specified period\n function periodAvailableInterest(\n uint256 periodIdx\n ) external view returns (uint256);\n\n /// @notice Gets the duration of a specific period in seconds\n /// @param periodIdx The period index to check\n /// @return The period duration in seconds\n function periodDuration(uint256 periodIdx) external view returns (uint256);\n\n /// @notice Calculates unclaimed amount for a receiver at a specific period\n /// @dev Capped at the period's remaining interest and at `totalInterestAmountUnused()`. The\n /// global cap is applied per period, so summing across periods can exceed the contract\n /// balance: this is what is owed, not what is payable now.\n /// @param receiver_ The receiver address to check\n /// @param paymentPeriodIdx_ The payment period index to check\n /// @return The unclaimed amount for the specified parameters\n function unclaimedAmountForPeriod(\n address receiver_,\n uint256 paymentPeriodIdx_\n ) external view returns (uint256);\n\n /// @notice Gets claimed amount for a receiver at a specific period\n /// @param receiver_ The receiver address to check\n /// @param paymentPeriodIdx_ The payment period index to check\n /// @return The claimed amount for the specified parameters\n function claimedAmountForPeriod(\n address receiver_,\n uint256 paymentPeriodIdx_\n ) external view returns (uint256);\n\n /// @notice Reclaims interest for a specific period from a specific wallet\n /// @param wallet The wallet address from which to reclaim interest\n /// @param paymentPeriodIdx_ The payment period index to reclaim interest from\n /// @param amount The amount to reclaim, capped at available reclaimable amount\n function reclaimInterestForPeriod(\n address wallet,\n uint256 paymentPeriodIdx_,\n uint256 amount\n ) external;\n\n /// @notice Reclaims interest for all recipients for a specific period\n /// @param paymentPeriodIdx_ The payment period index to reclaim all interest from\n function reclaimInterestForAllRecipients(\n uint256 paymentPeriodIdx_\n ) external;\n\n /// @notice Reclaims interest across all accrued periods from a specific wallet\n /// @dev All-or-nothing, as in `claimInterest`; `reclaimInterestForPeriod` reclaims one period.\n /// @param wallet The wallet address from which to reclaim interest\n /// @param amount The amount to reclaim, capped at available reclaimable amount\n function reclaimInterest(address wallet, uint256 amount) external;\n\n /// @notice Reclaims interest for multiple periods from a specific wallet\n /// @dev All-or-nothing, as in `claimInterest`.\n /// @param wallet The wallet address from which to reclaim interest\n /// @param paymentPeriodIdxs The payment period indexes to reclaim interest from\n /// @param amount The amount to reclaim, capped at available reclaimable amount\n function batchReclaimInterest(\n address wallet,\n uint256[] memory paymentPeriodIdxs,\n uint256 amount\n ) external;\n\n /// @notice Reclaims total unused interest without regard to periods or wallets\n /// @dev Does not adjust any period's `totalReclaimedInterest`, so `periodAvailableInterest`\n /// keeps reporting the full entitlement while the pool shrinks. Reclaiming below\n /// outstanding entitlements is allowed, and is what makes the all-or-nothing aggregate\n /// paths start reverting.\n /// @param amount The amount to reclaim from total unused interest\n function reclaimTotalInterest(uint256 amount) external;\n\n /// @notice Funds principal amount in payment token\n /// @dev The pool may be filled in tranches, but the running total\n /// (`totalAvailablePrincipalAmount() + amount`) may never exceed\n /// `requiredPrincipalFunding()` — the circulating supply in security-token BASE UNITS\n /// multiplied by `principalAmountPerToken()` — or the call reverts with\n /// `InterestPayment_PrincipalFundingExceedsEntitlement`.\n ///\n /// Principal redemption is open only while the pool covers `requiredPrincipalFunding()`.\n /// The call that brings it there emits `PrincipalFullyFunded` and makes\n /// `principalRedemptionOpen()` true. Since `claimPrincipal` pays out\n /// `balance * principalAmountPerToken()` per holder, opening claims against a short pool\n /// would let the first claimants redeem in full until it ran dry.\n ///\n /// Coverage is re-evaluated on every claim, so minting new supply or reclaiming from the\n /// pool shuts redemption again until the pool is funded back up. `PrincipalFullyFunded`\n /// therefore fires once per completion, not only the first time.\n ///\n /// Funding a total derived from the whole-token supply instead of the base-unit supply\n /// is short by `10 ** restrictedLockupToken.decimals()`, so it will fund without\n /// reverting but will never open redemption.\n /// @param amount The amount of tokens to fund as principal\n function fundPrincipal(uint256 amount) external;\n\n /// @notice Whether principal redemption is open, i.e. the pool currently covers the full\n /// entitlement of circulating supply\n /// @dev While false, `claimPrincipal` and `forceClaimPrincipal` revert with\n /// `InterestPayment_PrincipalNotFullyFunded` and `availablePrincipalAmount` returns 0.\n ///\n /// This is a LIVE predicate, not a latch. It can go back to false: minting new supply raises\n /// the entitlement and `reclaimPrincipal` lowers the pool, and in either case claims must stop\n /// until the pool is whole again, or the first claimants redeem in full at the expense of the\n /// rest. Re-read it after any mint or reclaim rather than caching it; no event marks the pool\n /// going short.\n ///\n /// Claims themselves never shut it: a claim burns `tokensToBurn` and removes exactly\n /// `tokensToBurn * principalAmountPerToken()` from the pool, lowering both sides equally.\n ///\n /// It also reads false in two states where no claim could succeed anyway: before any supply\n /// has been minted, and once every token has been redeemed. In both,\n /// `requiredPrincipalFunding()` is 0.\n /// @return True while the pool covers the full entitlement of circulating supply\n function principalRedemptionOpen() external view returns (bool);\n\n /// @notice Claims principal amount in payment token\n /// @dev A matching number of tokens (amount / principalAmountPerToken) is burned\n /// @param amount The amount to claim, or 0 for the caller's full entitlement.\n /// When 0, claims `balance * principalAmountPerToken` in full — redemption is only open\n /// while the pool covers that, so no capping applies.\n /// When greater than 0, must not exceed the caller's entitlement and must be divisible by\n /// principalAmountPerToken\n function claimPrincipal(uint256 amount) external;\n\n /// @notice Force claim principal for a specific wallet address\n /// @param wallet The wallet address to claim principal for\n /// @param amount The amount to claim, or 0 for all available principal.\n /// Follows the same rules as claimPrincipal, applied to `wallet`\n function forceClaimPrincipal(address wallet, uint256 amount) external;\n\n /// @notice Reclaims principal amount in payment token\n /// @param amount The amount to reclaim, or 0 for all available principal\n function reclaimPrincipal(uint256 amount) external;\n\n /// @notice Gets the available principal amount for a specific account\n /// @dev Returns the account's full entitlement (`balance * principalAmountPerToken`) while\n /// `principalRedemptionOpen()` is true, and 0 otherwise — either the account holds nothing or\n /// the pool does not cover the circulating entitlement, and no claim could succeed.\n ///\n /// A non-zero result is divisible by principalAmountPerToken and can be passed straight back\n /// as the `amount` argument of claimPrincipal. A result of 0 must NOT be: 0 is the\n /// claim-everything sentinel there, so forwarding it asks for the opposite of what this view\n /// reported. Check `principalRedemptionOpen()` instead of inferring it from a 0 here.\n ///\n /// This view does not apply the maturity gate `claimPrincipal` applies, so a non-zero result\n /// still reverts before `interestAccrualEndTimestamp`.\n /// @param account The account address to check\n /// @return The available principal amount for the specified account\n function availablePrincipalAmount(\n address account\n ) external view returns (uint256);\n\n /// @notice Gets the total principal funded\n /// @return The total principal funded\n function fundedPrincipalAmount() external view returns (uint256);\n\n /// @notice Gets the total principal reclaimed\n /// @return The total principal reclaimed\n function reclaimedPrincipalAmount() external view returns (uint256);\n\n /// @notice Gets the total principal claimed\n /// @return The total principal claimed\n function claimedPrincipalAmount() external view returns (uint256);\n\n /// @notice Gets the total available principal\n /// @return The total available principal\n function totalAvailablePrincipalAmount() external view returns (uint256);\n\n /// @notice Payment-token amount required to fully fund principal for the current circulating supply\n /// @dev Equals `circulatingBaseUnits * principalAmountPerToken()`, where `circulatingBaseUnits`\n /// is `totalSupply() - balanceOf(this)` of the security token. This is the CEILING\n /// `fundPrincipal` enforces on the running total, not a figure it must equal — the pool\n /// may be filled in tranches. Read it before calling `fundPrincipal` to avoid\n /// `InterestPayment_PrincipalFundingExceedsEntitlement`, and note that redemption opens\n /// only once the running total reaches it.\n ///\n /// The value falls as tokens are soft-burned by principal claims, and rises when new\n /// supply is minted. A supply reduction outside a principal claim (an admin burn or a\n /// force-transfer) can leave the pool ABOVE it, in which case further funding reverts\n /// until new supply is minted or the excess is reclaimed.\n ///\n /// Saturates at `type(uint256).max` if `circulatingBaseUnits * principalAmountPerToken()`\n /// would overflow, rather than reverting and bricking the views that depend on it. That\n /// is an unreachable requirement, so redemption stays shut; `reclaimPrincipal` is the\n /// exit.\n /// @return The payment-token amount needed to cover every outstanding principal entitlement\n function requiredPrincipalFunding() external view returns (uint256);\n\n /// @notice Gets the total accrued interest at the block timestamp\n /// @return The total accrued interest\n function totalAccruedInterest() external view returns (uint256);\n\n /// @notice Gets the total accrued interest at a specific timestamp\n /// @param timestamp The timestamp to get the accrued interest for\n /// @return The total accrued interest\n function totalAccruedInterestAt(\n uint256 timestamp\n ) external view returns (uint256);\n\n /// @notice Triggers early repayment by pausing payments and setting accrual end to a specific timestamp\n /// @param timestamp The timestamp to set the accrual end to\n function earlyRepayment(uint256 timestamp) external;\n\n /// @notice Sets the maximum interest rate\n /// @param maxInterestRate_ The new maximum interest rate in basis points\n function setMaxInterestRate(uint256 maxInterestRate_) external;\n\n /// @notice Updates the interest rate for a specific period\n /// @param periodIdx The period index to update\n /// @param interestRate_ The new interest rate in basis points\n /// @param interestRatePeriodDuration The duration of the interest rate period\n function updateInterestRateForPeriod(\n uint256 periodIdx,\n uint256 interestRate_,\n uint256 interestRatePeriodDuration\n ) external;\n\n /// @notice Gets the restricted lockup token contract\n /// @return The restricted lockup token contract\n function restrictedLockupToken() external view returns (address);\n\n /// @notice Gets the payment token contract\n /// @return The payment token contract\n function paymentToken() external view returns (address);\n\n /// @notice Gets the principal amount per smallest unit of the security token\n /// @dev Denominated in payment-token base units per security-token BASE UNIT, not per whole\n /// token. To express a face value per whole token, configure\n /// `faceValuePerWholeToken / 10 ** restrictedLockupToken.decimals()`; the division must be\n /// exact. The constructor requires the security token's decimals not to exceed the payment\n /// token's, reverting with `InterestPayment_IncompatibleTokenDecimals` otherwise, but that\n /// guard only guarantees an integer exists for face values that are whole multiples of\n /// `10 ** securityTokenDecimals` — at equal decimals the smallest expressible face value\n /// is one whole payment-token unit. A holder's entitlement is `balanceOf(holder) * this value`, with `balanceOf`\n /// in base units. Use `principalAmountPerWholeToken()` for the face value.\n /// @return The principal amount per security-token base unit\n function principalAmountPerToken() external view returns (uint256);\n\n /// @notice Gets the face value per whole security token implied by principalAmountPerToken()\n /// @dev Convenience view for verifying deployment configuration: a value that is\n /// orders of magnitude away from the intended face value means\n /// principalAmountPerToken was configured per whole token instead of per base unit.\n /// Saturates at `type(uint256).max` when the scaling would overflow; that is a signal\n /// the pairing is unusable, not a face value.\n /// @return principalAmountPerToken() scaled by 10 ** securityTokenDecimals\n function principalAmountPerWholeToken() external view returns (uint256);\n\n /// @notice Calculate interest for a specific amount over a time range based on internal payment periods\n /// @dev This function does not use token ownership data. It calculates continuous interest to\n /// `endTimestamp`. It does not round the end down to a payment boundary, but\n /// `accruedInterestAt` and the claim paths do. Thus a raw `block.timestamp` gives more\n /// than the holder can claim now.\n ///\n /// Use this function for quotes. To get a claimable amount, first round the end with\n /// `nearestInterestPaymentTimestampAt`, or use `accruedInterestAt`.\n /// @param amount The amount to calculate interest for (in token units)\n /// @param startTimestamp Start timestamp for interest calculation\n /// @param endTimestamp End timestamp for interest calculation\n /// @return interestAmount The calculated interest amount in payment token units\n function calculateInterestFor(\n uint256 amount,\n uint256 startTimestamp,\n uint256 endTimestamp\n ) external view returns (uint256 interestAmount);\n}\n"},"contracts/interfaces/IRecallablePayment.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {IDividends} from \"./IDividends.sol\";\n\n/**\n * @title IRecallablePayment\n * @notice Interface for a standalone dividend distribution contract with explicit,\n * on-chain per-recipient allocations (arbitrary amounts, not pro-rata).\n * @dev Implements {IDividends} for ABI compatibility with `InterestPayment`. Funds are\n * assigned to recipients explicitly at allocation time instead of being derived\n * from token holdings at a snapshot. Distributions are keyed by an arbitrary\n * `timestamp` identifier.\n */\ninterface IRecallablePayment is IDividends {\n /**\n * @title Events\n */\n\n /// @notice Emitted for each recipient whose allocation is set, updated, or removed in\n /// `createDistribution`\n /// @dev Allocations are overwrite-on-change: `amount` is the recipient's new allocation\n /// (not a delta). `amount == 0` means the recipient was removed; `amount > 0` means\n /// the recipient was created (if previously absent) or updated. Off-chain consumers\n /// can key on `(timestamp, token, recipient)` and upsert when `amount > 0` or delete\n /// when `amount == 0`. The resulting distribution total is readable via\n /// {totalAllocatedAt}.\n /// @param token ERC-20 token address\n /// @param recipient address whose allocation changed\n /// @param timestamp distribution identifier\n /// @param amount the recipient's new allocation (0 = removed)\n /// @param admin transfer admin that made the change\n event AllocationUpdated(\n address indexed token,\n address indexed recipient,\n uint256 indexed timestamp,\n uint256 amount,\n address admin\n );\n\n /// @notice Emitted when a distribution is activated (fully funded at start; claim unlock time assigned)\n /// @dev Allocations remain editable until claiming opens at `unlockedAtTs` (or immediately if\n /// `unlockedAtTs <= block.timestamp`). Indexers must keep processing {AllocationUpdated}\n /// until then — do not treat this event as a frozen allocation snapshot.\n /// @param admin transfer admin that started the distribution\n /// @param token ERC-20 token address\n /// @param timestamp distribution identifier\n /// @param totalAllocated total amount owed to recipients at activation time (point-in-time; may change before claiming opens)\n /// @param totalFunds total amount funded at activation time (>= totalAllocated; point-in-time)\n /// @param unlockedAtTs unix time at/after which recipients may claim (0 = no time lock). Claiming may still be blocked until this time is reached and the distribution remains fully funded.\n event DistributionStarted(\n address indexed admin,\n address indexed token,\n uint256 indexed timestamp,\n uint256 totalAllocated,\n uint256 totalFunds,\n uint256 unlockedAtTs\n );\n\n /// @notice Emitted when a distribution's claim unlock time is changed after start\n /// @param admin transfer admin that changed the unlock time\n /// @param token ERC-20 token address\n /// @param timestamp distribution identifier\n /// @param unlockedAtTs new unix time at/after which recipients may claim\n event DistributionUnlockTimeUpdated(\n address indexed admin,\n address indexed token,\n uint256 indexed timestamp,\n uint256 unlockedAtTs\n );\n\n /// @notice Emitted when the reclaimer address is changed\n /// @param admin address of the admin who changed the reclaimer\n /// @param previousReclaimer address of the previous reclaimer (zero on first assignment)\n /// @param newReclaimer address of the new reclaimer\n event ReclaimerAddressChanged(\n address indexed admin,\n address indexed previousReclaimer,\n address indexed newReclaimer\n );\n\n /// @notice Emitted when a reserve admin migrates a recipient's claiming position to a new wallet\n /// @param token ERC-20 token address\n /// @param timestamp distribution identifier\n /// @param oldWallet wallet losing the position\n /// @param newWallet wallet receiving the position\n /// @param allocatedAmount allocation amount moved from `oldWallet`\n /// @param claimedAmount claimed amount moved from `oldWallet`\n /// @param reclaimedAmount reclaimed amount moved from `oldWallet`\n /// @param admin reserve admin that performed the transfer\n event DividendForceTransferred(\n address indexed token,\n uint256 indexed timestamp,\n address indexed oldWallet,\n address newWallet,\n uint256 allocatedAmount,\n uint256 claimedAmount,\n uint256 reclaimedAmount,\n address admin\n );\n\n /// @notice Emitted when a transfer admin force-claims dividends to a recipient wallet\n /// @param admin transfer admin that triggered the claim\n /// @param wallet recipient wallet that received the funds\n /// @param token ERC-20 token address\n /// @param amount amount of tokens transferred\n /// @param timestamp distribution identifier\n event DividendForceClaimed(\n address indexed admin,\n address indexed wallet,\n address token,\n uint256 amount,\n uint256 indexed timestamp\n );\n\n /**\n * @title State-changing functions\n */\n\n /**\n * @dev Record or correct explicit allocations for recipients without pulling any funds.\n * Phase 1 of the distribution lifecycle. Can be called multiple times for the\n * same `(token, timestamp)` to add recipients in chunks (each call stays under\n * the block gas limit), so the total number of recipients is not bounded by a\n * single transaction. Allocations are overwrite-on-change: each amount replaces\n * the recipient's current allocation (not additive), so admins can correct\n * mistakes by re-submitting only the changed rows. An amount of 0 removes the\n * recipient. Re-submitting a recipient's current amount (including 0 for a\n * recipient that has none) reverts. Allocations stay editable until claiming\n * actually opens — i.e. after auto-start while the distribution is still locked\n * (`block.timestamp < unlockedAtTs`) — and freeze once the unlock time is reached\n * (reverts with `RecallablePayment_DistributionClaimingOpen`). Raising allocations\n * above `totalFunds` is allowed but blocks claiming until the gap is funded.\n * @param token ERC-20 token address\n * @param timestamp distribution identifier\n * @param recipients addresses receiving allocations; the zero address and the\n * distribution contract itself are rejected\n * (`RecallablePayment_InvalidRecipientAddress`)\n * @param amounts new allocation for each recipient (index-aligned with `recipients`; 0 removes)\n */\n function createDistribution(\n address token,\n uint256 timestamp,\n address[] calldata recipients,\n uint256[] calldata amounts\n ) external;\n\n /**\n * @dev Fund a distribution; auto-starts once it is both fully allocated and fully\n * funded (`totalAllocated > 0 && totalFunds >= totalAllocated`), assigning\n * `unlockedAtTs` as the claim-open time. Deposits `amount` of `token`. Requires\n * at least one allocation via {createDistribution} first. May run incrementally\n * until fully funded, and as a top-up after start. Auto-start is only reachable\n * from this function. Funds deposited beyond `totalAllocated` form a surplus\n * recoverable via {reclaimSurplus}. Reverts with\n * `RecallablePayment_NoAllocationsToFund` if no allocations exist yet. The first\n * 4-arg call pins `unlockedAtTs` for the distribution; every later pre-start 4-arg\n * deposit must present the same value or revert with\n * `RecallablePayment_UnlockTimeConflict`. The 3-arg {IDividends-fundDividend} overload\n * carries no unlock and funds freely, honoring the pinned value at auto-start.\n * After auto-start, a 4-arg call with a different value reverts with\n * `RecallablePayment_UnlockTimeNotApplicableAfterStart` (use {setUnlockTime} to reschedule).\n * @param token ERC-20 token address\n * @param amount amount of tokens to fund\n * @param timestamp distribution identifier\n * @param unlockedAtTs unix time at/after which recipients may claim. Pinned on the first\n * 4-arg deposit and committed at auto-start. Accepted as-is with no validation: a\n * past/now value opens claiming immediately, a future value schedules it. Later\n * 4-arg deposits (pre- or post-start) must match the pinned/current value or the call\n * reverts; use {setUnlockTime} to change it after start.\n */\n function fundDividend(\n address token,\n uint256 amount,\n uint256 timestamp,\n uint256 unlockedAtTs\n ) external;\n\n /**\n * @dev Change a started distribution's claim unlock time. Only callable by transfer admin and\n * only while the distribution is still locked (claiming has not yet opened). Not blocked\n * by global pause so admins can reschedule per-distribution claim windows while the\n * contract is paused. The new value may be moved earlier (to open sooner) or later\n * (to postpone), but once `block.timestamp >= unlockedAtTs` the unlock time is frozen\n * and any call reverts with `RecallablePayment_DistributionClaimingOpen`. Reverts with\n * `RecallablePayment_DistributionNotStarted` if not yet started.\n * @param token ERC-20 token address\n * @param timestamp distribution identifier\n * @param unlockedAtTs new unix time at/after which recipients may claim. Passing 0 means\n * \"open immediately\": it is stored and emitted as the current `block.timestamp`,\n * so {unlockTimeAt} never reads back 0 for a started distribution.\n */\n function setUnlockTime(\n address token,\n uint256 timestamp,\n uint256 unlockedAtTs\n ) external;\n\n /**\n * @dev Set the address that receives reclaimed funds.\n * @param reclaimerAddress the new reclaimer address; the zero address and the\n * distribution contract itself are rejected\n * (`RecallablePayment_InvalidReclaimerAddress`), as is the current value\n * (`RecallablePayment_ReclaimerAddressUnchanged`)\n */\n function setReclaimerAddress(address reclaimerAddress) external;\n\n /**\n * @dev Pause or unpause the contract.\n * @param shouldPause whether to pause (true) or unpause (false)\n */\n function pause(bool shouldPause) external;\n\n /**\n * @dev Migrate a recipient's full claiming position from a lost wallet to a new wallet for\n * one `(token, timestamp)` distribution. Callable only by contract admin. Merges into\n * `newWallet` if it already has a position in the same distribution. Must be called once\n * per distribution when the lost wallet has multiple distributions. Not blocked by global\n * pause.\n * Operational: in production the contract admin should be sufficiently decentralized\n * (e.g. a Gnosis Safe), since payment tokens such as USDC may require non-custodial,\n * isolated recovery controls. See docs/recallable-payment.md.\n * @param token ERC-20 token address\n * @param timestamp distribution identifier\n * @param oldWallet wallet losing the position\n * @param newWallet wallet receiving the position; the zero address and the\n * distribution contract itself are rejected\n * (`RecallablePayment_InvalidWalletAddress`)\n */\n function forceTransferDividend(\n address token,\n uint256 timestamp,\n address oldWallet,\n address newWallet\n ) external;\n\n /**\n * @dev Force-claim dividends for a recipient wallet. Only callable by transfer admin.\n * Same claimability gates as {claimDividend}; funds are sent to `wallet`, not the reclaimer.\n * @param token ERC-20 token address\n * @param wallet recipient wallet to claim for; the zero address and the\n * distribution contract itself are rejected\n * (`RecallablePayment_InvalidWalletAddress`)\n * @param timestamp distribution identifier\n * @param amount amount to claim (0 = claim all unclaimed)\n */\n function forceClaimDividend(\n address token,\n address wallet,\n uint256 timestamp,\n uint256 amount\n ) external;\n\n /**\n * @dev Batch force-claim dividends for a recipient wallet across multiple distributions.\n * Only callable by transfer admin.\n * @param token ERC-20 token address\n * @param wallet recipient wallet to claim for\n * @param timestamps distribution identifiers\n * @param amounts amounts to claim per timestamp (0 = claim all unclaimed)\n */\n function batchForceClaimDividend(\n address token,\n address wallet,\n uint256[] calldata timestamps,\n uint256[] calldata amounts\n ) external;\n\n /**\n * @dev Reclaim the distribution's surplus — funds deposited beyond what was allocated to\n * recipients (`totalFunds - totalAllocated`) — and send it to the reclaimer address.\n * This can never touch amounts owed to recipients, so it is intentionally NOT gated on\n * `started` or on whether recipients have already claimed.\n * Reverts with `RecallablePayment_NoFundsToClaim` when there is no surplus\n * (`totalFunds <= totalAllocated`). If `amount` is 0, reclaims the entire surplus;\n * an `amount` above the surplus reverts with `RecallablePayment_NotEnoughFundsToClaim`.\n * Callable only by transfer admin, while not paused, and requires a reclaimer address to\n * be set. Use {reclaimDividend} to recover a specific recipient's unclaimed allocation.\n * Use {reclaimTotalDividend} to recall the distribution's entire remaining pool instead\n * (only permitted before any claims/reclaims have occurred).\n * @param token ERC-20 token address\n * @param amount amount of surplus to reclaim (0 = reclaim all surplus)\n * @param timestamp distribution identifier\n */\n function reclaimSurplus(\n address token,\n uint256 amount,\n uint256 timestamp\n ) external;\n\n /**\n * @dev Reclaim the distribution's entire remaining funded balance (`totalFunds`) and send it\n * to the reclaimer address, per the {IDividends} contract: only callable while nothing\n * has been claimed or reclaimed for this `(token, timestamp)` distribution\n * (`totalClaimed == 0 && totalReclaimed == 0`). Reverts with\n * `RecallablePayment_DividendsAlreadyClaimed` if any claim or reclaim has occurred.\n * Reverts with `RecallablePayment_NoFundsToClaim` when `totalFunds == 0`. If `amount` is\n * 0, reclaims all remaining funds; an `amount` above `totalFunds` reverts with\n * `RecallablePayment_NotEnoughFundsToClaim`. Not gated on `started` or on the unlock\n * time. Decrementing `totalFunds` below `totalAllocated` makes the distribution\n * under-funded: claims and {reclaimDividend} block until it is topped up again (or\n * allocations are lowered via {createDistribution}), and a not-yet-started distribution\n * stays revivable by funding it again. Callable only by transfer admin, while not\n * paused, and requires a reclaimer address to be set. Use {reclaimSurplus} to recover\n * only the over-funded portion without disturbing recipient entitlements.\n * @param token ERC-20 token address\n * @param amount amount to reclaim (0 = reclaim all remaining funds)\n * @param timestamp distribution identifier\n */\n function reclaimTotalDividend(\n address token,\n uint256 amount,\n uint256 timestamp\n ) external override;\n\n /**\n * @title View functions\n */\n\n /// @dev Whether a distribution has been activated (claim time assigned; it was fully funded at\n /// the moment it started). This is the raw `started` flag: it does not imply allocations\n /// are currently frozen (they freeze at `unlockedAtTs`) nor that the distribution is still\n /// fully funded (a later allocation edit may raise `totalAllocated` above `totalFunds`).\n /// Use {isClaimable} for the combined \"can recipients claim right now\" check.\n function isStarted(\n address token,\n uint256 timestamp\n ) external view returns (bool);\n\n /// @dev Unix time at/after which recipients may claim. Returns 0 only before the\n /// distribution starts (no unlock pinned yet); a started distribution always\n /// reports the actual claim-open time (start paths and {setUnlockTime} normalize\n /// a 0 input to `block.timestamp`).\n function unlockTimeAt(\n address token,\n uint256 timestamp\n ) external view returns (uint256);\n\n /// @dev Whether recipients can claim right now: started, the unlock time has been reached, and\n /// the distribution is fully funded for its current allocations (`totalFunds >= totalAllocated`)\n function isClaimable(\n address token,\n uint256 timestamp\n ) external view returns (bool);\n\n /// @dev Total currently allocated across all recipients of a distribution\n function totalAllocatedAt(\n address token,\n uint256 timestamp\n ) external view returns (uint256);\n\n /// @dev Amount allocated to a recipient\n function allocatedBalanceAt(\n address token,\n address recipient,\n uint256 timestamp\n ) external view returns (uint256);\n\n /// @dev Amount reclaimed from a recipient by an admin\n function reclaimedBalanceAt(\n address token,\n address recipient,\n uint256 timestamp\n ) external view returns (uint256);\n\n /// @dev Amount a recipient can claim right now for a distribution. Unlike the raw\n /// entitlement (`allocated - claimed - reclaimed`), this is gated on {isClaimable}:\n /// it returns 0 while the distribution has not started, is still time-locked\n /// (`block.timestamp < unlockedAtTs`), or is under-funded (`totalFunds <\n /// totalAllocated`) — this overrides the {IDividends} description, which assumes\n /// InterestPayment's funding-at-snapshot semantics where a non-zero balance is\n /// always immediately claimable. A non-zero return here is a guarantee that\n /// {claimDividend} for that amount will succeed. Use {allocatedBalanceAt},\n /// {claimedBalanceAt}, and {reclaimedBalanceAt} to inspect raw entitlement for a\n /// pending or locked distribution.\n function unclaimedBalanceAt(\n address token,\n address receiver,\n uint256 timestamp\n ) external view override returns (uint256);\n}\n"},"contracts/interfaces/IRestrictedLockupToken.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport \"./IIdentityRegistry.sol\";\nimport \"./IAccessControl.sol\";\n\n// Interface for integration with Restricted Lockup Token\ninterface IRestrictedLockupToken {\n struct Timelock {\n address funder;\n uint256 scheduleId;\n uint256 commencementTimestamp;\n uint256 tokensTransferred;\n uint256 totalAmount;\n uint256 globalHoldingIndex; // Global index for token type + daysAfterDeployment combination\n address[] cancelableBy; // not cancelable unless set at the time of funding\n }\n struct FundReleaseScheduleParams {\n address to;\n uint256 amount;\n uint256 commencementTimestamp;\n uint256 scheduleId;\n uint256 holdingIdx;\n }\n // Holdings (wallet-scoped)\n function holdingOf(address who_, uint256 index_) external view returns (uint256, uint256, uint256);\n function holdingCountOf(address who_) external view returns (uint256);\n function previewTransferableFromHoldings(\n address from,\n address to,\n uint256 expectedTransferableAmount\n ) external view returns (uint256 transferableAmount);\n\n // Global holdings introspection (token-type scoped)\n function globalHoldingCount() external view returns (uint256);\n function getWalletIndexesLength(address who_) external view returns (uint256);\n function getWalletIndexes(address who_, uint256 index) external view returns (uint256);\n\n // Identity/registry\n function isAmlKycPassed(address who_) external view returns (bool);\n function identityRegistry() external view returns (IIdentityRegistry);\n function isValidIdentityRegistry(address identityRegistry) external view returns (bool);\n\n // Balances\n function balanceOf(address who_) external view returns (uint256);\n function unlockedTotalBalanceOf(address who_) external view returns (uint256);\n function unlockedBalanceOfTimelock(address who_, uint256 index_) external view returns (uint256);\n function isPaused() external view returns (bool);\n function getFrozenStatus(address who_) external view returns (bool);\n\n // Timelocks\n function timelockCountOf(address who_) external view returns (uint256);\n function timelockOf(address who_, uint256 index_) external view returns (Timelock memory);\n\n // Administrative token type updates\n function updateHoldingTokenType(address wallet, uint256 oldTokenType, uint256 newTokenType, uint256 mintTimestamp, uint256 amount) external;\n function updateTimelockTokenType(address wallet, uint256 timelockIndex, uint256 newTokenType) external;\n \n // Burn functions\n function burn(address from_, uint256 value_) external;\n function softBurn(address from_, uint256 value_) external;\n function burnHolding(address from_, uint256 globalHoldingIdx, uint256 amount) external;\n\n function snapshotPeriodsAddress() external view returns (address);\n\n function totalSupply() external view returns (uint256);\n function decimals() external view returns (uint8);\n\n // Mint functions\n function mint(address to, uint256 amount) external;\n function mintTokenType(address to, uint256 amount, uint256 tokenType) external;\n\n // Transfer functions\n function transfer(address to, uint256 amount) external returns (bool);\n function batchTransfer(address[] calldata to, uint256[] calldata amounts) external returns (bool);\n function transferFrom(address from, address to, uint256 amount) external returns (bool);\n function forceTransferBetween(address from, address to, uint256 amount) external;\n function transferHolding(address to, uint256 amount, uint256 globalHoldingIdx) external returns (bool);\n\n // Max total supply\n function setMaxTotalSupply(uint256 maxTotalSupply) external;\n\n // Transfer rules\n function transferRules() external view returns (address);\n function isValidTransferRules(address transferRules) external view returns (bool);\n function accessControl() external view returns (IAccessControl);\n function restrictedLockupTokenExtension() external view returns (address);\n function restrictedLockupTokenManagementExtension() external view returns (address);\n\n // token type view functions\n function existingTokenTypesCount() external view returns (uint256);\n function existingTokenTypes(uint256 index) external view returns (uint256);\n function tokenTypeExists(uint256 tokenType) external view returns (bool);\n\n // Holder Management Functions\n function createHolderFromAddress(address addr_) external returns (uint256);\n function appendHolderAddress(address addr_, uint256 holderId_) external;\n function addHolderWithAddresses(address[] calldata addresses_) external returns (uint256);\n function removeHolder(uint256 holderId_) external;\n function removeWalletFromHolder(address addr_) external;\n function batchRemoveWalletFromHolder(address[] calldata addresses_) external;\n function getHolderAddresses(uint256 holderId_) external view returns (address[] memory);\n function getHolderId(address addr_) external view returns (uint256);\n function addressHasHolder(address addr_) external view returns (bool);\n function holderExists(uint256 holderId_) external view returns (bool);\n function setHolderMax(uint256 holderMax_) external;\n\n // Admin Functions\n function pause(bool isPaused_) external;\n function freeze(address addr_, bool status_) external;\n\n // Upgrade Functions\n function upgradeTransferRules(address newTransferRules) external;\n function upgradeIdentityRegistry(address newIdentityRegistry) external;\n\n // Release Schedule Functions\n function createReleaseSchedule(\n uint256 releaseCount,\n uint256 delayUntilFirstReleaseInSeconds,\n uint256 initialReleasePortionInBips,\n uint256 periodBetweenReleasesInSeconds\n ) external returns (uint256);\n \n function fundReleaseScheduleWithHolding(\n FundReleaseScheduleParams memory params,\n address[] memory cancelableBy_\n ) external returns (bool);\n \n function fundReleaseSchedule(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_,\n uint256 scheduleId_,\n address[] memory cancelableBy_\n ) external returns (bool);\n \n function batchFundReleaseScheduleWithHolding(\n FundReleaseScheduleParams[] memory params,\n address[] memory cancelableBy_\n ) external returns (bool);\n \n function batchFundReleaseSchedule(\n address[] memory to_,\n uint256[] memory amounts_,\n uint256[] memory commencementTimestamps_,\n uint256[] memory scheduleIds_,\n address[] memory cancelableBy_\n ) external returns (bool);\n \n function mintReleaseScheduleTokenType(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_,\n uint256 scheduleId_,\n uint256 tokenType_,\n address[] memory cancelableBy_\n ) external returns (bool);\n \n function mintReleaseSchedule(\n address to_,\n uint256 amount_,\n uint256 commencementTimestamp_,\n uint256 scheduleId_,\n address[] memory cancelableBy_\n ) external returns (bool);\n \n function batchMintReleaseScheduleTokenType(\n address[] calldata to,\n uint256[] calldata amounts,\n uint256[] calldata commencementTimestamps,\n uint256[] calldata scheduleIds,\n uint256[] calldata tokenTypes,\n address[] calldata cancelableBy\n ) external returns (bool);\n \n function batchMintReleaseSchedule(\n address[] calldata to,\n uint256[] calldata amounts,\n uint256[] calldata commencementTimestamps,\n uint256[] calldata scheduleIds,\n address[] calldata cancelableBy\n ) external returns (bool);\n\n // Additional Functions\n function safeApprove(address spender_, uint256 value_) external;\n\n // Supply Functions\n function maxTotalSupply() external view returns (uint256);\n function circulatingTokenSupply() external view returns (uint256);\n function unissuedTokenSupply() external view returns (uint256);\n\n // Token Type Functions\n function determineTokenType(address wallet) external view returns (uint256);\n function totalBalanceOf(address account) external view returns (uint256);\n\n // Transfer Restriction Functions\n // NOTE: this header mirrors the RestrictedLockupToken implementation, which returns uint256\n // restriction codes. IERC1404.sol provides the ERC-1404-shaped uint8 view for standard consumers;\n // both decode the same 32-byte word on the wire.\n function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint256);\n function detectTransferRestrictionFor(\n uint256 tokenType,\n uint256 mintTimestamp,\n IIdentityRegistry.IdentityInfo memory recipientIdentity,\n bool isAmlKycPassed\n ) external view returns (uint256);\n function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory);\n\n // Timelock Functions\n function balanceOfTimelock(address who_, uint256 index_) external view returns (uint256);\n function cancelTimelock(address who_, uint256 timelockIndex_, uint256 scheduleId_, uint256 commencementTimestamp_, uint256 totalAmount_, address reclaimTokenTo_) external returns (bool);\n function transferTimelock(address to, uint256 value, uint256 timelockId) external returns (bool);\n\n // Schedule Functions\n function scheduleCount() external view returns (uint256);\n\n // Configuration Functions\n function setRecordMintTimestamp(bool enabled) external;\n \n /**\n * @dev Get token type and mint timestamp from global holding index\n * @param globalIndex The global holding index\n * @return tokenType The token type\n * @return mintTimestamp The mint timestamp\n */\n function getTokenTypeAndMintTimestampFromGlobalIndex(uint256 globalIndex) external view returns (uint256 tokenType, uint256 mintTimestamp); \n}\n"},"contracts/interfaces/IRestrictedLockupTokenExtension.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\n\n// Interface for integration with Restricted Lockup Token Extension\ninterface IRestrictedLockupTokenExtension {\n function maxTotalSupply() external view returns (uint256);\n function slotsPerWord() external view returns (uint256);\n}\n"},"contracts/interfaces/IRestrictedSwap.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\nimport \"@openzeppelin/contracts/utils/introspection/IERC165.sol\";\n\ninterface IRestrictedSwap is IERC165 {\n /**\n * @title Data Structures\n */\n enum SwapStatus {\n SellConfigured,\n BuyConfigured,\n Complete,\n Canceled\n }\n\n struct Swap {\n address restrictedTokenSender;\n address quoteTokenSender;\n address quoteToken;\n uint256 restrictedTokenAmount;\n uint256 quoteTokenAmount;\n SwapStatus status;\n // Absolute UNIX timestamp at which the swap expires. Required (must be > 0), must be\n // strictly in the future at configure time, and must sit within `maxSwapLifetime`\n // seconds of `block.timestamp` at configure time.\n uint256 deadline;\n // Remaining unfilled amount on the restricted side. Initialized to restrictedTokenAmount\n // at configure, decremented by each partial fill of an open order, and resized in place\n // by decreaseOrder / increaseOrder (an increase may push it ABOVE restrictedTokenAmount).\n // Only OPEN orders decrement it: a closed swap settles its ORIGINAL amounts in full, so it\n // reports the configure-time value for its whole lifetime.\n // Terminal states: a `Complete` swap always reports zero. A `Canceled` swap RETAINS the\n // amount withdrawn unfilled, deliberately kept as on-chain history — zero only when the\n // order was retired by shrinking to zero (decreaseOrder / parent-offer deduction), where\n // the shrink itself had already consumed the remainder.\n uint256 remainingRestrictedTokenAmount;\n // Remaining unfilled amount on the quote side. Adjusted by exact pro-rata on the\n // ORIGINAL price ratio at each fill and resize — amounts must divide cleanly (see\n // `_computeFillQuote`), so this field reaches zero exactly when\n // `remainingRestrictedTokenAmount` does, including on completion. On cancellation both\n // fields keep their last on-ratio values, so the pairing invariant still holds.\n uint256 remainingQuoteTokenAmount;\n // Minimum restricted-token amount a taker must fill on an OPEN order via\n // `takeOpenSell` / `takeOpenBuy`, unless the taker takes the entire remaining amount\n // (a below-minimum tail can always be taken in full). 0 means no minimum. Set once at\n // configure time; not enforced on owner resizes or bid settlement. Always 0 for closed\n // swaps — configure reverts otherwise (they settle in full and never consult it).\n uint256 minimumFillAmount;\n }\n\n /**\n * @title Functions\n */\n\n /**\n * @dev Configure swap and emit an event with new swap number.\n * Pass `token2Address = address(0)` to create an OPEN SELL ORDER that any compliant\n * buyer can fill (fully or partially) via `takeOpenSell`. Pass a concrete address to\n * create a traditional closed swap pinned to that buyer.\n * @param restrictedTokenAmount the required amount for the erc1404Sender to send\n * @param quoteToken the address of an erc1404 or erc20 that will be swapped\n * @param token2Address the address that is approved to fund quoteToken (or address(0) for open order)\n * @param quoteTokenAmount the required amount of quoteToken to swap\n * @param deadline absolute UNIX timestamp at which the swap expires. Must be > 0,\n * strictly in the future, and within `maxSwapLifetime` seconds of `block.timestamp`.\n * @param minimumFillAmount minimum restricted-token amount a taker must fill on this order\n * via `takeOpenSell` (unless taking the entire remaining amount). Must be <=\n * `restrictedTokenAmount`. 0 means no minimum. Must be 0 for closed swaps —\n * they settle in full and never consult it.\n */\n function configureSell(\n uint256 restrictedTokenAmount,\n address quoteToken,\n address token2Address,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount\n ) external;\n\n /**\n * @dev Configure swap and emit an event with new swap number.\n * Pass `restrictedTokenSender = address(0)` to create an OPEN BUY ORDER that any compliant\n * holder can sell into via `takeOpenBuy`. Pass a concrete address to create a traditional\n * closed swap pinned to that seller.\n * @param restrictedTokenAmount the required amount for the erc1404Sender to send\n * @param restrictedTokenSender restricted token sender (or address(0) for open order)\n * @param quoteToken the address of an erc1404 or erc20 that will be swapped\n * @param quoteTokenAmount the required amount of quoteToken to swap\n * @param deadline absolute UNIX timestamp at which the swap expires. Must be > 0,\n * strictly in the future, and within `maxSwapLifetime` seconds of `block.timestamp`.\n * @param minimumFillAmount minimum restricted-token amount a taker must fill on this order\n * via `takeOpenBuy` (unless taking the entire remaining amount). Must be <=\n * `restrictedTokenAmount`. 0 means no minimum. Must be 0 for closed swaps —\n * they settle in full and never consult it.\n */\n function configureBuy(\n uint256 restrictedTokenAmount,\n address restrictedTokenSender,\n address quoteToken,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount\n ) external;\n\n /**\n * @dev Configure sell using an EIP-2612 permit signature on the restricted token.\n * Sign `permitValue` as `requiredAllowance(seller, restrictedToken) +\n * restrictedTokenAmount`: `permit` OVERWRITES the allowance, so a value covering only\n * this order would clobber the allowance backing the seller's other active orders.\n * @param restrictedTokenAmount the required amount for the erc1404Sender to send\n * @param quoteToken the address of an erc1404 or erc20 that will be swapped\n * @param quoteTokenSender the address that is approved to fund quoteToken\n * @param quoteTokenAmount the required amount of quoteToken to swap\n * @param deadline absolute UNIX timestamp at which the swap expires. Must be > 0,\n * strictly in the future, and within `maxSwapLifetime` seconds of `block.timestamp`.\n * @param minimumFillAmount minimum restricted-token amount a taker must fill on this order\n * via `takeOpenSell` (unless taking the entire remaining amount). Must be <=\n * `restrictedTokenAmount`. 0 means no minimum. Must be 0 for closed swaps —\n * they settle in full and never consult it.\n * @param permitValue the EIP-2612 `value` the caller signed. Forwarded to `permit`\n * verbatim and never recomputed on-chain, so it must still cover the requirement\n * below when the transaction actually executes — see the sizing rule above.\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function configureSellWithPermit(\n uint256 restrictedTokenAmount,\n address quoteToken,\n address quoteTokenSender,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev Configure buy using an EIP-2612 permit signature on the quote token.\n * Sign `permitValue` as `requiredAllowance(buyer, quoteToken) + quoteTokenAmount`:\n * `permit` OVERWRITES the allowance, so a value covering only this order would\n * clobber the allowance backing the buyer's other active buy orders.\n * @param restrictedTokenAmount the required amount for the erc1404Sender to send\n * @param restrictedTokenSender restricted token sender\n * @param quoteToken the address of an erc1404 or erc20 that will be swapped\n * @param quoteTokenAmount the required amount of quoteToken to swap\n * @param deadline absolute UNIX timestamp at which the swap expires. Must be > 0,\n * strictly in the future, and within `maxSwapLifetime` seconds of `block.timestamp`.\n * @param minimumFillAmount minimum restricted-token amount a taker must fill on this order\n * via `takeOpenBuy` (unless taking the entire remaining amount). Must be <=\n * `restrictedTokenAmount`. 0 means no minimum. Must be 0 for closed swaps —\n * they settle in full and never consult it.\n * @param permitValue the EIP-2612 `value` the caller signed. Forwarded to `permit`\n * verbatim and never recomputed on-chain, so it must still cover the requirement\n * below when the transaction actually executes — see the sizing rule above.\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function configureBuyWithPermit(\n uint256 restrictedTokenAmount,\n address restrictedTokenSender,\n address quoteToken,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev Complete swap with quote token\n * @param swapNumber swap number\n */\n function completeSwapWithQuoteToken(uint256 swapNumber) external;\n\n /**\n * @dev Complete swap with quote token using an EIP-2612 permit signature.\n * Sign `permitValue` as `requiredAllowance(buyer, quoteToken) + quoteTokenAmount`:\n * `permit` OVERWRITES the allowance, so a settlement-only value would clobber the\n * allowance backing the buyer's other active buy orders.\n * @param swapNumber swap number\n * @param permitValue the EIP-2612 `value` the caller signed. Forwarded to `permit`\n * verbatim and never recomputed on-chain, so it must still cover the requirement\n * below when the transaction actually executes — see the sizing rule above.\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function completeSwapWithQuoteTokenPermit(\n uint256 swapNumber,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev Complete swap with restricted token (the seller settling a bid/closed buy order).\n * `sellSwapNumber` optionally names the seller's own OPEN sell offer the accepted bid\n * was drawn from: the offer's remaining size is shrunk by the bid amount in the same\n * transaction (emitting `OrderResized`), so it stops advertising inventory this\n * settlement just consumed and can never be over-filled afterwards. Pass 0 for a\n * standalone bid that does not draw from any open offer. A named parent must be an\n * open sell owned by the caller, with remaining size >= the bid amount, and the bid\n * amount must divide cleanly against the offer's price ratio (same exact-pricing rule\n * as partial fills); shrinking to exactly zero cancels the offer.\n * @param swapNumber swap number of the bid (closed buy order naming the caller as seller)\n * @param sellSwapNumber swap number of the caller's open sell offer to deduct from (0 = none)\n */\n function completeSwapWithRestrictedToken(\n uint256 swapNumber,\n uint256 sellSwapNumber\n ) external;\n\n /**\n * @dev Complete swap with restricted token using an EIP-2612 permit signature on the\n * restricted token — the seller's approve + settle in one atomic transaction, mirroring\n * `completeSwapWithQuoteTokenPermit` on the quote side. Because `permit` OVERWRITES the\n * allowance, `permitValue` must cover the seller's existing reservations, not just\n * this settlement: sign `requiredAllowance(seller, restrictedToken) + restrictedTokenAmount`\n * when `sellSwapNumber == 0`, or exactly `requiredAllowance(seller, restrictedToken)` when\n * a parent offer is named (the parent's reservation already covers the bid amount).\n * @param swapNumber swap number of the bid (closed buy order naming the caller as seller)\n * @param sellSwapNumber swap number of the caller's open sell offer to deduct from (0 = none)\n * @param permitValue the EIP-2612 `value` the caller signed. Forwarded to `permit`\n * verbatim and never recomputed on-chain, so it must still cover the requirement\n * below when the transaction actually executes — see the sizing rule above.\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function completeSwapWithRestrictedTokenPermit(\n uint256 swapNumber,\n uint256 sellSwapNumber,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev Shrink an active OPEN order's remaining size in place — owner-only, decrease-only.\n * The single-signature alternative to cancel-and-recreate when an investor's intent\n * drops below the advertised size. The paired quote amount shrinks exactly pro-rata on\n * the order's original price (same strict divisibility rule as partial fills — a\n * `newRemainingRestrictedAmount` that does not produce an integer quote remainder\n * reverts). Shrinking to zero cancels the order. Closed swaps (both counterparties\n * named) revert: they settle their original configured amounts, so an in-place resize\n * would corrupt the reservation accounting. Like `cancelSwap`, callable while paused\n * and on expired orders (exit-type action).\n * @param swapNumber swap number of the caller's open order\n * @param newRemainingRestrictedAmount new remaining restricted-token size; must be strictly\n * less than the current remaining amount (0 cancels the order)\n */\n function decreaseOrder(\n uint256 swapNumber,\n uint256 newRemainingRestrictedAmount\n ) external;\n\n /**\n * @dev Grow an active OPEN order's remaining size in place — owner-only, increase-only.\n * Unlike the exit-type `decreaseOrder`, growing an order CREATES new exposure, so\n * this carries configure-grade guards: blocked while paused, blocked on expired\n * orders, and the creator's allowance must already cover all reservations plus the\n * growth (`requiredAllowance + added` on the restricted side for sells, or\n * `requiredAllowance + quoteAdded` on the quote side for buys; buys also re-check\n * quote balance against the order's new total). The added amount must divide cleanly\n * against the order's ORIGINAL price ratio — the price cannot change. After an\n * increase the remaining size may exceed the originally configured amount; derive\n * traded totals from `OpenSwapFilled` events, not configured amounts.\n * @param swapNumber swap number of the caller's open order\n * @param newRemainingRestrictedAmount new remaining restricted-token size; must be\n * strictly greater than the current remaining amount\n */\n function increaseOrder(\n uint256 swapNumber,\n uint256 newRemainingRestrictedAmount\n ) external;\n\n /**\n * @dev `increaseOrder` with an EIP-2612 permit bundled in, so the allowance top-up and\n * the size increase land atomically in one transaction. The permitted token depends\n * on the order side: the restricted token for an open sell, the order's quote token\n * for an open buy. Because `permit` overwrites the allowance, `permitValue` must be\n * `requiredAllowance + added` (sell) or `requiredAllowance + quoteAdded` (buy). Only\n * one side's permit is ever consumed, so one signed value covers either branch.\n * @param swapNumber swap number of the caller's open order\n * @param newRemainingRestrictedAmount new remaining restricted-token size (strictly greater)\n * @param permitValue the EIP-2612 `value` the caller signed. Forwarded to `permit`\n * verbatim and never recomputed on-chain, so it must still cover the requirement\n * below when the transaction actually executes — see the sizing rule above.\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function increaseOrderWithPermit(\n uint256 swapNumber,\n uint256 newRemainingRestrictedAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev Fill an open sell order (configured with `quoteTokenSender = address(0)`). Caller pays\n * a pro-rata `fillQuoteAmount` of the quote token (ceil-rounded; the final fill of a\n * multi-buyer order collects the exact residual). Caller must hold sufficient quote token\n * allowance to this contract.\n * @param swapNumber swap number\n * @param fillAmount amount of restricted token to receive (must be > 0 and <= remaining)\n */\n function takeOpenSell(uint256 swapNumber, uint256 fillAmount) external;\n\n /**\n * @dev Fill an open sell order using an EIP-2612 permit signature on the quote token, so the\n * taker can approve + fill in a single relayer-driven tx.\n * Sign `permitValue` as `requiredAllowance(taker, quoteToken) + fillQuoteAmount` —\n * EIP-2612 `permit` overwrites the allowance, so a fill-only value would clobber the\n * allowance backing the taker's other active buy orders on the same quote token.\n * @param swapNumber swap number\n * @param fillAmount amount of restricted token to receive\n * @param permitValue the EIP-2612 `value` the caller signed. Forwarded to `permit`\n * verbatim and never recomputed on-chain, so it must still cover the requirement\n * below when the transaction actually executes — see the sizing rule above.\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function takeOpenSellWithPermit(\n uint256 swapNumber,\n uint256 fillAmount,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev Fill an open buy order (configured with `restrictedTokenSender = address(0)`). Caller\n * delivers `fillAmount` of the restricted token and receives the pro-rata quote token.\n * Caller must hold sufficient restricted-token allowance to this contract.\n * `sellSwapNumber` optionally names the caller's own OPEN sell offer the delivered\n * tokens are drawn from; the offer is shrunk by `fillAmount` atomically in the same\n * transaction (emitting `OrderResized`). Pass 0 when the fill draws from no open offer.\n * When linked, `fillAmount` must also divide cleanly against the parent offer's price\n * ratio, or the call reverts — callers whose fill does not divide must pass 0.\n * @param swapNumber swap number\n * @param fillAmount amount of restricted token to deliver\n * @param sellSwapNumber swap number of the caller's open sell offer to deduct from (0 = none)\n */\n function takeOpenBuy(\n uint256 swapNumber,\n uint256 fillAmount,\n uint256 sellSwapNumber\n ) external;\n\n /**\n * @dev Fill an open buy order using an EIP-2612 permit signature on the restricted token.\n * `permitValue` sizing (EIP-2612 `permit` overwrites the allowance): with\n * `sellSwapNumber == 0` sign `requiredAllowance(taker, restrictedToken) + fillAmount`;\n * with a parent offer\n * named sign exactly `requiredAllowance(taker, restrictedToken)` — the parent's\n * reservation already covers the fill since the offer shrinks in the same transaction.\n * @param swapNumber swap number\n * @param fillAmount amount of restricted token to deliver\n * @param sellSwapNumber swap number of the caller's open sell offer to deduct from (0 = none)\n * @param permitValue the EIP-2612 `value` the caller signed. Forwarded to `permit`\n * verbatim and never recomputed on-chain, so it must still cover the requirement\n * below when the transaction actually executes — see the sizing rule above.\n * @param permitDeadline EIP-2612 permit deadline (must be >= block.timestamp)\n * @param v signature v\n * @param r signature r\n * @param s signature s\n */\n function takeOpenBuyWithPermit(\n uint256 swapNumber,\n uint256 fillAmount,\n uint256 sellSwapNumber,\n uint256 permitValue,\n uint256 permitDeadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n /**\n * @dev cancel swap\n * @param swapNumber swap number\n */\n function cancelSwap(uint256 swapNumber) external;\n\n /**\n * @dev Hard compile-time ceiling on `maxSwapLifetime`. The constructor refuses any value\n * above this constant.\n */\n function MAX_SWAP_LIFETIME_LIMIT() external view returns (uint256);\n\n /**\n * @dev Hard compile-time floor on `maxSwapLifetime`. The constructor refuses any value\n * below this constant.\n */\n function MIN_SWAP_LIFETIME_LIMIT() external view returns (uint256);\n\n /**\n * @dev Maximum swap lifetime in seconds, fixed at deployment. Constrains how far in the\n * future `deadline` may sit at configure time: `deadline - block.timestamp <= maxSwapLifetime`.\n */\n function maxSwapLifetime() external view returns (uint256);\n\n /**\n * @dev Returns the swap status if exists\n * @param swapNumber swap number\n * @return SwapStatus status of the swap record\n */\n function swapStatus(uint256 swapNumber) external view returns (SwapStatus);\n\n /**\n * @dev Check if a swap has expired based on its deadline\n * @param swapNumber swap number\n * @return bool true if the swap has expired, false otherwise\n */\n function isSwapExpired(uint256 swapNumber) external view returns (bool);\n\n /**\n * @dev Minimum restricted-token amount a taker must fill on this order via `takeOpenSell` /\n * `takeOpenBuy`, unless taking the entire remaining amount. 0 means no minimum.\n * @param swapNumber swap number\n * @return uint256 the configured minimum fill amount\n */\n function minimumFillAmount(uint256 swapNumber) external view returns (uint256);\n\n /**\n * @dev Pending restricted-token delivery obligations for `addr` from CLOSED buy orders only.\n * Open buy orders (configured with `restrictedTokenSender == address(0)`) are filled via\n * `takeOpenBuy` with atomic settlement and do NOT contribute to this value — the seller\n * is unknown at configure time and delivery happens in the same call as the take, so no\n * pending window ever exists for a read to observe. Integrators that need full\n * delivery-obligation coverage must also index `OpenSwapFilled` events.\n * @param addr Seller address (the named `restrictedTokenSender` on a `configureBuy` call).\n * @return Pending restricted-token amount owed by `addr` across active closed buy orders.\n */\n function pendingBuys(address addr) external view returns (uint256);\n\n /**\n * @dev Pending restricted-token delivery obligations for `addr` across active sell orders\n * (both closed and open sells — unlike `pendingBuys`, the seller is always the\n * configurer and is known at `configureSell` time).\n * @param addr Seller address.\n * @return Pending restricted-token amount that `addr` is obligated to deliver.\n */\n function pendingSells(address addr) external view returns (uint256);\n\n /****************************\n * Events\n ****************************/\n\n /// @notice This event is emitted when swap is canceled\n /// @param sender address of canceler\n /// @param swapNumber swap number\n event SwapCanceled(address indexed sender, uint256 indexed swapNumber);\n\n /// @notice This event is emitted when swap is configured\n /// @param swapNumber swap number\n /// @param restrictedTokenSender address of restricted token sender\n /// @param restrictedTokenAmount amount of restricted token\n /// @param quoteToken address of quote token\n /// @param quoteTokenSender address of quote token sender\n /// @param quoteTokenAmount amount of quote token\n /// @param minimumFillAmount minimum restricted-token amount a taker must fill on this order\n /// (unless taking the entire remaining amount); 0 means no minimum\n event SwapConfigured(\n uint256 indexed swapNumber,\n address indexed restrictedTokenSender,\n uint256 restrictedTokenAmount,\n address quoteToken,\n address indexed quoteTokenSender,\n uint256 quoteTokenAmount,\n uint256 deadline,\n uint256 minimumFillAmount\n );\n\n /// @notice This event is emitted when swap is completed\n /// @param swapNumber swap number\n /// @param restrictedTokenSender address of restricted token sender\n /// @param restrictedTokenAmount amount of restricted token\n /// @param quoteTokenSender address of quote token\n /// @param quoteToken address of quote token\n /// @param quoteTokenAmount amount of quote token\n event SwapComplete(\n uint256 indexed swapNumber,\n address indexed restrictedTokenSender,\n uint256 restrictedTokenAmount,\n address indexed quoteTokenSender,\n address quoteToken,\n uint256 quoteTokenAmount,\n uint256 deadline\n );\n\n /// @notice Emitted when an open order's remaining size changes WITHOUT a fill — the owner\n /// called `decreaseOrder` or `increaseOrder`, or an accepted bid was deducted from\n /// its parent offer via `completeSwapWithRestrictedToken(bid, parent)`. Off-chain\n /// indexers must handle this alongside `OpenSwapFilled` to keep displayed sizes\n /// honest — and note the size can both shrink and GROW (possibly above the\n /// originally configured amount). If a shrink reaches zero, `SwapCanceled` is\n /// emitted in the same transaction and the order is terminal.\n /// @param swapNumber swap number of the resized order\n /// @param newRemainingRestrictedAmount restricted-token amount still offered after the resize\n /// @param newRemainingQuoteAmount quote-token amount still expected after the resize\n event OrderResized(\n uint256 indexed swapNumber,\n uint256 newRemainingRestrictedAmount,\n uint256 newRemainingQuoteAmount\n );\n\n /// @notice Emitted on every partial OR final fill of an open order.\n /// `remainingRestrictedAmount == 0` indicates the order is now fully filled\n /// (status transitions to `Complete` and `SwapComplete` is also emitted with the last filler).\n /// @param swapNumber swap number\n /// @param filler address that took the fill (the actual counterparty for this slice)\n /// @param fillRestrictedAmount restricted-token amount transferred in this fill\n /// @param fillQuoteAmount quote-token amount transferred in this fill\n /// @param remainingRestrictedAmount restricted-token amount still unfilled after this event\n event OpenSwapFilled(\n uint256 indexed swapNumber,\n address indexed filler,\n uint256 fillRestrictedAmount,\n uint256 fillQuoteAmount,\n uint256 remainingRestrictedAmount\n );\n}\n"},"contracts/interfaces/ISnapshotPeriods.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\nimport \"@openzeppelin/contracts/utils/introspection/IERC165.sol\";\ninterface ISnapshotPeriods {\n struct Period {\n uint256 start;\n uint256 end;\n uint256 totalAccruedOwnership;\n uint256 accruedOwnership;\n }\n\n struct WalletInfo {\n uint256 latestAccrualTimestamp;\n uint256 totalAccruedOwnership;\n uint256 periodCount;\n }\n\n function onUpdate(\n address token,\n address from,\n address to,\n uint256 amount\n ) external;\n\n function addressInfo(\n address token,\n address account\n ) external view returns (WalletInfo memory);\n\n function supplyInfo(\n address token\n ) external view returns (WalletInfo memory);\n\n function walletPeriodByIndex(\n address token,\n address account,\n uint256 index\n ) external view returns (Period memory);\n\n function totalSupplyPeriodByIndex(\n address token,\n uint256 index\n ) external view returns (Period memory);\n\n function findPeriodIndexForTimestamp(\n address token,\n address account,\n uint256 timestamp\n ) external view returns (uint256 periodIndex);\n\n function findTotalSupplyPeriodIndexForTimestamp(\n address token,\n uint256 timestamp\n ) external view returns (uint256 periodIndex);\n\n function calculateOwnershipForPeriod(\n uint256 amount,\n uint256 periodStart,\n uint256 periodEnd\n ) external view returns (uint256 ownership);\n\n function totalOwnershipForPeriod(\n address token,\n uint256 startTimestamp,\n uint256 endTimestamp\n ) external view returns (uint256);\n\n function ownershipForPeriod(\n address token,\n address account,\n uint256 startTimestamp,\n uint256 endTimestamp\n ) external view returns (uint256);\n\n function totalOwnershipAt(\n address token,\n uint256 timestamp\n ) external view returns (uint256);\n\n function ownershipAt(\n address token,\n address account,\n uint256 timestamp\n ) external view returns (uint256);\n\n function walletPeriodCount(\n address token,\n address account\n ) external view returns (uint256);\n\n function totalSupplyPeriodCount(\n address token\n ) external view returns (uint256);\n\n /**\n * @dev Returns the balanceOf that `account` had at a specific moment in the past\n * @param token The address of the token\n * @param account The address of the account\n * @param timestamp The timestamp of the past balance\n * @return The balance of `account` at `timestamp`\n */\n function getPastBalanceOf(\n address token,\n address account,\n uint256 timestamp\n ) external view returns (uint256);\n\n /**\n * @dev Returns the total supply available at a specific moment in the past.\n * @param token The address of the token\n * @param timestamp The timestamp of the past total supply\n * @return The total supply of `token` at `timestamp`\n */\n function getPastTotalSupply(\n address token,\n uint256 timestamp\n ) external view returns (uint256);\n}\n"},"contracts/interfaces/ITransferRules.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\nimport \"@openzeppelin/contracts/utils/introspection/IERC165.sol\";\nimport \"./IIdentityRegistry.sol\";\n\ninterface ITransferRules {\n // ============================================\n // ORIGINAL TRANSFER RULES FUNCTIONALITY\n // ============================================\n \n /// @notice Detects if a transfer will be reverted and if so returns an appropriate reference code\n /// validates sufficient balance of each holdings and timelocks of sender to recipient \n /// @param token address of the token\n /// @param from Sending address\n /// @param to Receiving address\n /// @param value Amount of tokens being transferred\n /// @return Code by which to reference message for rejection reasoning\n function detectTransferRestriction(\n address token,\n address from,\n address to,\n uint256 value\n ) external view returns (uint256);\n\n /// @notice Detects if a transfer will be reverted and if so returns an appropriate reference code\n /// @param token address of the token\n /// @param from Sending address\n /// @param to Receiving address\n /// @param value Amount of tokens being transferred\n /// @return Code by which to reference message for rejection reasoning\n function detectTransferRestrictionBasic(\n address token,\n address from,\n address to,\n uint256 value\n ) external view returns (uint256);\n\n /// @notice Returns a human-readable message for a given restriction code\n /// @param restrictionCode Identifier for looking up a message\n /// @return Text showing the restriction's reasoning\n function messageForTransferRestriction(\n uint256 restrictionCode\n ) external view returns (string memory);\n\n function checkSuccess(uint256 restrictionCode) external view returns (bool);\n\n // ============================================\n // TOKEN TYPE RULES ENGINE FUNCTIONALITY\n // ============================================\n \n /**\n * @dev Structure for efficient lockup table entries\n */\n struct TokenTypeRule {\n uint256 tokenType; // Token type to assign\n bool requiresAmlKyc; // Whether AML/KYC is required\n bool isActive; // Whether this rule is active\n }\n\n /**\n * @dev Determine the token type for a wallet based on their identity.\n *\n * Scans the wallet's regions and returns the token type of the first active rule\n * matching (region, accreditation) EXACTLY -- there is no wildcard value. A rule\n * requiring AML/KYC is skipped when the wallet has not passed. If no region rule\n * matches, the default rule applies; if that is absent too the call REVERTS -- it\n * does NOT fall back to TOKEN_TYPE_GENERIC.\n *\n * A wallet with several regions may match more than one rule; every token type\n * reachable from its regions is permitted for it (see tokenTypeAllowed). Use\n * mintTokenType / mintReleaseScheduleTokenType to choose the type explicitly.\n *\n * @param wallet The wallet address to check\n * @param identityRegistry The identity registry to query\n * @return tokenType The determined token type\n * @custom:reverts TransferRules_TokenTypeRuleNotFound if no region rule matches and\n * no default rule is active\n * @custom:reverts TransferRules_RecipientNotAmlKycPassed if the default rule requires\n * AML/KYC and the wallet has not passed\n */\n function determineTokenType(\n address wallet,\n IIdentityRegistry identityRegistry\n ) external view returns (uint256 tokenType);\n\n\n /**\n * @dev Whether a specific token type is permitted for a wallet.\n *\n * Returns true if ANY of the wallet's regions has an active rule for this token\n * type (subject to AML/KYC), matched EXACTLY on (region, accreditation), or if the\n * default rule matches it. Unlike {determineTokenType} this never reverts -- an\n * unmatched wallet simply returns false.\n *\n * @param tokenType The token type to check\n * @param wallet The wallet address to check\n * @param identityRegistry The identity registry to query\n * @return allowed True if a matching active rule assigns this token type to the wallet\n */\n function tokenTypeAllowed(\n uint256 tokenType,\n address wallet,\n IIdentityRegistry identityRegistry\n ) external view returns (bool allowed);\n\n /**\n * @dev Set a lockup rule directly in the efficient lockup table\n *\n * Matching is EXACT. `region` and `accreditation` are compared literally against the\n * recipient's `IIdentityRegistry.IdentityInfo`; there is no wildcard value. In\n * particular `accreditation = 0` is NOT \"any accreditation\" -- it is the real\n * `NO_ACCREDITATION` value written by `IIdentityRegistry.revokeAccreditation`, so a\n * rule stored under 0 applies only to wallets with no accreditation.\n *\n * For a catch-all that applies when no region/accreditation rule matches, use\n * {setDefaultTokenTypeRule} instead.\n *\n * @param region The region code, matched exactly against the wallet's regions array\n * @param accreditation The accreditation level, matched exactly (0 = NO_ACCREDITATION)\n * @param tokenType The token type to assign\n * @param requiresAmlKyc Whether AML/KYC is required\n * @param isActive Whether the rule is active\n */\n function setTokenTypeRule(\n uint256 region,\n uint256 accreditation,\n uint256 tokenType,\n bool requiresAmlKyc,\n bool isActive\n ) external;\n\n /**\n * @dev Remove a lockup rule from the table\n * @param region The region code\n * @param accreditation The accreditation level\n */\n function removeTokenTypeRule(\n uint256 region,\n uint256 accreditation\n ) external;\n\n /**\n * @dev Set the default token type rule\n * @param tokenType The token type to assign\n * @param requiresAmlKyc Whether AML/KYC is required\n * @param isActive Whether the rule is active\n */\n function setDefaultTokenTypeRule(\n uint256 tokenType,\n bool requiresAmlKyc,\n bool isActive\n ) external;\n\n /**\n * @dev Reset the default token type rule\n */\n function resetDefaultTokenTypeRule() external;\n\n /**\n * @dev Get the current default token type rule\n */\n function defaultTokenTypeRule() external view returns (TokenTypeRule memory);\n\n /**\n * @dev Get a lockup rule from the table\n * @param region The region code\n * @param accreditation The accreditation level\n * @return rule The lockup rule\n */\n function getTokenTypeRule(\n uint256 region,\n uint256 accreditation\n ) external view returns (TokenTypeRule memory rule);\n\n /**\n * @dev Batch set multiple lockup rules\n * @param regions Array of region codes\n * @param accreditations Array of accreditation levels\n * @param tokenTypes Array of token types\n * @param requiresAmlKycFlags Array of AML/KYC requirement flags\n * @param isActiveFlags Array of active flags\n */\n function batchSetTokenTypeRules(\n uint256[] calldata regions,\n uint256[] calldata accreditations,\n uint256[] calldata tokenTypes,\n bool[] calldata requiresAmlKycFlags,\n bool[] calldata isActiveFlags\n ) external;\n\n // ============================================\n // TOKEN TYPE TRANSFER RULES FUNCTIONALITY \n // ============================================\n \n /**\n * @dev Structure to define transfer restrictions by token type and recipient identity\n */\n struct TransferRule {\n // Holding period in seconds from mint timestamp. Implementations may pack this rule into\n // a single storage word, so the value must not exceed TransferRules.MAX_LOCK_DURATION\n // (2^254 - 1); larger values are rejected rather than silently truncated.\n uint256 lockDurationSeconds;\n bool requiresAmlKyc; // Whether recipient must have AML/KYC\n bool isActive; // Whether this rule is currently active\n }\n\n /**\n * @dev Detects if a token holding can be transferred based on all applicable rules\n *\n * Iterates the recipient's regions and looks up\n * `(tokenType, region, accreditationType)` by EXACT match -- there is no wildcard\n * fallback. Succeeds if any region permits the transfer; if no region has an active\n * rule the result is `NO_RULE_FOR_RECIPIENT`, i.e. unmatched recipients are blocked,\n * not allowed. Defined as `eligibilityThresholdFor` applied to `mintTimestamp`; see\n * that function for the domain on which the two forms are interchangeable.\n *\n * @param tokenType The type of token being transferred\n * @param mintTimestamp When the tokens were originally minted\n * @param recipientIdentity The identity of the recipient\n * @param isAmlKycPassed Whether the recipient has passed AML/KYC\n * @return restrictionCode Error code if transfer is blocked (0 if allowed)\n */\n function detectTransferRestrictionForHolding(\n uint256 tokenType,\n uint256 mintTimestamp,\n IIdentityRegistry.IdentityInfo memory recipientIdentity,\n bool isAmlKycPassed\n ) external view returns (uint256 restrictionCode);\n\n /**\n * @dev Collapses the per-region rules for one token type into a single mintTimestamp\n * threshold, so a caller iterating many holdings can evaluate eligibility locally\n * instead of making one call per holding.\n *\n * A holding of `tokenType` minted at `mintTimestamp` is transferable to this recipient\n * iff `allowed && mintTimestamp <= maxEligibleMintTimestamp`; otherwise it is blocked\n * with `failureCode`. Equivalent to detectTransferRestrictionForHolding for every\n * mintTimestamp at which `mintTimestamp + lockDurationSeconds` does not overflow, which\n * is every mint timestamp a token can record: those are below 2^64 while lock durations\n * are capped at 2^254 - 1. For an absurd caller-supplied mintTimestamp the historical\n * per-region loop panicked on that addition; this form reports `failureCode` instead.\n *\n * Evaluates every region rather than stopping at the first qualifying one: the result\n * has to be the *most permissive* threshold to stand in for all holdings of the type.\n * Callers that check many holdings amortise this with a per-token-type cache\n * (EligibilityCacheLib), paying for it once per type instead of once per holding.\n * @param tokenType The type of token being transferred\n * @param recipientIdentity The identity of the recipient\n * @param isAmlKycPassed Whether the recipient has passed AML/KYC\n * @return allowed False when no region can ever qualify, whatever the mint timestamp\n * @return maxEligibleMintTimestamp Highest mint timestamp that is still transferable\n * @return failureCode Restriction code to report when the holding is not eligible\n */\n function eligibilityThresholdFor(\n uint256 tokenType,\n IIdentityRegistry.IdentityInfo memory recipientIdentity,\n bool isAmlKycPassed\n ) external view returns (bool allowed, uint256 maxEligibleMintTimestamp, uint256 failureCode);\n\n /**\n * @dev Add or update a transfer rule\n *\n * Matching is EXACT. `recipientRegion` and `recipientAccreditation` are compared\n * literally against the recipient's `IIdentityRegistry.IdentityInfo`; there is no\n * wildcard value. In particular `recipientAccreditation = 0` is NOT \"any\n * accreditation\" -- it is the real `NO_ACCREDITATION` value, so a rule stored under 0\n * applies only to recipients with no accreditation.\n *\n * A rule written for a (region, accreditation) pair that no identity actually holds\n * is inert: it will never match, and recipients fall through to\n * `NO_RULE_FOR_RECIPIENT`. There is no default/catch-all transfer rule -- every\n * permitted (tokenType, region, accreditation) combination must be enumerated.\n *\n * @param tokenType The token type this rule applies to (1=RegS, 2=RegD, etc.)\n * @param recipientRegion The recipient region, matched exactly\n * @param recipientAccreditation The recipient accreditation, matched exactly (0 = NO_ACCREDITATION)\n * @param rule The transfer rule to add/update. rule.lockDurationSeconds must be no greater\n * than TransferRules.MAX_LOCK_DURATION (2^254 - 1), otherwise the call reverts with\n * TransferRules_LockDurationTooLarge.\n */\n function setTransferRule(\n uint256 tokenType,\n uint256 recipientRegion,\n uint256 recipientAccreditation,\n TransferRule memory rule\n ) external;\n\n /**\n * @dev Remove a transfer rule\n * @param tokenType The token type to remove the rule for\n * @param recipientRegion The region to remove the rule for\n * @param recipientAccreditation The accreditation to remove the rule for\n */\n function removeTransferRule(uint256 tokenType, uint256 recipientRegion, uint256 recipientAccreditation) external;\n\n /**\n * @dev Get the transfer rule for a specific token type, recipient region, and recipient accreditation\n * @param tokenType The token type to query rules for\n * @param recipientRegion The recipient region to query rules for\n * @param recipientAccreditation The recipient accreditation to query rules for\n * @return rule The transfer rule\n */\n function transferRuleFor(\n uint256 tokenType,\n uint256 recipientRegion,\n uint256 recipientAccreditation\n ) external view returns (TransferRule memory rule);\n\n /**\n * @dev Get the earliest holding-period expiry for a token holding sent to a given recipient.\n *\n * Scans the recipient's regions and returns the smallest `mintTimestamp + lockDuration`\n * among the rules that are active AND satisfiable at the recipient's current AML/KYC\n * status, matching how detectTransferRestrictionForHolding selects a permitting region.\n * Returns `type(uint256).max` when no such rule exists - either no active rule matches the\n * recipient (detectTransferRestrictionForHolding would return NO_RULE_FOR_RECIPIENT) or\n * every matching rule requires AML/KYC the recipient does not have\n * (RECIPIENT_NOT_AMLKYCPASSED). It never returns 0 as a \"no restriction\" signal.\n *\n * This is a holding-period estimate, NOT an authorization oracle. It does not account for\n * pause state, sender/recipient freezes, balances, or timelock schedules, and its value\n * moves as AML/KYC is granted or expires. Always re-check detectTransferRestriction at the\n * time of transfer - it is the only authoritative answer.\n *\n * @param tokenType The type of token\n * @param mintTimestamp When the tokens were minted\n * @param recipient The intended recipient\n * @param identityRegistry The identity registry to query\n * @return unlockTimestamp Earliest holding-period expiry, or type(uint256).max if no active\n * rule can permit the transfer at the recipient's current AML/KYC status\n */\n function getUnlockTimestamp(\n uint256 tokenType,\n uint256 mintTimestamp,\n address recipient,\n IIdentityRegistry identityRegistry\n ) external view returns (uint256 unlockTimestamp);\n\n /**\n * @dev Batch create multiple rules\n * @param rules Array of transfer rules to create. Every entry is subject to the same\n * lockDurationSeconds bound as setTransferRule; one out-of-range entry reverts the whole call.\n */\n function batchSetTransferRules(\n uint256[] calldata tokenType,\n uint256[] calldata recipientRegion,\n uint256[] calldata recipientAccreditation,\n TransferRule[] calldata rules\n ) external;\n\n // ============================================\n // EVENTS\n // ============================================\n \n // Token Type Rules Engine Events\n event TokenTypeRuleSet(\n uint256 indexed region,\n uint256 indexed accreditation,\n uint256 indexed tokenType,\n bool requiresAmlKyc,\n bool isActive\n );\n\n event DefaultTokenTypeRuleSet(\n uint256 indexed tokenType,\n bool requiresAmlKyc,\n bool isActive\n );\n \n event TokenTypeRuleRemoved(\n uint256 indexed region,\n uint256 indexed accreditation\n );\n\n // Transfer Rules Events\n event TransferRuleSet(\n uint256 indexed tokenType,\n uint256 recipientRegion,\n uint256 recipientAccreditation,\n uint256 lockDurationSeconds,\n bool requiresAmlKyc,\n bool isActive\n );\n \n event TransferRuleRemoved(uint256 indexed tokenType, uint256 indexed recipientRegion, uint256 indexed recipientAccreditation);\n}\n"},"contracts/libraries/BitManipulationLib.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\n/**\n * @title BitManipulationLib\n * @notice Library for complex bit manipulation operations used in token storage optimization\n * @dev Extracted from Storage.sol to reduce contract size and improve modularity\n */\nlibrary BitManipulationLib {\n // ============================================\n // CONSTANTS\n // ============================================\n \n // Element packing constants (8-element packing)\n uint256 internal constant TOKEN_TYPE_BITS = 8; // Token type uses 8 bits (supports 0-255)\n uint256 internal constant TOKEN_TYPE_MASK = (1 << TOKEN_TYPE_BITS) - 1; // 0xFF\n \n // ============================================\n // ELEMENT PACKING FUNCTIONS\n // ============================================\n \n /**\n * @dev Pack a single element (tokenType + daysAfterDeployment) into dynamic bits\n * @param tokenType The token type (0-255, uses 8 bits)\n * @param daysAfterDeployment The days after deployment (uses remaining bits)\n * @param elementBitSize The total bits per element\n * @return packed The packed element\n */\n function packElement(uint256 tokenType, uint256 daysAfterDeployment, uint256 elementBitSize) \n internal \n pure \n returns (uint256 packed) \n {\n require(tokenType <= TOKEN_TYPE_MASK, \"TokenType exceeds 8-bit max\");\n uint256 daysBitSize = elementBitSize - TOKEN_TYPE_BITS;\n uint256 daysMask = (1 << daysBitSize) - 1;\n // Days are stored as (daysAfterDeployment + 1) so that 0 is reserved exclusively as\n // the empty-slot sentinel. Without this, (tokenType 0, day 0) packs to 0, which the\n // allocator in Storage._findOrCreateOptimizedGlobalIndex cannot distinguish from an\n // unused slot - the element never persists and the index is handed out twice.\n require(daysAfterDeployment < daysMask, \"Days exceeds available bits\");\n\n // Pack: [daysBitSize bits: daysAfterDeployment + 1][8 bits: tokenType]\n uint256 encodedDays = daysAfterDeployment + 1;\n packed = (encodedDays << TOKEN_TYPE_BITS) | tokenType;\n }\n \n /**\n * @dev Unpack a dynamic-bit element into tokenType and daysAfterDeployment\n * @param packed The packed element\n * @return tokenType The token type\n * @return daysAfterDeployment The days after deployment\n */\n function unpackElement(uint256 packed) \n internal \n pure \n returns (uint256 tokenType, uint256 daysAfterDeployment) \n {\n tokenType = packed & TOKEN_TYPE_MASK; // Extract lower 8 bits\n // Undo the +1 offset applied by packElement. An all-zero packed value is an unused\n // slot rather than a real element; it decodes to (0, 0) so callers that read an\n // unallocated index keep the previous behaviour.\n uint256 encodedDays = packed >> TOKEN_TYPE_BITS; // Extract upper bits\n daysAfterDeployment = encodedDays == 0 ? 0 : (encodedDays - 1);\n }\n\n /**\n * @dev Unpack tokenType from a packed 32-bit element\n * @param packedElement The packed 32-bit element\n * @return tokenType The token type (lower 8 bits)\n */\n function unpackTokenTypeElement(uint256 packedElement) \n internal \n pure \n returns (uint256 tokenType) \n {\n tokenType = packedElement & TOKEN_TYPE_MASK;\n }\n \n /**\n * @dev Get a single element from packed structure\n * @param packed The packed 256-bit value\n * @param subIndex The subIndex (0 to slotsPerWord-1)\n * @param elementBitSize The number of bits per element\n * @return element The element at the specified subIndex\n */\n function getPackedElement(uint256 packed, uint256 subIndex, uint256 elementBitSize) \n internal \n pure \n returns (uint256 element) \n {\n element = (packed >> (subIndex * elementBitSize)) & ((1 << elementBitSize) - 1);\n }\n\n // ============================================\n // BALANCE PACKING FUNCTIONS\n // ============================================\n \n /**\n * @dev Get a single balance from packed balances\n * @param packed The packed balances\n * @param subIndex The subIndex (0-7)\n * @return balance The balance at the specified subIndex\n */\n function getPackedBalance(uint256 packed, uint256 subIndex, uint256 elementBitSize) \n internal \n pure \n returns (uint256 balance) \n {\n balance = (packed >> (subIndex * elementBitSize)) & ((1 << elementBitSize) - 1);\n }\n \n /**\n * @dev Set a single balance in packed balances\n * @param packed The current packed balances\n * @param subIndex The subIndex (0-7)\n * @param newBalance The new balance to set\n * @return newPacked The updated packed balances\n */\n function setPackedBalance(uint256 packed, uint256 subIndex, uint256 newBalance, uint256 elementBitSize) \n internal \n pure \n returns (uint256 newPacked) \n {\n // Clear the old balance bits and set the new balance\n uint256 mask = ((1 << elementBitSize) - 1) << (subIndex * elementBitSize);\n newPacked = (packed & ~mask) | (newBalance << (subIndex * elementBitSize));\n }\n\n // ============================================\n // INDEX CALCULATION FUNCTIONS\n // ============================================\n \n /**\n * @dev Get the packed balance key (bucket index) from global index\n * @param globalIndex The global index\n * @return packedKey The bucket index for balance storage (maps 1:1 to globalMintTimestamps)\n */\n function getPackedBalanceKeyFromGlobalIndex(uint256 globalIndex, uint256 slotsPerWord) \n internal \n pure \n returns (uint256 packedKey) \n {\n // Calculate bucket index - each bucket in globalMintTimestamps contains 8 elements\n packedKey = globalIndex / slotsPerWord;\n }\n\n // ============================================\n // BITMAP MANIPULATION FUNCTIONS\n // ============================================\n\n /**\n * @dev Clear all bits that belong to a bucket and are in the current word\n * @param word_ The bitmap word to modify\n * @param globalIndex Any global index within the bucket (doesn't need to be aligned)\n * @param wordIndex_ The word index in the bitmap (0, 1, 2, ...)\n * @param slotsPerWord The number of slots per word (4-8)\n * @return The updated word with cleared bits\n * @notice Clears slotsPerWord consecutive bits starting from bucket start\n * @notice Only clears bits that are within the current word boundaries\n */\n function clearBucketBits(\n uint256 word_,\n uint256 globalIndex,\n uint256 wordIndex_,\n uint256 slotsPerWord\n ) internal pure returns (uint256) {\n uint256 bucketStartIndex = (globalIndex / slotsPerWord) * slotsPerWord;\n uint256 wordStartBit = wordIndex_ * 256;\n uint256 wordEndBit = wordStartBit + 256;\n \n // Calculate which bits of the bucket are in this word\n uint256 firstBitToClear = bucketStartIndex < wordStartBit ? 0 : bucketStartIndex - wordStartBit;\n uint256 lastBitToClear = bucketStartIndex + slotsPerWord > wordEndBit ? 255 : bucketStartIndex + slotsPerWord - wordStartBit - 1;\n \n // Create mask and clear bits in one operation\n if (firstBitToClear <= lastBitToClear && lastBitToClear < 256) {\n uint256 mask = ((1 << (lastBitToClear - firstBitToClear + 1)) - 1) << firstBitToClear;\n return word_ & ~mask;\n }\n \n return word_;\n }\n\n // ============================================\n // UTILITY FUNCTIONS\n // ============================================\n \n /**\n * @dev Calculate days after deployment from timestamps\n * @param mintTimestamp The mint timestamp to calculate from\n * @param deploymentDay_ The deployment timestamp to calculate relative to\n * @return daysAfterDeployment The number of days after deployment (midnight-aligned)\n */\n function calculateDaysAfterDeployment(\n uint256 mintTimestamp,\n uint256 deploymentDay_\n ) internal pure returns (uint256) {\n if (mintTimestamp < deploymentDay_) {\n return 0;\n }\n return (mintTimestamp - deploymentDay_) / 1 days;\n }\n\n /**\n * @dev Convert days since deploy back to absolute timestamp\n * @param daysSinceDeploy The days since deployment\n * @param deploymentDay_ The deployment day timestamp\n * @return timestamp The absolute timestamp\n */\n function daysSinceDeployToTimestamp(uint256 daysSinceDeploy, uint256 deploymentDay_) \n internal \n pure \n returns (uint256 timestamp) \n {\n timestamp = deploymentDay_ + (daysSinceDeploy * 1 days);\n }\n\n // ============================================\n // SLOT CALCULATION FUNCTIONS\n // ============================================\n \n /**\n * @dev Calculate the optimal number of slots per 256-bit word based on max total supply\n * @param maxTotalSupply The maximum total supply of the token (in basis units)\n * @return slotsPerWord The number of slots that fit in a 256-bit word (4-8)\n * @notice Decision is based on max total supply divided by 1000 to account for \n * worst case scenario of 1000 holdings with overflow value\n * @notice Uses powers of 2 for precise mathematical calculation\n */\n function calculateSlotsPerWord(uint256 maxTotalSupply) \n internal \n pure \n returns (uint256 slotsPerWord) \n {\n // Calculate the effective max value\n // Divide by 1000 to account for worst case scenario of 1000 holdings with overflow\n uint256 effectiveMaxValue = maxTotalSupply / 1000;\n \n // Determine optimal slot size based on effective max value using powers of 2\n if (effectiveMaxValue <= (2 ** 32)) {\n // 32 bits: fits 2^32 = 4,294,967,296\n // 8 slots in 256 bits (256/32 = 8)\n slotsPerWord = 8;\n } else if (effectiveMaxValue <= (2 ** 36)) {\n // 36 bits: fits 2^36 = 68,719,476,736\n // 7 slots in 256 bits (256/36 = 7.11, so 7 slots)\n slotsPerWord = 7;\n } else if (effectiveMaxValue <= (2 ** 42)) {\n // 42 bits: fits 2^42 = 4,398,046,511,104\n // 6 slots in 256 bits (256/42 = 6.09, so 6 slots)\n slotsPerWord = 6;\n } else if (effectiveMaxValue <= (2 ** 51)) {\n // 51 bits: fits 2^51 = 2,251,799,813,685,248\n // 5 slots in 256 bits (256/51 = 5.02, so 5 slots)\n slotsPerWord = 5;\n } else {\n // 64 bits: fits 2^64 = 18,446,744,073,709,551,616\n // 4 slots in 256 bits (256/64 = 4)\n slotsPerWord = 4;\n }\n \n return slotsPerWord;\n }\n\n /**\n * @dev Calculate the maximum balance value that can fit in each slot\n * @param slotsPerWord The number of slots per 256-bit word (4-8)\n * @return maxBalancePerSubIndex The maximum balance value per slot\n * @notice This calculates 2^(256/slotsPerWord) - 1 to get the maximum value per slot\n */\n function calculateMaxBalancePerSubIndex(uint256 slotsPerWord) \n internal \n pure \n returns (uint256 maxBalancePerSubIndex) \n {\n // Calculate the number of bits per slot\n uint256 bitsPerSlot = 256 / slotsPerWord;\n \n // Calculate 2^bitsPerSlot - 1 to get the maximum value per slot\n maxBalancePerSubIndex = (2 ** bitsPerSlot) - 1;\n }\n\n /**\n * @dev Calculate the number of bits needed to store an element (tokenType + daysAfterDeployment)\n * @param slotsPerWord The number of slots per 256-bit word (4-8)\n * @return elementBitSize The number of bits needed to store an element\n */\n function calculateElementBitSize(uint256 slotsPerWord) \n internal \n pure \n returns (uint256 elementBitSize) \n {\n elementBitSize = 256 / slotsPerWord;\n }\n\n /**\n * @dev Calculate the number of bits for days after deployment based on element size\n * @param elementBitSize The total bits per element\n * @return daysBitSize The number of bits for days (elementBitSize - 8 for tokenType)\n */\n function calculateDaysBitSize(uint256 elementBitSize) \n internal \n pure \n returns (uint256 daysBitSize) \n {\n require(elementBitSize > TOKEN_TYPE_BITS, \"Element bit size too small for token type\");\n daysBitSize = elementBitSize - TOKEN_TYPE_BITS;\n }\n}\n"},"contracts/libraries/EligibilityCacheLib.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\n/// @dev Number of cache slots. File-level so it can size array types in other contracts\n/// (`uint256[ELIGIBILITY_CACHE_SLOTS] memory`); a library constant is not accepted there.\nuint256 constant ELIGIBILITY_CACHE_SLOTS = 16;\n\n/**\n * @title EligibilityCacheLib\n * @notice Small direct-mapped memory cache for per-token-type transfer eligibility.\n * @dev ITransferRules.eligibilityThresholdFor collapses a recipient's per-region rules for one\n * token type into `(allowed, maxEligibleMintTimestamp)`. A loop over many holdings or\n * timelocks needs that answer once per distinct token type, not once per item. This library\n * packs the answers into a `uint256[SLOTS]` memory array so every such loop - the transfer\n * engine, the ERC-1404 preflight, findHoldingFor, the timelock walk - shares one encoding.\n *\n * Direct-mapped: token type `t` lives in slot `t & (SLOTS - 1)`. Two token types that\n * collide evict each other, which costs an extra resolution but never a wrong answer: a hit\n * requires the stored token type to match exactly. Token types wider than 8 bits are never\n * cached (isHit is false, store does not write), which keeps callers correct for any input.\n *\n * Entry layout (0 = empty):\n * bit 0 valid\n * bits 1..8 token type (8 bits, BitManipulationLib.TOKEN_TYPE_BITS)\n * bits 9..255 exclusive upper bound: a holding with value `v` is admitted iff\n * `v < bound`. \"Never allowed\" is therefore bound 0 and needs no flag; an\n * inclusive threshold `T` is stored as `T + 1`, saturated to BOUND_MAX.\n * The library does not care what unit `v` is in: the token side stores the\n * last eligible day after deployment, TransferRules stores the\n * mint-timestamp threshold itself. Either way the \"always eligible\"\n * sentinel type(uint256).max saturates to a bound astronomically above any\n * real value.\n *\n * Hot path. A per-holding loop reads the raw entry and decodes it in place:\n *\n * uint256 entry = EligibilityCacheLib.entryFor(cache, tokenType);\n * if (!EligibilityCacheLib.isHit(entry, tokenType)) entry = <resolve, store, return entry>;\n * if (!EligibilityCacheLib.admits(entry, value)) continue;\n *\n * entryFor, isHit and admits are each a single branch-free expression on purpose: that is\n * what lets the optimizer inline them. A version of this check built from `&&` chains, or\n * wrapped in a helper function, cost a real call per holding - measured at roughly the\n * price of the check itself again - because short-circuit operators compile to branches\n * that the expression inliner will not touch.\n */\nlibrary EligibilityCacheLib {\n uint256 internal constant SLOTS = ELIGIBILITY_CACHE_SLOTS;\n /// @dev SLOTS is a power of two so the slot index is a mask, not a modulo. Spelled as a\n /// literal (== SLOTS - 1) because inline assembly only accepts literal constants.\n uint256 private constant SLOT_MASK = 0xF;\n\n uint256 private constant VALID_BIT = 1;\n uint256 private constant TOKEN_TYPE_SHIFT = 1;\n uint256 private constant TOKEN_TYPE_MASK = 0xFF;\n uint256 private constant BOUND_SHIFT = 9;\n /// @dev Everything below the bound: valid bit plus token type. A hit compares these bits.\n uint256 private constant KEY_MASK = (1 << BOUND_SHIFT) - 1;\n\n /// @dev Largest exclusive bound an entry can hold; larger values are saturated on store.\n uint256 internal constant BOUND_MAX = type(uint256).max >> BOUND_SHIFT;\n\n /// @dev The raw entry in `tokenType`'s slot (0 when empty). Decode with isHit and admits.\n function entryFor(\n uint256[ELIGIBILITY_CACHE_SLOTS] memory cache,\n uint256 tokenType\n ) internal pure returns (uint256 entry) {\n // Plain indexing would add a bounds check the mask already guarantees; keep this a\n // single expression so it inlines.\n assembly (\"memory-safe\") {\n entry := mload(add(cache, shl(5, and(tokenType, SLOT_MASK))))\n }\n }\n\n /// @dev True when `entry` was stored for exactly `tokenType`. A token type wider than 8\n /// bits can never match because its shifted form has bits above KEY_MASK.\n function isHit(uint256 entry, uint256 tokenType) internal pure returns (bool) {\n return (entry & KEY_MASK) == ((tokenType << TOKEN_TYPE_SHIFT) | VALID_BIT);\n }\n\n /// @dev Whether a holding with `value` (day or timestamp, matching what was stored) passes.\n function admits(uint256 entry, uint256 value) internal pure returns (bool) {\n return value < (entry >> BOUND_SHIFT);\n }\n\n /**\n * @dev Record the answer for `tokenType`, replacing whatever occupied its slot.\n * @param allowed False when no value can ever be admitted\n * @param threshold Inclusive upper bound on admitted values (ignored when !allowed)\n * @return entry The encoded entry, so the caller can decode it with admits exactly as a\n * later hit would. For a token type too wide to cache the entry is returned but\n * not stored.\n */\n function store(\n uint256[ELIGIBILITY_CACHE_SLOTS] memory cache,\n uint256 tokenType,\n bool allowed,\n uint256 threshold\n ) internal pure returns (uint256 entry) {\n uint256 bound;\n if (allowed) {\n bound = threshold >= BOUND_MAX - 1 ? BOUND_MAX : threshold + 1;\n }\n entry = (bound << BOUND_SHIFT) | ((tokenType & TOKEN_TYPE_MASK) << TOKEN_TYPE_SHIFT) | VALID_BIT;\n if (tokenType <= TOKEN_TYPE_MASK) {\n cache[tokenType & SLOT_MASK] = entry;\n }\n }\n}\n"},"contracts/libraries/VestingMath.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @title VestingMath Library\n/// @notice Contains pure helper functions that were previously embedded in RestrictedLockupToken.\n/// Linking this library removes a few kilobytes from the main contract byte-code.\nlibrary VestingMath {\n uint256 internal constant BIPS_PRECISION = 10_000;\n\n /**\n * @notice Calculates how many tokens are unlocked for a vesting schedule at a given timestamp.\n * @param commencedTimestamp_ Timestamp when the schedule started.\n * @param currentTimestamp_ Timestamp to calculate the unlocked amount for.\n * @param amount_ Total tokens under the schedule.\n * @param releaseCount_ Total number of releases (including initial one).\n * @param delayUntilFirstReleaseInSeconds_ Cliff period in seconds before first release.\n * @param initialReleasePortionInBips_ Portion unlocked at the first release in BIPS (1/100th of a percent).\n * @param periodBetweenReleasesInSeconds_ Time between subsequent releases after the first.\n * @return unlocked Amount of tokens that should be unlocked.\n */\n function calculateUnlocked(\n uint256 commencedTimestamp_,\n uint256 currentTimestamp_,\n uint256 amount_,\n uint256 releaseCount_,\n uint256 delayUntilFirstReleaseInSeconds_,\n uint256 initialReleasePortionInBips_,\n uint256 periodBetweenReleasesInSeconds_\n ) internal pure returns (uint256 unlocked) {\n // Nothing unlocked before commencement\n if (commencedTimestamp_ > currentTimestamp_) {\n return 0;\n }\n\n uint256 secondsElapsed = currentTimestamp_ - commencedTimestamp_;\n\n // All tokens unlocked if vesting fully complete\n if (\n secondsElapsed >=\n delayUntilFirstReleaseInSeconds_ +\n (periodBetweenReleasesInSeconds_ * (releaseCount_ - 1))\n ) {\n return amount_;\n }\n\n // Initial cliff release\n if (secondsElapsed >= delayUntilFirstReleaseInSeconds_) {\n unlocked = (amount_ * initialReleasePortionInBips_) / BIPS_PRECISION;\n\n // Subsequent periodic releases\n if (\n secondsElapsed - delayUntilFirstReleaseInSeconds_ >=\n periodBetweenReleasesInSeconds_\n ) {\n uint256 additionalPeriods = (secondsElapsed -\n delayUntilFirstReleaseInSeconds_) /\n periodBetweenReleasesInSeconds_;\n\n unlocked +=\n ((amount_ - unlocked) * additionalPeriods) /\n (releaseCount_ - 1);\n }\n }\n }\n\n /**\n * @notice Rounds a timestamp up to midnight UTC of the next day.\n */\n function toMidnightTimestamp(uint256 timestamp)\n internal\n pure\n returns (uint256)\n {\n return ((timestamp + 86399) / 86400) * 86400;\n }\n} "},"contracts/metatx/ERC2771CustomForwarder.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.28;\n\nimport {ERC2771Context} from \"@openzeppelin/contracts/metatx/ERC2771Context.sol\";\nimport {ECDSA} from \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\nimport {EIP712} from \"@openzeppelin/contracts/utils/cryptography/EIP712.sol\";\nimport {Address} from \"@openzeppelin/contracts/utils/Address.sol\";\nimport {Errors} from \"@openzeppelin/contracts/utils/Errors.sol\";\nimport {Nonces} from \"../utils/Nonces.sol\";\n\n/**\n * @dev Adapted from OpenZeppelin Contracts (last updated v5.0.0) (metatx/ERC2771Forwarder.sol)\n * @dev A forwarder compatible with ERC2771 contracts. See {ERC2771Context}.\n *\n * This forwarder operates on forward requests that include:\n *\n * * `from`: An address to operate on behalf of. It is required to be equal to the request signer.\n * * `to`: The address that should be called.\n * * `value`: The amount of native token to attach with the requested call. Must be zero — see the\n * \"No native value\" note below.\n * * `gas`: The amount of gas limit that will be forwarded with the requested call.\n * * `nonce`: A unique transaction ordering identifier to avoid replayability and request invalidation.\n * * `deadline`: A timestamp after which the request is not executable anymore.\n * * `data`: Encoded `msg.data` to send with the requested call.\n *\n * Relayers are able to submit batches if they are processing a high volume of requests. With high\n * throughput, relayers may run into limitations of the chain such as limits on the number of\n * transactions in the mempool. In these cases the recommendation is to distribute the load among\n * multiple accounts.\n *\n * NOTE: Batching requests includes an optional refund for unused `msg.value` that is achieved by\n * performing a call with empty calldata. While this is within the bounds of ERC-2771 compliance,\n * if the refund receiver happens to consider the forwarder a trusted forwarder, it MUST properly\n * handle `msg.data.length == 0`. `ERC2771Context` in OpenZeppelin Contracts versions prior to 4.9.3\n * do not handle this properly. In this fork the refund is unreachable, because no request may\n * carry native value — see below.\n *\n * ==== No native value\n *\n * This forwarder deliberately narrows upstream OpenZeppelin v5: {execute} and {executeBatch}\n * reject any nonzero `msg.value` or `request.value` with\n * {ERC2771CustomForwarder_NonZeroValueNotAllowed}. They remain `payable` so the ABI is unchanged.\n *\n * No ERC-2771 target in this system is payable, and the documented relayer flow specifies\n * `Value: 0` (`use_cases/UC-11-Meta-Transactions.md`), so forwarding native value could only ever\n * fail. What it did add was a way to strand ETH: a target that force-sends value back (`SELFDESTRUCT`,\n * or a coinbase payout) leaves it in this contract while the forwarded call still succeeds.\n *\n * This contract holds no ETH and therefore has, by design, no `receive`, no `fallback` and no\n * sweep. That is a deliberate choice over the alternative of adding an owner-gated recovery\n * function: force-sends cannot be prevented by any contract, so recovery would not close the hole\n * either, and it would insert a privileged role into the meta-transaction trust path — the larger\n * risk of the two. With value forwarding refused, no relayer principal can be stranded here; ETH\n * force-sent by a griefer is their own, and has no victim to recover it for.\n *\n * IMPORTANT: If a payable ERC-2771 target is ever introduced, this guard has to be revisited, and\n * the trapped-ETH and refund-delivery questions it closes reopen with it.\n *\n * ==== Security Considerations\n *\n * If a relayer submits a forward request, it should be willing to pay up to 100% of the gas amount\n * specified in the request. This contract does not implement any kind of retribution for this gas,\n * and it is assumed that there is an out of band incentive for relayers to pay for execution on\n * behalf of signers. Often, the relayer is operated by a project that will consider it a user\n * acquisition cost.\n *\n * By offering to pay for gas, relayers are at risk of having that gas used by an attacker toward\n * some other purpose that is not aligned with the expected out of band incentives. If you operate a\n * relayer, consider whitelisting target contracts and function selectors. When relaying ERC-721 or\n * ERC-1155 transfers specifically, consider rejecting the use of the `data` field, since it can be\n * used to execute arbitrary code.\n */\ncontract ERC2771CustomForwarder is EIP712, Nonces {\n using ECDSA for bytes32;\n\n struct ForwardRequestData {\n address from;\n address to;\n uint256 value;\n uint256 gas;\n uint48 deadline;\n uint256 nonce; // new field vs OpenZeppelin ERC2771Forwarder.sol\n bytes data;\n bytes signature;\n }\n\n bytes32 internal constant _FORWARD_REQUEST_TYPEHASH =\n keccak256(\n \"ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint48 deadline,bytes data)\"\n );\n\n /**\n * @dev Emitted when a `ForwardRequest` is executed.\n *\n * NOTE: An unsuccessful forward request could be due to an invalid signature, an expired deadline,\n * or simply a revert in the requested call. The contract guarantees that the relayer is not able to force\n * the requested call to run out of gas.\n */\n event ExecutedForwardRequest(\n address indexed signer,\n uint256 nonce,\n bool success\n );\n\n /**\n * @dev The request `from` doesn't match with the recovered `signer`.\n */\n error ERC2771CustomForwarder_InvalidSigner(address signer, address from);\n\n /**\n * @dev The `requestedValue` doesn't match with the available `msgValue`.\n */\n error ERC2771CustomForwarder_MismatchedValue(\n uint256 requestedValue,\n uint256 msgValue\n );\n\n /**\n * @dev Native value is not supported for forwarded requests. No ERC-2771 target in this system\n * is payable, so forwarding value can only strand ETH in this contract, which has no recovery\n * path by design. See the \"No native value\" note on the contract.\n */\n error ERC2771CustomForwarder_NonZeroValueNotAllowed();\n\n /**\n * @dev The request `deadline` has expired.\n */\n error ERC2771CustomForwarder_ExpiredRequest(uint48 deadline);\n\n /**\n * @dev The request target doesn't trust the `forwarder`.\n */\n error ERC2771CustomForwarder_UntrustfulTarget(\n address target,\n address forwarder\n );\n\n /**\n * @dev See {EIP712-constructor}.\n */\n constructor(string memory name) EIP712(name, \"1\") {}\n\n /**\n * @dev Returns `true` if a request is valid for a provided `signature` at the current block timestamp.\n *\n * A transaction is considered valid when the target trusts this forwarder, the request hasn't expired\n * (deadline is not met), the signer matches the `from` parameter of the signed request, and the\n * request's nonce hasn't been consumed yet.\n *\n * NOTE: A request may return false here but it won't cause {executeBatch} to revert if a refund\n * receiver is provided.\n *\n * NOTE: This only reports request validity. A request that is valid here can still fail when the\n * forwarded call reverts, which reverts {execute} and atomic {executeBatch}.\n *\n * NOTE: The nonce is part of the signed request in this forwarder, so a consumed nonce does not\n * invalidate the signature and has to be checked separately. The answer can also go stale: a\n * request that is valid when this is queried may have its nonce consumed by another transaction\n * before it is included. That is why {executeBatch} skips such a request instead of reverting.\n */\n function verify(\n ForwardRequestData calldata request\n ) public view virtual returns (bool) {\n (\n bool isTrustedForwarder,\n bool active,\n bool signerMatch,\n bool nonceValid,\n\n ) = _validate(request);\n return isTrustedForwarder && active && signerMatch && nonceValid;\n }\n\n /**\n * @dev Executes a `request` on behalf of `signature`'s signer using the ERC-2771 protocol. The gas\n * provided to the requested call may not be exactly the amount requested, but the call will not run\n * out of gas. Will revert if the request is invalid or the call reverts, in this case the nonce is not consumed.\n *\n * Requirements:\n *\n * - The request value should be equal to the provided `msg.value`.\n * - Both must be zero. See the \"No native value\" note on the contract.\n * - The request should be valid according to {verify}.\n */\n function execute(\n ForwardRequestData calldata request\n ) public payable virtual {\n // We make sure that msg.value and request.value match exactly.\n // If the request is invalid or the call reverts, this whole function\n // will revert, ensuring value isn't stuck.\n if (msg.value != request.value) {\n revert ERC2771CustomForwarder_MismatchedValue(\n request.value,\n msg.value\n );\n }\n\n // Checked after the mismatch above so a caller who simply mis-funded still gets the more\n // specific error. That check pins msg.value == request.value, so this covers both.\n _checkNoValue(msg.value);\n\n if (!_execute(request, true)) {\n revert Errors.FailedCall();\n }\n }\n\n /**\n * @dev Batch version of {execute} with optional refunding and atomic execution.\n *\n * In case a batch contains at least one invalid request (see {verify}), or one whose\n * forwarded call reverts, the request will be skipped and the `refundReceiver` parameter will\n * receive back the unused requested value at the end of the execution. This is done to\n * prevent reverting the entire batch when a request is invalid.\n *\n * NOTE: A request whose nonce was already consumed counts as invalid here, so it is skipped\n * and refunded rather than reverting the batch. This matters because the nonce may be\n * consumed between the moment the relayer builds the batch and the moment it is included.\n *\n * If the `refundReceiver` is the `address(0)`, this function will revert when at least\n * one of the requests was not valid, or when one of the forwarded calls fails, instead of\n * skipping it. This could be useful if a batch is required to get executed atomically\n * (at least at the top-level). For example, refunding (and thus atomicity) can be opt-out\n * if the relayer is using a service that avoids including reverted transactions.\n *\n * Requirements:\n *\n * - The sum of the requests' values should be equal to the provided `msg.value`.\n * - `msg.value` and every `request.value` must be zero. See the \"No native value\" note on the\n * contract. This makes `refundValue` — and with it the refund push below — unreachable.\n * - All of the requests should be valid (see {verify}) and all of their forwarded calls should\n * succeed when `refundReceiver` is the zero address.\n *\n * NOTE: Setting a zero `refundReceiver` guarantees an all-or-nothing requests execution only for\n * the first-level forwarded calls. In case a forwarded request calls to a contract with another\n * subcall, the second-level call may revert without the top-level call reverting.\n */\n function executeBatch(\n ForwardRequestData[] calldata requests,\n address payable refundReceiver\n ) public payable virtual {\n // Fail before executing anything. Without this the batch would run every request and only\n // unwind at the `requestsValue != msg.value` check below.\n _checkNoValue(msg.value);\n\n bool atomic = refundReceiver == address(0);\n\n uint256 requestsValue;\n uint256 refundValue;\n\n for (uint256 i; i < requests.length; ++i) {\n // Unconditional, not gated on `atomic`: a value-bearing request can never be part of a\n // valid batch, since the guard above already forced msg.value == 0. This is a\n // relayer-supplied malformation, not the front-running case refund mode absorbs.\n _checkNoValue(requests[i].value);\n\n requestsValue += requests[i].value;\n bool success = _execute(requests[i], atomic);\n if (!success) {\n refundValue += requests[i].value;\n }\n }\n\n // The batch should revert if there's a mismatched msg.value provided\n // to avoid request value tampering\n if (requestsValue != msg.value) {\n revert ERC2771CustomForwarder_MismatchedValue(\n requestsValue,\n msg.value\n );\n }\n\n // Some requests with value were invalid (possibly due to frontrunning).\n // To avoid leaving ETH in the contract this value is refunded.\n if (refundValue != 0) {\n // Unreachable in atomic mode: _execute reverts on any failed request when\n // requireValidRequest is true. Enforced here so an override of the virtual\n // _execute can never route ETH to address(0) and burn it.\n if (refundReceiver == address(0)) {\n revert Errors.FailedCall();\n }\n\n // We know refundReceiver != address(0) && requestsValue == msg.value\n // meaning we can ensure refundValue is not taken from the original contract's balance\n // and refundReceiver is a known account.\n //\n // Pushed with a raw call rather than {Address-sendValue}: a refundReceiver that rejects\n // plain ETH would otherwise revert the whole non-atomic batch and undo the requests\n // that already succeeded — the mode that exists precisely so it does not revert. On\n // rejection the value goes to the relayer, who funded it, instead.\n //\n // Unreachable while nonzero value is rejected above, since refundValue can only be a\n // sum of zeros. Kept because executeBatch and _execute are both virtual and an override\n // can reintroduce a nonzero refundValue.\n (bool refunded, ) = refundReceiver.call{value: refundValue}(\"\");\n if (!refunded) {\n Address.sendValue(payable(msg.sender), refundValue);\n }\n }\n }\n\n /**\n * @dev Reverts unless `value` is zero. Single point of enforcement for the \"No native value\"\n * rule, called by {execute} for `msg.value` and by {executeBatch} for `msg.value` and for each\n * `request.value`.\n *\n * `virtual` so tests can reach the value-carrying paths this rule makes unreachable in\n * production — the `refundReceiver == address(0)` guard and the refund fallback in\n * {executeBatch} — and pin that those backstops still hold.\n */\n function _checkNoValue(uint256 value) internal view virtual {\n if (value != 0) {\n revert ERC2771CustomForwarder_NonZeroValueNotAllowed();\n }\n }\n\n /**\n * @dev Validates if the provided request can be executed at current block timestamp with\n * the given `request.signature` on behalf of `request.signer`.\n *\n * Returns `nonceValid` separately because this forwarder carries the nonce inside the signed\n * request, so consuming it does not change the digest and cannot be detected through\n * `signerMatch` the way upstream's storage-derived nonce is.\n */\n function _validate(\n ForwardRequestData calldata request\n )\n internal\n view\n virtual\n returns (\n bool isTrustedForwarder,\n bool active,\n bool signerMatch,\n bool nonceValid,\n address signer\n )\n {\n (bool isValid, address recovered) = _recoverForwardRequestSigner(\n request\n );\n\n return (\n _isTrustedByTarget(request.to),\n request.deadline >= block.timestamp,\n isValid && recovered == request.from,\n // Keyed on `request.from` to match the {Nonces-_useNonce} call in {_execute}\n isNonceValid(request.from, request.nonce),\n recovered\n );\n }\n\n /**\n * @dev Returns a tuple with the recovered the signer of an EIP712 forward request message hash\n * and a boolean indicating if the signature is valid.\n *\n * NOTE: The signature is considered valid if {ECDSA-tryRecover} indicates no recover error for it.\n */\n function _recoverForwardRequestSigner(\n ForwardRequestData calldata request\n ) internal view virtual returns (bool, address) {\n (address recovered, ECDSA.RecoverError err, ) = _hashTypedDataV4(\n keccak256(\n abi.encode(\n _FORWARD_REQUEST_TYPEHASH,\n request.from,\n request.to,\n request.value,\n request.gas,\n request.nonce,\n request.deadline,\n keccak256(request.data)\n )\n )\n ).tryRecover(request.signature);\n\n return (err == ECDSA.RecoverError.NoError, recovered);\n }\n\n /**\n * @dev Validates and executes a signed request returning the request call `success` value.\n *\n * Internal function without msg.value validation.\n *\n * Requirements:\n *\n * - The caller must have provided enough gas to forward with the call.\n * - The request must be valid (see {verify}) if the `requireValidRequest` is true. This\n * includes the request's nonce being unconsumed; when `requireValidRequest` is false a\n * consumed nonce makes the request skippable instead of reverting the caller.\n * - The forwarded call must succeed if the `requireValidRequest` is true. Failures carrying\n * return data are bubbled up as-is; failures with empty return data revert with\n * {Errors-FailedCall}. When `requireValidRequest` is false the failure is reported through\n * the returned `success` flag instead, so a non-atomic batch can skip and refund it.\n *\n * Emits an {ExecutedForwardRequest} event.\n *\n * IMPORTANT: Using this function doesn't check that all the `msg.value` was sent, potentially\n * leaving value stuck in the contract.\n */\n function _execute(\n ForwardRequestData calldata request,\n bool requireValidRequest\n ) internal virtual returns (bool success) {\n (\n bool isTrustedForwarder,\n bool active,\n bool signerMatch,\n bool nonceValid,\n address signer\n ) = _validate(request);\n\n // Need to explicitly specify if a revert is required since non-reverting is default for\n // batches and reversion is opt-in since it could be useful in some scenarios\n if (requireValidRequest) {\n if (!isTrustedForwarder) {\n revert ERC2771CustomForwarder_UntrustfulTarget(\n request.to,\n address(this)\n );\n }\n\n if (!active) {\n revert ERC2771CustomForwarder_ExpiredRequest(request.deadline);\n }\n\n if (!signerMatch) {\n revert ERC2771CustomForwarder_InvalidSigner(\n signer,\n request.from\n );\n }\n\n // Raised here rather than by {Nonces-_useNonce} below so that the same condition\n // reverts when validity is required and is skipped when it is not. The error and\n // its arguments match what _useNonce would have raised.\n if (!nonceValid) {\n revert Nonces_InvalidAccountNonce(request.from, request.nonce);\n }\n }\n\n // Ignore an invalid request because requireValidRequest = false\n if (isTrustedForwarder && signerMatch && active && nonceValid) {\n // Nonce should be used before the call to prevent reusing by reentrancy.\n // Its revert is unreachable from here after the `nonceValid` gate above, but it\n // stays as the backstop for reentrancy and for overrides of this virtual function.\n _useNonce(request.from, request.nonce);\n\n uint256 reqGas = request.gas;\n address to = request.to;\n uint256 value = request.value;\n bytes memory data = abi.encodePacked(request.data, request.from);\n\n uint256 gasLeft;\n\n // gasLeft must be measured immediately after the call, before any other work,\n // otherwise the EIP-150 forwarded-gas check below can be bypassed.\n /// @solidity memory-safe-assembly\n assembly {\n success := call(\n reqGas,\n to,\n value,\n add(data, 0x20),\n mload(data),\n 0,\n 0\n )\n gasLeft := gas()\n\n // Bubble the target's revert data (if any), but only when the caller opted into\n // all-or-nothing execution (requireValidRequest = true, i.e. execute() or atomic\n // executeBatch). If there's no revert data, _execute returns false and the\n // Errors.FailedCall() check below reverts instead. Non-atomic batches must be able\n // to skip a failed request and refund it rather than reverting the whole batch.\n // Never copies return data on the success path.\n if iszero(success) {\n if requireValidRequest {\n let returnDataSize := returndatasize()\n if returnDataSize {\n let ptr := mload(0x40)\n returndatacopy(ptr, 0, returnDataSize)\n revert(ptr, returnDataSize)\n }\n }\n }\n }\n\n _checkForwardedGas(gasLeft, request);\n\n // The assembly above only bubbles failures that carry return data. An empty\n // revert (value sent to a nonpayable function, unknown selector on a target\n // without a fallback) would otherwise be swallowed, letting an atomic batch\n // continue past a failed request and refund its value to address(0). Revert\n // explicitly so {execute} and atomic {executeBatch} agree on every failure mode.\n // In non-atomic batches `success` is returned as-is, so the caller skips the\n // request and refunds it.\n if (requireValidRequest && !success) {\n revert Errors.FailedCall();\n }\n\n emit ExecutedForwardRequest(signer, request.nonce, success);\n }\n }\n\n /**\n * @dev Returns whether the target trusts this forwarder.\n *\n * This function performs a static call to the target contract calling the\n * {ERC2771Context-isTrustedForwarder} function.\n */\n function _isTrustedByTarget(address target) private view returns (bool) {\n bytes memory encodedParams = abi.encodeCall(\n ERC2771Context.isTrustedForwarder,\n (address(this))\n );\n\n bool success;\n uint256 returnSize;\n uint256 returnValue;\n /// @solidity memory-safe-assembly\n assembly {\n // Perform the staticcal and save the result in the scratch space.\n // | Location | Content | Content (Hex) |\n // |-----------|----------|--------------------------------------------------------------------|\n // | | | result ↓ |\n // | 0x00:0x1F | selector | 0x0000000000000000000000000000000000000000000000000000000000000001 |\n success := staticcall(\n gas(),\n target,\n add(encodedParams, 0x20),\n mload(encodedParams),\n 0,\n 0x20\n )\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n\n return success && returnSize >= 0x20 && returnValue > 0;\n }\n\n /**\n * @dev Checks if the requested gas was correctly forwarded to the callee.\n *\n * As a consequence of https://eips.ethereum.org/EIPS/eip-150[EIP-150]:\n * - At most `gasleft() - floor(gasleft() / 64)` is forwarded to the callee.\n * - At least `floor(gasleft() / 64)` is kept in the caller.\n *\n * It reverts consuming all the available gas if the forwarded gas is not the requested gas.\n *\n * IMPORTANT: The `gasLeft` parameter should be measured exactly at the end of the forwarded call.\n * Any gas consumed in between will make room for bypassing this check.\n */\n function _checkForwardedGas(\n uint256 gasLeft,\n ForwardRequestData calldata request\n ) private pure {\n // To avoid insufficient gas griefing attacks, as referenced in https://ronan.eth.limo/blog/ethereum-gas-dangers/\n //\n // A malicious relayer can attempt to shrink the gas forwarded so that the underlying call reverts out-of-gas\n // but the forwarding itself still succeeds. In order to make sure that the subcall received sufficient gas,\n // we will inspect gasleft() after the forwarding.\n //\n // Let X be the gas available before the subcall, such that the subcall gets at most X * 63 / 64.\n // We can't know X after CALL dynamic costs, but we want it to be such that X * 63 / 64 >= req.gas.\n // Let Y be the gas used in the subcall. gasleft() measured immediately after the subcall will be gasleft() = X - Y.\n // If the subcall ran out of gas, then Y = X * 63 / 64 and gasleft() = X - Y = X / 64.\n // Under this assumption req.gas / 63 > gasleft() is true is true if and only if\n // req.gas / 63 > X / 64, or equivalently req.gas > X * 63 / 64.\n // This means that if the subcall runs out of gas we are able to detect that insufficient gas was passed.\n //\n // We will now also see that req.gas / 63 > gasleft() implies that req.gas >= X * 63 / 64.\n // The contract guarantees Y <= req.gas, thus gasleft() = X - Y >= X - req.gas.\n // - req.gas / 63 > gasleft()\n // - req.gas / 63 >= X - req.gas\n // - req.gas >= X * 63 / 64\n // In other words if req.gas < X * 63 / 64 then req.gas / 63 <= gasleft(), thus if the relayer behaves honestly\n // the forwarding does not revert.\n if (gasLeft < request.gas / 63) {\n // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since\n // neither revert or assert consume all gas since Solidity 0.8.20\n // https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require\n /// @solidity memory-safe-assembly\n assembly {\n invalid()\n }\n }\n }\n}\n"},"contracts/utils/Nonces.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.28;\n\n/**\n * @dev Provides tracking nonces for addresses. Nonces just need to be unique, but do not need to be sequential.\n */\nabstract contract Nonces {\n mapping(address account => mapping(uint256 => bool)) private _nonces; // address => nonce => isUsed\n\n event NonceUsed(address indexed account, uint256 nonce);\n\n /**\n * @dev The nonce used for an `account` is not the expected current nonce.\n */\n error Nonces_InvalidAccountNonce(address account, uint256 nonce);\n\n /**\n * @dev Returns whether the nonce is valid for the given owner.\n * @param owner_ The owner of the nonce.\n * @param nonce_ The nonce to check.\n * @return Whether the nonce is valid.\n */\n function isNonceValid(\n address owner_,\n uint256 nonce_\n ) public view virtual returns (bool) {\n // bool defaults to false, so it stores isUsed. Therefore false means it is valid.\n return !_nonces[owner_][nonce_];\n }\n\n /**\n * @dev Consumes a provided nonce.\n * @param owner_ The owner of the nonce.\n */\n function _useNonce(address owner_, uint256 nonce_) internal virtual {\n if (!isNonceValid(owner_, nonce_)) {\n revert Nonces_InvalidAccountNonce(owner_, nonce_);\n }\n _nonces[owner_][nonce_] = true;\n emit NonceUsed(owner_, nonce_);\n }\n}\n"}}},"solcLongVersion":"0.8.28+commit.7893614a","solcVersion":"0.8.28"}