Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/collections/CollectionsManager.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {ERC721EnumerableUpgradeable} from
"@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {MulticallUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {CollectionsManagerBase} from "@src/collections/CollectionsManagerBase.sol";
import {CollectionsManagerCuratorActions} from "@src/collections/actions/CollectionsManagerCuratorActions.sol";
import {CollectionsManagerUserActions} from "@src/collections/actions/CollectionsManagerUserActions.sol";
import {CollectionsManagerView} from "@src/collections/actions/CollectionsManagerView.sol";
import {ICollectionsManager} from "@src/collections/interfaces/ICollectionsManager.sol";
import {DEFAULT_ADMIN_ROLE, ISizeFactory} from "@src/factory/interfaces/ISizeFactory.sol";
import {ISize} from "@src/market/interfaces/ISize.sol";
/// @title CollectionsManager
/// @custom:security-contact security@size.credit
/// @author Size (https://size.credit/)
/// @notice See the documentation in {ICollectionsManager}.
contract CollectionsManager is
ICollectionsManager,
CollectionsManagerBase,
ERC721EnumerableUpgradeable,
CollectionsManagerCuratorActions,
CollectionsManagerView,
CollectionsManagerUserActions,
MulticallUpgradeable,
UUPSUpgradeable
{
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(ISizeFactory _sizeFactory) external initializer {
__ERC721_init("Size Collections", "SIZE_COLLECTIONS");
__ERC721Enumerable_init();
__Multicall_init();
__UUPSUpgradeable_init();
sizeFactory = _sizeFactory;
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlySizeFactoryHasRole(DEFAULT_ADMIN_ROLE)
{}
function _baseURI() internal view virtual override returns (string memory) {
return string.concat("https://api.size.credit/collections/", Strings.toString(block.chainid), "/");
}
}
"
},
"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (proxy/utils/UUPSUpgradeable.sol)
pragma solidity ^0.8.22;
import {IERC1822Proxiable} from "@openzeppelin/contracts/interfaces/draft-IERC1822.sol";
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
import {Initializable} from "./Initializable.sol";
/**
* @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an
* {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy.
*
* A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is
* reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing
* `UUPSUpgradeable` with a custom implementation of upgrades.
*
* The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism.
*/
abstract contract UUPSUpgradeable is Initializable, IERC1822Proxiable {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address private immutable __self = address(this);
/**
* @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgradeTo(address)`
* and `upgradeToAndCall(address,bytes)` are present, and `upgradeTo` must be used if no function should be called,
* while `upgradeToAndCall` will invoke the `receive` function if the second argument is the empty byte string.
* If the getter returns `"5.0.0"`, only `upgradeToAndCall(address,bytes)` is present, and the second argument must
* be the empty byte string if no function should be called, making it impossible to invoke the `receive` function
* during an upgrade.
*/
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
/**
* @dev The call is from an unauthorized context.
*/
error UUPSUnauthorizedCallContext();
/**
* @dev The storage `slot` is unsupported as a UUID.
*/
error UUPSUnsupportedProxiableUUID(bytes32 slot);
/**
* @dev Check that the execution is being performed through a delegatecall call and that the execution context is
* a proxy contract with an implementation (as defined in ERC-1967) pointing to self. This should only be the case
* for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a
* function through ERC-1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to
* fail.
*/
modifier onlyProxy() {
_checkProxy();
_;
}
/**
* @dev Check that the execution is not being performed through a delegate call. This allows a function to be
* callable on the implementing contract but not through proxies.
*/
modifier notDelegated() {
_checkNotDelegated();
_;
}
function __UUPSUpgradeable_init() internal onlyInitializing {
}
function __UUPSUpgradeable_init_unchained() internal onlyInitializing {
}
/**
* @dev Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the
* implementation. It is used to validate the implementation's compatibility when performing an upgrade.
*
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
* function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.
*/
function proxiableUUID() external view virtual notDelegated returns (bytes32) {
return ERC1967Utils.IMPLEMENTATION_SLOT;
}
/**
* @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call
* encoded in `data`.
*
* Calls {_authorizeUpgrade}.
*
* Emits an {Upgraded} event.
*
* @custom:oz-upgrades-unsafe-allow-reachable delegatecall
*/
function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, data);
}
/**
* @dev Reverts if the execution is not performed via delegatecall or the execution
* context is not of a proxy with an ERC-1967 compliant implementation pointing to self.
*/
function _checkProxy() internal view virtual {
if (
address(this) == __self || // Must be called through delegatecall
ERC1967Utils.getImplementation() != __self // Must be called through an active proxy
) {
revert UUPSUnauthorizedCallContext();
}
}
/**
* @dev Reverts if the execution is performed via delegatecall.
* See {notDelegated}.
*/
function _checkNotDelegated() internal view virtual {
if (address(this) != __self) {
// Must not be called through delegatecall
revert UUPSUnauthorizedCallContext();
}
}
/**
* @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
* {upgradeToAndCall}.
*
* Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
*
* ```solidity
* function _authorizeUpgrade(address) internal onlyOwner {}
* ```
*/
function _authorizeUpgrade(address newImplementation) internal virtual;
/**
* @dev Performs an implementation upgrade with a security check for UUPS proxies, and additional setup call.
*
* As a security check, {proxiableUUID} is invoked in the new implementation, and the return value
* is expected to be the implementation slot in ERC-1967.
*
* Emits an {IERC1967-Upgraded} event.
*/
function _upgradeToAndCallUUPS(address newImplementation, bytes memory data) private {
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) {
revert UUPSUnsupportedProxiableUUID(slot);
}
ERC1967Utils.upgradeToAndCall(newImplementation, data);
} catch {
// The implementation is not UUPS
revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation);
}
}
}
"
},
"lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Enumerable.sol)
pragma solidity ^0.8.20;
import {ERC721Upgradeable} from "../ERC721Upgradeable.sol";
import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {Initializable} from "../../../proxy/utils/Initializable.sol";
/**
* @dev This implements an optional extension of {ERC721} defined in the ERC that adds enumerability
* of all the token ids in the contract as well as all token ids owned by each account.
*
* CAUTION: {ERC721} extensions that implement custom `balanceOf` logic, such as {ERC721Consecutive},
* interfere with enumerability and should not be used together with {ERC721Enumerable}.
*/
abstract contract ERC721EnumerableUpgradeable is Initializable, ERC721Upgradeable, IERC721Enumerable {
/// @custom:storage-location erc7201:openzeppelin.storage.ERC721Enumerable
struct ERC721EnumerableStorage {
mapping(address owner => mapping(uint256 index => uint256)) _ownedTokens;
mapping(uint256 tokenId => uint256) _ownedTokensIndex;
uint256[] _allTokens;
mapping(uint256 tokenId => uint256) _allTokensIndex;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC721Enumerable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC721EnumerableStorageLocation = 0x645e039705490088daad89bae25049a34f4a9072d398537b1ab2425f24cbed00;
function _getERC721EnumerableStorage() private pure returns (ERC721EnumerableStorage storage $) {
assembly {
$.slot := ERC721EnumerableStorageLocation
}
}
/**
* @dev An `owner`'s token query was out of bounds for `index`.
*
* NOTE: The owner being `address(0)` indicates a global out of bounds index.
*/
error ERC721OutOfBoundsIndex(address owner, uint256 index);
/**
* @dev Batch mint is not allowed.
*/
error ERC721EnumerableForbiddenBatchMint();
function __ERC721Enumerable_init() internal onlyInitializing {
}
function __ERC721Enumerable_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721Upgradeable) returns (bool) {
return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev See {IERC721Enumerable-tokenOfOwnerByIndex}.
*/
function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256) {
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
if (index >= balanceOf(owner)) {
revert ERC721OutOfBoundsIndex(owner, index);
}
return $._ownedTokens[owner][index];
}
/**
* @dev See {IERC721Enumerable-totalSupply}.
*/
function totalSupply() public view virtual returns (uint256) {
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
return $._allTokens.length;
}
/**
* @dev See {IERC721Enumerable-tokenByIndex}.
*/
function tokenByIndex(uint256 index) public view virtual returns (uint256) {
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
if (index >= totalSupply()) {
revert ERC721OutOfBoundsIndex(address(0), index);
}
return $._allTokens[index];
}
/**
* @dev See {ERC721-_update}.
*/
function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address) {
address previousOwner = super._update(to, tokenId, auth);
if (previousOwner == address(0)) {
_addTokenToAllTokensEnumeration(tokenId);
} else if (previousOwner != to) {
_removeTokenFromOwnerEnumeration(previousOwner, tokenId);
}
if (to == address(0)) {
_removeTokenFromAllTokensEnumeration(tokenId);
} else if (previousOwner != to) {
_addTokenToOwnerEnumeration(to, tokenId);
}
return previousOwner;
}
/**
* @dev Private function to add a token to this extension's ownership-tracking data structures.
* @param to address representing the new owner of the given token ID
* @param tokenId uint256 ID of the token to be added to the tokens list of the given address
*/
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
uint256 length = balanceOf(to) - 1;
$._ownedTokens[to][length] = tokenId;
$._ownedTokensIndex[tokenId] = length;
}
/**
* @dev Private function to add a token to this extension's token tracking data structures.
* @param tokenId uint256 ID of the token to be added to the tokens list
*/
function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
$._allTokensIndex[tokenId] = $._allTokens.length;
$._allTokens.push(tokenId);
}
/**
* @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
* while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
* gas optimizations e.g. when performing a transfer operation (avoiding double writes).
* This has O(1) time complexity, but alters the order of the _ownedTokens array.
* @param from address representing the previous owner of the given token ID
* @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
*/
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
// To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
// then delete the last slot (swap and pop).
uint256 lastTokenIndex = balanceOf(from);
uint256 tokenIndex = $._ownedTokensIndex[tokenId];
mapping(uint256 index => uint256) storage _ownedTokensByOwner = $._ownedTokens[from];
// When the token to delete is the last token, the swap operation is unnecessary
if (tokenIndex != lastTokenIndex) {
uint256 lastTokenId = _ownedTokensByOwner[lastTokenIndex];
_ownedTokensByOwner[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
$._ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
}
// This also deletes the contents at the last position of the array
delete $._ownedTokensIndex[tokenId];
delete _ownedTokensByOwner[lastTokenIndex];
}
/**
* @dev Private function to remove a token from this extension's token tracking data structures.
* This has O(1) time complexity, but alters the order of the _allTokens array.
* @param tokenId uint256 ID of the token to be removed from the tokens list
*/
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
// To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
// then delete the last slot (swap and pop).
uint256 lastTokenIndex = $._allTokens.length - 1;
uint256 tokenIndex = $._allTokensIndex[tokenId];
// When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
// rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
// an 'if' statement (like in _removeTokenFromOwnerEnumeration)
uint256 lastTokenId = $._allTokens[lastTokenIndex];
$._allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
$._allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
// This also deletes the contents at the last position of the array
delete $._allTokensIndex[tokenId];
$._allTokens.pop();
}
/**
* See {ERC721-_increaseBalance}. We need that to account tokens that were minted in batch
*/
function _increaseBalance(address account, uint128 amount) internal virtual override {
if (amount > 0) {
revert ERC721EnumerableForbiddenBatchMint();
}
super._increaseBalance(account, amount);
}
}
"
},
"lib/openzeppelin-contracts-upgradeable/contracts/utils/MulticallUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (utils/Multicall.sol)
pragma solidity ^0.8.20;
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ContextUpgradeable} from "./ContextUpgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Provides a function to batch together multiple calls in a single external call.
*
* Consider any assumption about calldata validation performed by the sender may be violated if it's not especially
* careful about sending transactions invoking {multicall}. For example, a relay address that filters function
* selectors won't filter calls nested within a {multicall} operation.
*
* NOTE: Since 5.0.1 and 4.9.4, this contract identifies non-canonical contexts (i.e. `msg.sender` is not {Context-_msgSender}).
* If a non-canonical context is identified, the following self `delegatecall` appends the last bytes of `msg.data`
* to the subcall. This makes it safe to use with {ERC2771Context}. Contexts that don't affect the resolution of
* {Context-_msgSender} are not propagated to subcalls.
*/
abstract contract MulticallUpgradeable is Initializable, ContextUpgradeable {
function __Multicall_init() internal onlyInitializing {
}
function __Multicall_init_unchained() internal onlyInitializing {
}
/**
* @dev Receives and executes a batch of function calls on this contract.
* @custom:oz-upgrades-unsafe-allow-reachable delegatecall
*/
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
bytes memory context = msg.sender == _msgSender()
? new bytes(0)
: msg.data[msg.data.length - _contextSuffixLength():];
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
results[i] = Address.functionDelegateCall(address(this), bytes.concat(data[i], context));
}
return results;
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/Strings.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (utils/Strings.sol)
pragma solidity ^0.8.20;
import {Math} from "./math/Math.sol";
import {SafeCast} from "./math/SafeCast.sol";
import {SignedMath} from "./math/SignedMath.sol";
/**
* @dev String operations.
*/
library Strings {
using SafeCast for *;
bytes16 private constant HEX_DIGITS = "0123456789abcdef";
uint8 private constant ADDRESS_LENGTH = 20;
uint256 private constant SPECIAL_CHARS_LOOKUP =
(1 << 0x08) | // backspace
(1 << 0x09) | // tab
(1 << 0x0a) | // newline
(1 << 0x0c) | // form feed
(1 << 0x0d) | // carriage return
(1 << 0x22) | // double quote
(1 << 0x5c); // backslash
/**
* @dev The `value` string doesn't fit in the specified `length`.
*/
error StringsInsufficientHexLength(uint256 value, uint256 length);
/**
* @dev The string being parsed contains characters that are not in scope of the given base.
*/
error StringsInvalidChar();
/**
* @dev The string being parsed is not a properly formatted address.
*/
error StringsInvalidAddressFormat();
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
assembly ("memory-safe") {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
assembly ("memory-safe") {
mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
/**
* @dev Converts a `int256` to its ASCII `string` decimal representation.
*/
function toStringSigned(int256 value) internal pure returns (string memory) {
return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value)));
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
return toHexString(value, Math.log256(value) + 1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
uint256 localValue = value;
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = HEX_DIGITS[localValue & 0xf];
localValue >>= 4;
}
if (localValue != 0) {
revert StringsInsufficientHexLength(value, length);
}
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal
* representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal
* representation, according to EIP-55.
*/
function toChecksumHexString(address addr) internal pure returns (string memory) {
bytes memory buffer = bytes(toHexString(addr));
// hash the hex part of buffer (skip length + 2 bytes, length 40)
uint256 hashValue;
assembly ("memory-safe") {
hashValue := shr(96, keccak256(add(buffer, 0x22), 40))
}
for (uint256 i = 41; i > 1; --i) {
// possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f)
if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) {
// case shift by xoring with 0x20
buffer[i] ^= 0x20;
}
hashValue >>= 4;
}
return string(buffer);
}
/**
* @dev Returns true if the two strings are equal.
*/
function equal(string memory a, string memory b) internal pure returns (bool) {
return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));
}
/**
* @dev Parse a decimal string and returns the value as a `uint256`.
*
* Requirements:
* - The string must be formatted as `[0-9]*`
* - The result must fit into an `uint256` type
*/
function parseUint(string memory input) internal pure returns (uint256) {
return parseUint(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseUint-string} that parses a substring of `input` located between position `begin` (included) and
* `end` (excluded).
*
* Requirements:
* - The substring must be formatted as `[0-9]*`
* - The result must fit into an `uint256` type
*/
function parseUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) {
(bool success, uint256 value) = tryParseUint(input, begin, end);
if (!success) revert StringsInvalidChar();
return value;
}
/**
* @dev Variant of {parseUint-string} that returns false if the parsing fails because of an invalid character.
*
* NOTE: This function will revert if the result does not fit in a `uint256`.
*/
function tryParseUint(string memory input) internal pure returns (bool success, uint256 value) {
return _tryParseUintUncheckedBounds(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseUint-string-uint256-uint256} that returns false if the parsing fails because of an invalid
* character.
*
* NOTE: This function will revert if the result does not fit in a `uint256`.
*/
function tryParseUint(
string memory input,
uint256 begin,
uint256 end
) internal pure returns (bool success, uint256 value) {
if (end > bytes(input).length || begin > end) return (false, 0);
return _tryParseUintUncheckedBounds(input, begin, end);
}
/**
* @dev Implementation of {tryParseUint-string-uint256-uint256} that does not check bounds. Caller should make sure that
* `begin <= end <= input.length`. Other inputs would result in undefined behavior.
*/
function _tryParseUintUncheckedBounds(
string memory input,
uint256 begin,
uint256 end
) private pure returns (bool success, uint256 value) {
bytes memory buffer = bytes(input);
uint256 result = 0;
for (uint256 i = begin; i < end; ++i) {
uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i)));
if (chr > 9) return (false, 0);
result *= 10;
result += chr;
}
return (true, result);
}
/**
* @dev Parse a decimal string and returns the value as a `int256`.
*
* Requirements:
* - The string must be formatted as `[-+]?[0-9]*`
* - The result must fit in an `int256` type.
*/
function parseInt(string memory input) internal pure returns (int256) {
return parseInt(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseInt-string} that parses a substring of `input` located between position `begin` (included) and
* `end` (excluded).
*
* Requirements:
* - The substring must be formatted as `[-+]?[0-9]*`
* - The result must fit in an `int256` type.
*/
function parseInt(string memory input, uint256 begin, uint256 end) internal pure returns (int256) {
(bool success, int256 value) = tryParseInt(input, begin, end);
if (!success) revert StringsInvalidChar();
return value;
}
/**
* @dev Variant of {parseInt-string} that returns false if the parsing fails because of an invalid character or if
* the result does not fit in a `int256`.
*
* NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`.
*/
function tryParseInt(string memory input) internal pure returns (bool success, int256 value) {
return _tryParseIntUncheckedBounds(input, 0, bytes(input).length);
}
uint256 private constant ABS_MIN_INT256 = 2 ** 255;
/**
* @dev Variant of {parseInt-string-uint256-uint256} that returns false if the parsing fails because of an invalid
* character or if the result does not fit in a `int256`.
*
* NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`.
*/
function tryParseInt(
string memory input,
uint256 begin,
uint256 end
) internal pure returns (bool success, int256 value) {
if (end > bytes(input).length || begin > end) return (false, 0);
return _tryParseIntUncheckedBounds(input, begin, end);
}
/**
* @dev Implementation of {tryParseInt-string-uint256-uint256} that does not check bounds. Caller should make sure that
* `begin <= end <= input.length`. Other inputs would result in undefined behavior.
*/
function _tryParseIntUncheckedBounds(
string memory input,
uint256 begin,
uint256 end
) private pure returns (bool success, int256 value) {
bytes memory buffer = bytes(input);
// Check presence of a negative sign.
bytes1 sign = begin == end ? bytes1(0) : bytes1(_unsafeReadBytesOffset(buffer, begin)); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
bool positiveSign = sign == bytes1("+");
bool negativeSign = sign == bytes1("-");
uint256 offset = (positiveSign || negativeSign).toUint();
(bool absSuccess, uint256 absValue) = tryParseUint(input, begin + offset, end);
if (absSuccess && absValue < ABS_MIN_INT256) {
return (true, negativeSign ? -int256(absValue) : int256(absValue));
} else if (absSuccess && negativeSign && absValue == ABS_MIN_INT256) {
return (true, type(int256).min);
} else return (false, 0);
}
/**
* @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as a `uint256`.
*
* Requirements:
* - The string must be formatted as `(0x)?[0-9a-fA-F]*`
* - The result must fit in an `uint256` type.
*/
function parseHexUint(string memory input) internal pure returns (uint256) {
return parseHexUint(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseHexUint-string} that parses a substring of `input` located between position `begin` (included) and
* `end` (excluded).
*
* Requirements:
* - The substring must be formatted as `(0x)?[0-9a-fA-F]*`
* - The result must fit in an `uint256` type.
*/
function parseHexUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) {
(bool success, uint256 value) = tryParseHexUint(input, begin, end);
if (!success) revert StringsInvalidChar();
return value;
}
/**
* @dev Variant of {parseHexUint-string} that returns false if the parsing fails because of an invalid character.
*
* NOTE: This function will revert if the result does not fit in a `uint256`.
*/
function tryParseHexUint(string memory input) internal pure returns (bool success, uint256 value) {
return _tryParseHexUintUncheckedBounds(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseHexUint-string-uint256-uint256} that returns false if the parsing fails because of an
* invalid character.
*
* NOTE: This function will revert if the result does not fit in a `uint256`.
*/
function tryParseHexUint(
string memory input,
uint256 begin,
uint256 end
) internal pure returns (bool success, uint256 value) {
if (end > bytes(input).length || begin > end) return (false, 0);
return _tryParseHexUintUncheckedBounds(input, begin, end);
}
/**
* @dev Implementation of {tryParseHexUint-string-uint256-uint256} that does not check bounds. Caller should make sure that
* `begin <= end <= input.length`. Other inputs would result in undefined behavior.
*/
function _tryParseHexUintUncheckedBounds(
string memory input,
uint256 begin,
uint256 end
) private pure returns (bool success, uint256 value) {
bytes memory buffer = bytes(input);
// skip 0x prefix if present
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
uint256 offset = hasPrefix.toUint() * 2;
uint256 result = 0;
for (uint256 i = begin + offset; i < end; ++i) {
uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i)));
if (chr > 15) return (false, 0);
result *= 16;
unchecked {
// Multiplying by 16 is equivalent to a shift of 4 bits (with additional overflow check).
// This guarantees that adding a value < 16 will not cause an overflow, hence the unchecked.
result += chr;
}
}
return (true, result);
}
/**
* @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as an `address`.
*
* Requirements:
* - The string must be formatted as `(0x)?[0-9a-fA-F]{40}`
*/
function parseAddress(string memory input) internal pure returns (address) {
return parseAddress(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseAddress-string} that parses a substring of `input` located between position `begin` (included) and
* `end` (excluded).
*
* Requirements:
* - The substring must be formatted as `(0x)?[0-9a-fA-F]{40}`
*/
function parseAddress(string memory input, uint256 begin, uint256 end) internal pure returns (address) {
(bool success, address value) = tryParseAddress(input, begin, end);
if (!success) revert StringsInvalidAddressFormat();
return value;
}
/**
* @dev Variant of {parseAddress-string} that returns false if the parsing fails because the input is not a properly
* formatted address. See {parseAddress-string} requirements.
*/
function tryParseAddress(string memory input) internal pure returns (bool success, address value) {
return tryParseAddress(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseAddress-string-uint256-uint256} that returns false if the parsing fails because input is not a properly
* formatted address. See {parseAddress-string-uint256-uint256} requirements.
*/
function tryParseAddress(
string memory input,
uint256 begin,
uint256 end
) internal pure returns (bool success, address value) {
if (end > bytes(input).length || begin > end) return (false, address(0));
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
uint256 expectedLength = 40 + hasPrefix.toUint() * 2;
// check that input is the correct length
if (end - begin == expectedLength) {
// length guarantees that this does not overflow, and value is at most type(uint160).max
(bool s, uint256 v) = _tryParseHexUintUncheckedBounds(input, begin, end);
return (s, address(uint160(v)));
} else {
return (false, address(0));
}
}
function _tryParseChr(bytes1 chr) private pure returns (uint8) {
uint8 value = uint8(chr);
// Try to parse `chr`:
// - Case 1: [0-9]
// - Case 2: [a-f]
// - Case 3: [A-F]
// - otherwise not supported
unchecked {
if (value > 47 && value < 58) value -= 48;
else if (value > 96 && value < 103) value -= 87;
else if (value > 64 && value < 71) value -= 55;
else return type(uint8).max;
}
return value;
}
/**
* @dev Escape special characters in JSON strings. This can be useful to prevent JSON injection in NFT metadata.
*
* WARNING: This function should only be used in double quoted JSON strings. Single quotes are not escaped.
*
* NOTE: This function escapes all unicode characters, and not just the ones in ranges defined in section 2.5 of
* RFC-4627 (U+0000 to U+001F, U+0022 and U+005C). ECMAScript's `JSON.parse` does recover escaped unicode
* characters that are not in this range, but other tooling may provide different results.
*/
function escapeJSON(string memory input) internal pure returns (string memory) {
bytes memory buffer = bytes(input);
bytes memory output = new bytes(2 * buffer.length); // worst case scenario
uint256 outputLength = 0;
for (uint256 i; i < buffer.length; ++i) {
bytes1 char = bytes1(_unsafeReadBytesOffset(buffer, i));
if (((SPECIAL_CHARS_LOOKUP & (1 << uint8(char))) != 0)) {
output[outputLength++] = "\\";
if (char == 0x08) output[outputLength++] = "b";
else if (char == 0x09) output[outputLength++] = "t";
else if (char == 0x0a) output[outputLength++] = "n";
else if (char == 0x0c) output[outputLength++] = "f";
else if (char == 0x0d) output[outputLength++] = "r";
else if (char == 0x5c) output[outputLength++] = "\\";
else if (char == 0x22) {
// solhint-disable-next-line quotes
output[outputLength++] = '"';
}
} else {
output[outputLength++] = char;
}
}
// write the actual length and deallocate unused memory
assembly ("memory-safe") {
mstore(output, outputLength)
mstore(0x40, add(output, shl(5, shr(5, add(outputLength, 63)))))
}
return string(output);
}
/**
* @dev Reads a bytes32 from a bytes array without bounds checking.
*
* NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the
* assembly block as such would prevent some optimizations.
*/
function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) {
// This is not memory safe in the general case, but all calls to this private function are within bounds.
assembly ("memory-safe") {
value := mload(add(buffer, add(0x20, offset)))
}
}
}
"
},
"src/collections/CollectionsManagerBase.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {ISizeFactory} from "@src/factory/interfaces/ISizeFactory.sol";
import {ISize} from "@src/market/interfaces/ISize.sol";
import {CopyLimitOrderConfig} from "@src/market/libraries/OfferLibrary.sol";
struct MarketInformation {
bool initialized;
CopyLimitOrderConfig ___deprecated_copyLoanOfferConfig;
CopyLimitOrderConfig ___deprecated_copyBorrowOfferConfig;
EnumerableSet.AddressSet rateProviders;
}
struct UserCollectionCopyLimitOrderConfigs {
CopyLimitOrderConfig copyLoanOfferConfig;
CopyLimitOrderConfig copyBorrowOfferConfig;
}
/// @title CollectionManagerStorage
/// @custom:security-contact security@size.credit
/// @author Size (https://size.credit/)
/// @dev Introduced in v1.8
abstract contract CollectionsManagerBase {
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
// size factory
ISizeFactory sizeFactory;
// collection Id counter
uint256 collectionIdCounter;
// mapping of collection Id to collection
mapping(uint256 collectionId => mapping(ISize market => MarketInformation marketInformation) collection) collections;
// mapping of user to collection Ids set
mapping(address user => EnumerableSet.UintSet collectionIds) userToCollectionIds;
// mapping of user to collection Ids to CopyLimitOrderConfig
mapping(
address user
=> mapping(uint256 collectionId => UserCollectionCopyLimitOrderConfigs userCollectionCopyLimitOrderConfigs)
) userToCollectionCopyLimitOrderConfigs;
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error InvalidCollectionId(uint256 collectionId);
error OnlySizeFactory(address user);
error MarketNotInCollection(uint256 collectionId, address market);
/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/
modifier onlySizeFactoryHasRole(bytes32 role) {
if (!AccessControlUpgradeable(address(sizeFactory)).hasRole(role, msg.sender)) {
revert IAccessControl.AccessControlUnauthorizedAccount(msg.sender, role);
}
_;
}
modifier onlySizeFactory() {
if (msg.sender != address(sizeFactory)) {
revert OnlySizeFactory(msg.sender);
}
_;
}
}
"
},
"src/collections/actions/CollectionsManagerCuratorActions.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {ERC721EnumerableUpgradeable} from
"@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {CopyLimitOrderConfig, OfferLibrary} from "@src/market/libraries/OfferLibrary.sol";
import {CollectionsManagerBase} from "@src/collections/CollectionsManagerBase.sol";
import {ICollectionsManagerCuratorActions} from "@src/collections/interfaces/ICollectionsManagerCuratorActions.sol";
import {ISizeFactory} from "@src/factory/interfaces/ISizeFactory.sol";
import {ISize} from "@src/market/interfaces/ISize.sol";
import {Errors} from "@src/market/libraries/Errors.sol";
bytes32 constant DEFAULT_ADMIN_ROLE = 0x00;
/// @title CollectionsManagerCuratorActions
/// @custom:security-contact security@size.credit
/// @author Size (https://size.credit/)
/// @notice See the documentation in {ICollectionsManagerCuratorActions}.
abstract contract CollectionsManagerCuratorActions is
ICollectionsManagerCuratorActions,
CollectionsManagerBase,
ERC721EnumerableUpgradeable
{
using EnumerableSet for EnumerableSet.AddressSet;
/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/
modifier onlyCollectionCuratorAuthorized(uint256 collectionId) {
_checkAuthorized(ownerOf(collectionId), msg.sender, collectionId);
_;
}
/*//////////////////////////////////////////////////////////////
CURATOR ACTIONS
//////////////////////////////////////////////////////////////*/
/// @inheritdoc ICollectionsManagerCuratorActions
function createCollection() external returns (uint256 collectionId) {
collectionId = collectionIdCounter++;
_safeMint(msg.sender, collectionId);
}
/// @inheritdoc ICollectionsManagerCuratorActions
function addMarketsToCollection(uint256 collectionId, ISize[] memory markets)
external
onlyCollectionCuratorAuthorized(collectionId)
{
// slither-disable-start calls-loop
for (uint256 i = 0; i < markets.length; i++) {
if (!sizeFactory.isMarket(address(markets[i]))) {
revert Errors.INVALID_MARKET(address(markets[i]));
}
if (PausableUpgradeable(address(markets[i])).paused()) {
revert Errors.PAUSED_MARKET(address(markets[i]));
}
collections[collectionId][markets[i]].initialized = true;
emit MarketAddedToCollection(collectionId, address(markets[i]));
}
// slither-disable-end calls-loop
}
/// @inheritdoc ICollectionsManagerCuratorActions
function removeMarketsFromCollection(uint256 collectionId, ISize[] memory markets)
external
onlyCollectionCuratorAuthorized(collectionId)
{
for (uint256 i = 0; i < markets.length; i++) {
address[] memory rateProviders = collections[collectionId][markets[i]].rateProviders.values();
removeRateProvidersFromCollectionMarket(collectionId, markets[i], rateProviders);
delete collections[collectionId][markets[i]];
emit MarketRemovedFromCollection(collectionId, address(markets[i]));
}
}
/// @inheritdoc ICollectionsManagerCuratorActions
function addRateProvidersToCollectionMarket(uint256 collectionId, ISize market, address[] memory rateProviders)
external
onlyCollectionCuratorAuthorized(collectionId)
{
if (!collections[collectionId][market].initialized) {
revert MarketNotInCollection(collectionId, address(market));
}
for (uint256 i = 0; i < rateProviders.length; i++) {
bool added = collections[collectionId][market].rateProviders.add(rateProviders[i]);
if (added) {
emit RateProviderAddedToMarket(collectionId, address(market), rateProviders[i]);
}
}
}
/// @inheritdoc ICollectionsManagerCuratorActions
function removeRateProvidersFromCollectionMarket(uint256 collectionId, ISize market, address[] memory rateProviders)
public
onlyCollectionCuratorAuthorized(collectionId)
{
if (!collections[collectionId][market].initialized) {
revert MarketNotInCollection(collectionId, address(market));
}
for (uint256 i = 0; i < rateProviders.length; i++) {
bool removed = collections[collectionId][market].rateProviders.remove(rateProviders[i]);
if (removed) {
emit RateProviderRemovedFromMarket(collectionId, address(market), rateProviders[i]);
}
}
}
}
"
},
"src/collections/actions/CollectionsManagerUserActions.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {UserCollectionCopyLimitOrderConfigs} from "@src/collections/CollectionsManagerBase.sol";
import {CollectionsManagerView} from "@src/collections/actions/CollectionsManagerView.sol";
import {ICollectionsManagerUserActions} from "@src/collections/interfaces/ICollectionsManagerUserActions.sol";
import {CopyLimitOrderConfig, OfferLibrary} from "@src/market/libraries/OfferLibrary.sol";
/// @title CollectionsManagerUserActions
/// @custom:security-contact security@size.credit
/// @author Size (https://size.credit/)
/// @notice See the documentation in {ICollectionsManagerUserActions}.
abstract contract CollectionsManagerUserActions is ICollectionsManagerUserActions, CollectionsManagerView {
using EnumerableSet for EnumerableSet.UintSet;
/*//////////////////////////////////////////////////////////////
USER ACTIONS
//////////////////////////////////////////////////////////////*/
function subscribeUserToCollections(address user, uint256[] memory collectionIds) external onlySizeFactory {
CopyLimitOrderConfig memory fullCopy = CopyLimitOrderConfig({
minTenor: 0,
maxTenor: type(uint256).max,
minAPR: 0,
maxAPR: type(uint256).max,
offsetAPR: 0
});
for (uint256 i = 0; i < collectionIds.length; i++) {
if (!isValidCollectionId(collectionIds[i])) {
revert InvalidCollectionId(collectionIds[i]);
}
bool added = userToCollectionIds[user].add(collectionIds[i]);
if (added) {
emit SubscribedToCollection(user, collectionIds[i]);
_setUserCollectionCopyLimitOrderConfigs(user, collectionIds[i], fullCopy, fullCopy);
}
}
}
function unsubscribeUserFromCollections(address user, uint256[] memory collectionIds) external onlySizeFactory {
// slither-disable-next-line uninitialized-local
CopyLimitOrderConfig memory nullCopy;
for (uint256 i = 0; i < collectionIds.length; i++) {
if (!isValidCollectionId(collectionIds[i])) {
revert InvalidCollectionId(collectionIds[i]);
}
bool removed = userToCollectionIds[user].remove(collectionIds[i]);
if (removed) {
emit UnsubscribedFromCollection(user, collectionIds[i]);
_setUserCollectionCopyLimitOrderConfigs(user, collectionIds[i], nullCopy, nullCopy);
}
}
}
function setUserCollectionCopyLimitOrderConfigs(
address user,
uint256 collectionId,
CopyLimitOrderConfig memory copyLoanOfferConfig,
CopyLimitOrderConfig memory copyBorrowOfferConfig
) external onlySizeFactory {
_setUserCollectionCopyLimitOrderConfigs(user, collectionId, copyLoanOfferConfig, copyBorrowOfferConfig);
}
function _setUserCollectionCopyLimitOrderConfigs(
address user,
uint256 collectionId,
CopyLimitOrderConfig memory copyLoanOfferConfig,
CopyLimitOrderConfig memory copyBorrowOfferConfig
) private {
if (!isValidCollectionId(collectionId)) {
revert InvalidCollectionId(collectionId);
}
OfferLibrary.validateCopyLimitOrderConfigs(copyLoanOfferConfig, copyBorrowOfferConfig);
userToCollectionCopyLimitOrderConfigs[user][collectionId] = UserCollectionCopyLimitOrderConfigs({
copyLoanOfferConfig: copyLoanOfferConfig,
copyBorrowOfferConfig: copyBorrowOfferConfig
});
emit SetUserCollectionCopyLimitOrderConfigs(user, collectionId, copyLoanOfferConfig, copyBorrowOfferConfig);
}
}
"
},
"src/collections/actions/CollectionsManagerView.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {CollectionsManagerBase} from "@src/collections/CollectionsManagerBase.sol";
import {ICollectionsManagerView} from "@src/collections/interfaces/ICollectionsManagerView.sol";
import {RESERVED_ID} from "@src/market/libraries/LoanLibrary.sol";
import {ISize} from "@src/market/interfaces/ISize.sol";
import {CopyLimitOrderConfig, LimitOrder, OfferLibrary} from "@src/market/libraries/OfferLibrary.sol";
/// @title CollectionsManagerView
/// @custom:security-contact security@size.credit
/// @author Size (https://size.credit/)
/// @notice See the documentation in {ICollectionsManagerView}.
abstract contract CollectionsManagerView is ICollectionsManagerView, CollectionsManagerBase {
using EnumerableSet for EnumerableSet.UintSet;
using EnumerableSet for EnumerableSet.AddressSet;
using OfferLibrary for CopyLimitOrderConfig;
using OfferLibrary for LimitOrder;
/*//////////////////////////////////////////////////////////////
COLLECTION VIEW
//////////////////////////////////////////////////////////////*/
/// @inheritdoc ICollectionsManagerView
function isValidCollectionId(uint256 collectionId) public view returns (bool) {
return collectionId < collectionIdCounter;
}
/// @inheritdoc ICollectionsManagerView
function isSubscribedToCollection(address user, uint256 collectionId) external view returns (bool) {
return userToCollectionIds[user].contains(collectionId);
}
/// @inheritdoc ICollectionsManagerView
function collectionContainsMarket(uint256 collectionId, ISize market) external view returns (bool) {
if (!isValidCollectionId(collectionId)) {
return false;
}
return collections[collectionId][market].initialized;
}
/// @inheritdoc ICollectionsManagerView
function getCollectionMarketRateProviders(uint256 collectionId, ISize market)
external
view
returns (address[] memory)
{
if (!isValidCollectionId(collectionId)) {
revert InvalidCollectionId(collectionId);
}
if (!collections[collectionId][market].initialized) {
revert MarketNotInCollection(collectionId, address(market));
}
return collections[collectionId][market].rateProviders.values();
}
/// @inheritdoc ICollectionsManagerView
function isCopyingCollectionMarketRateProvider(
address user,
uint256 collectionId,
ISize market,
address rateProvider
) public view returns (bool) {
if (!isValidCollectionId(collectionId)) {
return false;
}
if (!userToCollectionIds[user].contains(collectionId)) {
return false;
}
if (!collections[collectionId][market].initialized) {
return false;
}
return collections[collectionId][market].rateProviders.contains(rateProvider);
}
/// @inheritdoc ICollectionsManagerView
function getSubscribedCollections(address user) external view returns (uint256[] memory collectionIds) {
return userToCollectionIds[user].values();
}
/*//////////////////////////////////////////////////////////////
APR VIEW
//////////////////////////////////////////////////////////////*/
function _isUserDefinedLimitOrderNull(address user, ISize market, bool isLoanOffer) private view returns (bool) {
return isLoanOffer ? market.isUserDefinedLoanOfferNull(user) : market.isUserDefinedBorrowOfferNull(user);
}
/// @inheritdoc ICollectionsManagerView
function getLoanOfferAPR(address user, uint256 collectionId, ISize market, address rateProvider, uint256 tenor)
external
view
returns (uint256 apr)
{
return getLimitOrderAPR(user, collectionId, market, rateProvider, tenor, true);
}
/// @inheritdoc ICollectionsManagerView
function getBorrowOfferAPR(address user, uint256 collectionId, ISize market, address rateProvider, uint256 tenor)
external
view
returns (uint256 apr)
{
return getLimitOrderAPR(user, collectionId, market, rateProvider, tenor, false);
}
function getLimitOrderAPR(
address user,
uint256 collectionId,
ISize market,
address rateProvider,
uint256 tenor,
bool isLoanOffer
) public view returns (uint256 apr) {
// if collectionId is RESERVED_ID, return the user-defined yield curve
// and ignore the user-defined CopyLimitOrderConfig params
if (collectionId == RESERVED_ID) {
return _getUserDefinedLimitOrderAPR(user, market, tenor, isLoanOffer);
}
// else if the user is not copying the collection market rate provider, revert
else if (!isCopyingCollectionMarketRateProvider(user, collectionId, market, rateProvider)) {
revert InvalidCollectionMarketRateProvider(collectionId, address(market), rateProvider);
}
// else, return the yield curve for that collection, market and rate provider
else {
// validate min/max tenor
CopyLimitOrderConfig memory copyLimitOrder =
_getCopyLimitOrderConfig(user, collectionId, market, isLoanOffer);
if (tenor < copyLimitOrder.minTenor || tenor > copyLimitOrder.maxTenor) {
revert InvalidTenor(tenor, copyLimitOrder.minTenor, copyLimitOrder.maxTenor);
} else {
uint256 baseAPR = _getUserDefinedLimitOrderAPR(rateProvider, market, tenor, isLoanOffer);
// apply offset APR
apr = SafeCast.toUint256(SafeCast.toInt256(baseAPR) + copyLimitOrder.offsetAPR);
// validate min/max APR
if (apr < copyLimitOrder.minAPR) {
apr = copyLimitOrder.minAPR;
} else if (apr > copyLimitOrder.maxAPR) {
apr = copyLimitOrder.maxAPR;
}
}
}
}
function _getUserDefinedLimitOrderAPR(address user, ISize market, uint256 tenor, bool isLoanOffer)
private
view
returns (uint256 apr)
{
if (isLoanOffer) {
return market.getUserDefinedLoanOfferAPR(user, tenor);
} else {
return market.getUserDefinedBorrowOfferAPR(user, tenor);
}
}
/// @inheritdoc ICollectionsManagerView
function isLoanAPRGreaterThanBorrowOfferAPRs(address user, uint256 loanAPR, ISize market, uint256 tenor)
external
view
returns (bool)
{
return _isAPRLowerThanOfferAPRs(user, loanAPR, market, tenor, true);
}
/// @inheritdoc ICollectionsManagerView
function isBorrowAPRLowerThanLoanOfferAPRs(address user, uint256 borrowAPR, ISize market, uint256 tenor)
external
view
returns (bool)
{
return _isAPRLowerThanOfferAPRs(user, borrowAPR, market, tenor, false);
}
// slither-disable-start var-read-using-this
// slither-disable-start calls-loop
function _isAPRLowerThanOfferAPRs(address user, uint256 apr, ISize market, uint256 tenor, bool aprIsLoanAPR)
private
view
returns (bool)
{
// collections check
EnumerableSet.UintSet storage collectionIds = userToCollectionIds[user];
for (uint256 i = 0; i < collectionIds.length(); i++) {
uint256 collectionId = collectionIds.at(i);
if (!collections[collectionId][market].initialized) {
continue;
}
EnumerableSet.AddressSet storage rateProviders = collections[collectionId][market].rateProviders;
for (uint256 j = 0; j < rateProviders.length(); j++) {
address rateProvider = rateProviders.at(j);
if (_isUserDefinedLimitOrderNull(rateProvider, market, !aprIsLoanAPR)) {
continue;
}
try this.getLimitOrderAPR(user, collectionId, market, rateProvider, tenor, !aprIsLoanAPR) returns (
uint256 otherAPR
) {
if ((aprIsLoanAPR && otherAPR >= apr) || (!aprIsLoanAPR && apr >= otherAPR)) {
return false;
}
} catch (bytes memory) {
// N/A
}
}
}
// user-defined check
if (_isUserDefinedLimitOrderNull(user, market, !aprIsLoanAPR)) {
return true;
} else {
try this.getLimitOrderAPR(user, RESERVED_ID, market, address(0), tenor, !aprIsLoanAPR) returns (
uint256 otherAPR
) {
if ((aprIsLoanAPR && otherAPR >= apr) || (!aprIsLoanAPR && apr >= otherAPR)) {
return false;
}
} catch (bytes memory) {
// N/A
}
return true;
}
}
// slither-disable-end calls-loop
// slither-disable-end var-read-using-this
/*//////////////////////////////////////////////////////////////
COPY LIMIT ORDER VIEW
//////////////////////////////////////////////////////////////*/
/// @inheritdoc ICollectionsManagerView
function getUserDefinedCollectionCopyLoanOfferConfig(address user, uint256 collectionId)
external
view
returns (CopyLimitOrderConfig memory)
{
return _getUserDefinedCollectionCopyLimitOrderConfig(user, collectionId, true);
}
/// @inheritdoc ICollectionsManagerView
function getUserDefinedCollectionCopyBorrowOfferConfig(address user, uint256 collectionId)
external
view
returns (CopyLimitOrderConfig memory)
{
return _getUserDefinedCollectionCopyLimitOrderConfig(user, collectionId, false);
}
/// @dev Reverts if the collection id is invalid or the market is not in the collection
/// @dev Changed in v1.8.1
function _getCopyLimitOrderConfig(address user, uint256 collectionId, ISize market, bool isLoanOffer)
private
view
returns (CopyLimitOrderConfig memory copyLimitOrder)
{
copyLimitOrder = _getUserDefinedMarketCopyLimitOrderConfig(user, market, isLoanOffer);
if (copyLimitOrder.isNull()) {
copyLimitOrder = _getUserDefinedCollectionCopyLimitOrderConfig(user, collectionId, isLoanOffer);
}
}
function _getUserDefinedMarketCopyLimitOrderConfig(address user, ISize market, bool isLoanOffer)
private
view
returns (CopyLimitOrderConfig memory copyLimitOrder)
{
return isLoanOffer
? market.getUserDefinedCopyLoanOfferConfig(user)
: market.getUserDefinedCopyBorrowOfferConfig(user);
}
function _getUserDefinedCollectionCopyLimitOrderConfig(address user, uint256 collectionId, bool isLoanOffer)
private
view
returns (CopyLimitOrderConfig memory copyLimitOrder)
{
return isLoanOffer
? userToCollectionCopyLimitOrderConfigs[user][collectionId].copyLoanOfferConfig
: userToCollectionCopyLimitOrderConfigs[user][collectionId].copyBorrowOfferConfig;
}
}
"
},
"src/collections/interfaces/ICollectionsManager.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {ICollectionsManagerCuratorActions} from "@src/collections/interfaces/ICollectionsManagerCuratorActions.sol";
import {ICollectionsManagerUserActions} from "@src/collections/interfaces/ICollectionsManagerUserActions.sol";
import {ICollectionsManagerView} from "@src/collections/interfaces/ICollectionsManagerView.sol";
/// @title ICollectionsManager
/// @custom:security-contact security@size.credit
/// @author Size (https://size.credit/)
interface ICollectionsManager is
ICollectionsManagerCuratorActions,
ICollectionsManagerUserActions,
Submitted on: 2025-10-24 20:47:56
Comments
Log in to comment.
No comments yet.