// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.17;
uint256 constant CIRCULAR_TENSION_ID = 1;
uint256 constant FALLEN_ANGEL_ID = 2;
uint256 constant RED_NIGHT_ID = 3;
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/cryptography/ECDSA.sol)
pragma solidity ^0.8.0;
import "../Strings.sol";
/**
* @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
*
* These functions can be used to verify that a message was signed by the holder
* of the private keys of a given address.
*/
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS,
InvalidSignatureV // Deprecated in v4.8
}
function _throwError(RecoverError error) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert("ECDSA: invalid signature");
} else if (error == RecoverError.InvalidSignatureLength) {
revert("ECDSA: invalid signature length");
} else if (error == RecoverError.InvalidSignatureS) {
revert("ECDSA: invalid signature 's' value");
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature` or error string. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*
* Documentation for signature generation:
* - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
* - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength);
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature`. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*/
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
*
* See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures]
*
* _Available since v4.3._
*/
function tryRecover(
bytes32 hash,
bytes32 r,
bytes32 vs
) internal pure returns (address, RecoverError) {
bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
*
* _Available since v4.2._
*/
function recover(
bytes32 hash,
bytes32 r,
bytes32 vs
) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, r, vs);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `v`,
* `r` and `s` signature fields separately.
*
* _Available since v4.3._
*/
function tryRecover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address, RecoverError) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}
return (signer, RecoverError.NoError);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, v, r, s);
_throwError(error);
return recovered;
}
/**
* @dev Returns an Ethereum Signed Message, created from a `hash`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
// 32 is the length in bytes of hash,
// enforced by the type signature above
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
/**
* @dev Returns an Ethereum Signed Message, created from `s`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s));
}
/**
* @dev Returns an Ethereum Signed Typed Data, created from a
* `domainSeparator` and a `structHash`. This produces hash corresponding
* to the one signed with the
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`]
* JSON-RPC method as part of EIP-712.
*
* See {recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ECDSA.sol";
/**
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
*
* The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible,
* thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding
* they need in their contracts using a combination of `abi.encode` and `keccak256`.
*
* This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
* ({_hashTypedDataV4}).
*
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
* the chain id to protect against replay attacks on an eventual fork of the chain.
*
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
*
* _Available since v3.4._
*/
abstract contract EIP712 {
/* solhint-disable var-name-mixedcase */
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
// invalidate the cached domain separator if the chain id changes.
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;
address private immutable _CACHED_THIS;
bytes32 private immutable _HASHED_NAME;
bytes32 private immutable _HASHED_VERSION;
bytes32 private immutable _TYPE_HASH;
/* solhint-enable var-name-mixedcase */
/**
* @dev Initializes the domain separator and parameter caches.
*
* The meaning of `name` and `version` is specified in
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]:
*
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
* - `version`: the current major version of the signing domain.
*
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
* contract upgrade].
*/
constructor(string memory name, string memory version) {
bytes32 hashedName = keccak256(bytes(name));
bytes32 hashedVersion = keccak256(bytes(version));
bytes32 typeHash = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
_HASHED_NAME = hashedName;
_HASHED_VERSION = hashedVersion;
_CACHED_CHAIN_ID = block.chainid;
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
_CACHED_THIS = address(this);
_TYPE_HASH = typeHash;
}
/**
* @dev Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
if (address(this) == _CACHED_THIS && block.chainid == _CACHED_CHAIN_ID) {
return _CACHED_DOMAIN_SEPARATOR;
} else {
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
}
}
function _buildDomainSeparator(
bytes32 typeHash,
bytes32 nameHash,
bytes32 versionHash
) private view returns (bytes32) {
return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this)));
}
/**
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
* function returns the hash of the fully encoded EIP712 message for this domain.
*
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
*
* ```solidity
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
* keccak256("Mail(address to,string contents)"),
* mailTo,
* keccak256(bytes(mailContents))
* )));
* address signer = ECDSA.recover(digest, signature);
* ```
*/
function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
}
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.15;
import "openzeppelin/token/ERC1155/IERC1155.sol";
/**
* @author Sam King (samkingstudio.eth) for Fount Gallery
* @title Fount Gallery Card Check
* @notice Utility functions to check ownership of a Fount Gallery Patron Card NFT
*/
contract FountCardCheck {
/// @dev Address of the Fount Gallery Patron Card contract
IERC1155 internal _fountCard;
/// @dev Does not own a Fount Gallery Patron Card
error NotFountCardHolder();
/**
* @dev Does not own enough Fount Gallery Patron Cards
* @param required The minimum amount of cards that need to be owned
* @param owned The actualy amount of cards owned
*/
error DoesNotHoldEnoughFountCards(uint256 required, uint256 owned);
/**
* @dev Init with the Fount Gallery Patron Card contract address
* @param fountCard The Fount Gallery Patron Card contract address
*/
constructor(address fountCard) {
_fountCard = IERC1155(fountCard);
}
/**
* @dev Modifier that only allows the caller to do something if they hold
* a Fount Gallery Patron Card
*/
modifier onlyWhenFountCardHolder() {
if (_getFountCardBalance(msg.sender) < 1) revert NotFountCardHolder();
_;
}
/**
* @dev Modifier that only allows the caller to do something if they hold
* at least a specific amount Fount Gallery Patron Cards
* @param minAmount The minimum amount of cards that need to be owned
*/
modifier onlyWhenHoldingMinFountCards(uint256 minAmount) {
uint256 balance = _getFountCardBalance(msg.sender);
if (minAmount > balance) revert DoesNotHoldEnoughFountCards(minAmount, balance);
_;
}
/**
* @dev Get the number of Fount Gallery Patron Cards an address owns
* @param owner The owner address to query
* @return balance The balance of the owner
*/
function _getFountCardBalance(address owner) internal view returns (uint256 balance) {
balance = _fountCard.balanceOf(owner, 1);
}
/**
* @dev Check if an address holds at least one Fount Gallery Patron Card
* @param owner The owner address to query
* @return isHolder If the owner holds at least one card
*/
function _isFountCardHolder(address owner) internal view returns (bool isHolder) {
isHolder = _getFountCardBalance(owner) > 0;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC1155/IERC1155.sol)
pragma solidity ^0.8.0;
import "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC1155 compliant contract, as defined in the
* https://eips.ethereum.org/EIPS/eip-1155[EIP].
*
* _Available since v3.1._
*/
interface IERC1155 is IERC165 {
/**
* @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`.
*/
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
/**
* @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all
* transfers.
*/
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);
/**
* @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
* `approved`.
*/
event ApprovalForAll(address indexed account, address indexed operator, bool approved);
/**
* @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI.
*
* If an {URI} event was emitted for `id`, the standard
* https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value
* returned by {IERC1155MetadataURI-uri}.
*/
event URI(string value, uint256 indexed id);
/**
* @dev Returns the amount of tokens of token type `id` owned by `account`.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function balanceOf(address account, uint256 id) external view returns (uint256);
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}.
*
* Requirements:
*
* - `accounts` and `ids` must have the same length.
*/
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
external
view
returns (uint256[] memory);
/**
* @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`,
*
* Emits an {ApprovalForAll} event.
*
* Requirements:
*
* - `operator` cannot be the caller.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns true if `operator` is approved to transfer ``account``'s tokens.
*
* See {setApprovalForAll}.
*/
function isApprovedForAll(address account, address operator) external view returns (bool);
/**
* @dev Transfers `amount` tokens of token type `id` from `from` to `to`.
*
* Emits a {TransferSingle} event.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}.
* - `from` must have a balance of tokens of type `id` of at least `amount`.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
* acceptance magic value.
*/
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes calldata data
) external;
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}.
*
* Emits a {TransferBatch} event.
*
* Requirements:
*
* - `ids` and `amounts` must have the same length.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
* acceptance magic value.
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata amounts,
bytes calldata data
) external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.0;
import "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the caller.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.17;
interface IPayments {
function releaseAllETH() external;
function releaseAllToken(address tokenAddress) external;
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.17;
interface IRedNight {
function mintRedNight(address to) external;
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Simple single owner authorization mixin.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Owned.sol)
abstract contract Owned {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event OwnerUpdated(address indexed user, address indexed newOwner);
/*//////////////////////////////////////////////////////////////
OWNERSHIP STORAGE
//////////////////////////////////////////////////////////////*/
address public owner;
modifier onlyOwner() virtual {
require(msg.sender == owner, "UNAUTHORIZED");
_;
}
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(address _owner) {
owner = _owner;
emit OwnerUpdated(address(0), _owner);
}
/*//////////////////////////////////////////////////////////////
OWNERSHIP LOGIC
//////////////////////////////////////////////////////////////*/
function setOwner(address newOwner) public virtual onlyOwner {
owner = newOwner;
emit OwnerUpdated(msg.sender, newOwner);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol)
pragma solidity ^0.8.0;
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20;
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
}
return toHexString(value, length);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
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_SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
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);
}
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.17;
import {Owned} from "solmate/auth/Owned.sol";
import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol";
import {EIP712} from "openzeppelin/utils/cryptography/EIP712.sol";
import {IERC721} from "openzeppelin/token/ERC721/IERC721.sol";
import {IERC1155} from "openzeppelin/token/ERC1155/IERC1155.sol";
import {FountCardCheck} from "fount-contracts/community/FountCardCheck.sol";
import {Withdraw} from "fount-contracts/utils/Withdraw.sol";
import {IPayments} from "./interfaces/IPayments.sol";
import {IRedNight} from "./interfaces/IRedNight.sol";
import {CIRCULAR_TENSION_ID, FALLEN_ANGEL_ID, RED_NIGHT_ID} from "./Constants.sol";
/**
* @author Fount Gallery
* @title Tormius 23 sale contract
* @notice Allows the sale of NFTs from the Tormius 23 collection
*/
contract TormiusSale is Owned, FountCardCheck, Withdraw, EIP712 {
/* ------------------------------------------------------------------------
S T O R A G E
------------------------------------------------------------------------ */
/// @notice tormius.eth
// address public artist = 0xFF559FC20D9B6d78E6E570D234B69c18142BB65e;
address public artist = 0xeb9500b009BB53C8b28a46d8f591d620F184f53b;
/// @notice Address where payments should be sent
address public payments;
/// @notice Sale start time: Thu Mar 23 2023 18:00:00 UTC
uint256 public saleStart = 1679594400;
/// @notice Whether the sale is paused
bool public salePaused;
/// @notice Struct for token sale data
struct SaleData {
uint96 price;
uint8 available;
uint32 availableUntil;
address tokenAddress;
}
/// @notice Mapping of token IDs to their respective token addresses
mapping(uint256 => SaleData) public saleData;
/// @notice EIP-712 signing domain
string public constant SIGNING_DOMAIN = "TormiusSale";
/// @notice EIP-712 signature version
string public constant SIGNATURE_VERSION = "1";
/// @notice EIP-712 signed data type hash for collecting with an off-chain signature
bytes32 public constant COLLECT_SIGNATURE_HASH =
keccak256("CollectSignatureData(uint256 id,address to,uint256 nonce)");
/// @dev EIP-712 signed data struct for collecting with an off-chain signature
struct CollectSignatureData {
uint256 id;
address to;
uint256 nonce;
bytes signature;
}
/// @notice Approved signer public addresses
mapping(address => bool) public approvedSigners;
/// @notice Nonce management to avoid signature replay attacks
mapping(address => uint256) public nonces;
/* ------------------------------------------------------------------------
E R R O R S
------------------------------------------------------------------------ */
error SalePausedOrNotStarted();
error InvalidTokenId();
error SoldOut();
error NoLongerCollectable();
error IncorrectPaymentAmount();
error InvalidSignature();
error AlreadyCollected();
/* ------------------------------------------------------------------------
E V E N T S
------------------------------------------------------------------------ */
event TokenListed(uint256 indexed id);
event Collected(uint256 indexed id);
/* ------------------------------------------------------------------------
I N I T
------------------------------------------------------------------------ */
/**
* @param owner_ The owner of the contract
* @param payments_ The address where payments should be sent
* @param fountCard_ The address of the Fount Card contract
*/
constructor(
address owner_,
address payments_,
address fountCard_
) Owned(owner_) FountCardCheck(fountCard_) EIP712(SIGNING_DOMAIN, SIGNATURE_VERSION) {
payments = payments_;
}
/**
* @notice Maps token IDs to their respective token addresses
* @param circularTension_ The address of the Circular Tension NFT
* @param fallenAngel_ The address of the Fallen Angel NFT
* @param redNight_ The address of the Red Night NFT
*/
function mapTokenIdsToAddresses(
address circularTension_,
address fallenAngel_,
address redNight_
) external onlyOwner {
saleData[1] = SaleData({
price: 3.5 ether,
available: 1,
availableUntil: 0,
tokenAddress: circularTension_
});
saleData[2] = SaleData({
price: 0.23 ether,
available: 22, // One is reserved for the artist to airdrop
availableUntil: 0,
tokenAddress: fallenAngel_
});
saleData[3] = SaleData({
price: 0,
available: 0,
availableUntil: 1680821940,
tokenAddress: redNight_
});
emit TokenListed(1);
emit TokenListed(2);
emit TokenListed(3);
}
/* ------------------------------------------------------------------------
P U R C H A S I N G
------------------------------------------------------------------------ */
/**
* @notice
* Purchase either the Circular Tension or a Fallen Angel NFT
*
* @dev
* Reverts if:
* - The token ID is invalid
* - The sale is paused or has not started
* - The token is sold out
* - The token is no longer collectable
* - The payment amount is incorrect
*
* @param id The token ID to purchase
*/
function purchase(uint256 id) external payable {
// Check the token ID is valid
if (id == 0 || id > 2) revert InvalidTokenId();
// Check global sale state
if (block.timestamp < saleStart || salePaused) revert SalePausedOrNotStarted();
// Check conditions for the given token ID
SaleData storage data = saleData[id];
if (data.available == 0) revert SoldOut();
if (data.availableUntil > 0 && block.timestamp > data.availableUntil) {
revert NoLongerCollectable();
}
if (msg.value != data.price) revert IncorrectPaymentAmount();
// Decrement the available amount
data.available--;
// Transfer the token from the artist to the caller
if (id == 1) {
IERC721(data.tokenAddress).transferFrom(artist, msg.sender, CIRCULAR_TENSION_ID);
}
if (id == 2) {
IERC1155(data.tokenAddress).safeTransferFrom(
artist,
msg.sender,
FALLEN_ANGEL_ID,
1,
""
);
}
emit Collected(id);
}
/**
* @notice
* Collect an edition of the Red Night NFT with a mint signature
*
* @dev
* Reverts if:
* - The sale is paused or has not started
* - The token is no longer collectable
* - The price is not free and the payment amount is incorrect
* - The provided signature is invalid
* - The caller already holds a Red Night edition
*/
function collectRedNight(bytes calldata signature) external payable {
// Red Night requires a Fount Card or a valid signature
// If a signature was provided, then it must be valid
if (!_verifyCollectSignature(3, msg.sender, signature)) revert InvalidSignature();
_collectRedNight();
}
/**
* @notice
* Collect an edition of the Red Night NFT with a Fount Patron card
*
* @dev
* Reverts if:
* - The sale is paused or has not started
* - The token is no longer collectable
* - The price is not free and the payment amount is incorrect
* - The caller does not hold a Fount Card
* - The caller already holds a Red Night edition
*/
function collectRedNight() external payable onlyWhenFountCardHolder {
_collectRedNight();
}
/**
* @dev
* Internal function to collect the Red Night NFT. This checks all the shared
* conditions and then mints the token to the caller.
*/
function _collectRedNight() internal {
// Check global sale state
if (block.timestamp < saleStart || salePaused) revert SalePausedOrNotStarted();
// Check if the Red Night is still collectable
SaleData memory data = saleData[3];
if (block.timestamp > data.availableUntil) revert NoLongerCollectable();
if (data.price > 0 && msg.value != data.price) revert IncorrectPaymentAmount();
if (IERC1155(data.tokenAddress).balanceOf(msg.sender, 3) > 0) revert AlreadyCollected();
// Mint the token to the caller
IRedNight(data.tokenAddress).mintRedNight(msg.sender);
emit Collected(3);
}
/* ------------------------------------------------------------------------
S I G N A T U R E V E R I F I C A T I O N
------------------------------------------------------------------------ */
/**
* @notice Internal function to verify an EIP-712 collecting signature
* @param id The token id
* @param to The account that has approval to collect
* @param signature The EIP-712 signature
* @return bool If the signature is verified or not
*/
function _verifyCollectSignature(
uint256 id,
address to,
bytes calldata signature
) internal returns (bool) {
CollectSignatureData memory data = CollectSignatureData({
id: id,
to: to,
nonce: nonces[to],
signature: signature
});
// Hash the data for verification
bytes32 digest = _hashTypedDataV4(
keccak256(abi.encode(COLLECT_SIGNATURE_HASH, data.id, data.to, nonces[data.to]++))
);
// Verify signature is ok
address addr = ECDSA.recover(digest, data.signature);
return approvedSigners[addr] && addr != address(0);
}
/* ------------------------------------------------------------------------
A D M I N
------------------------------------------------------------------------ */
/**
* @notice Admin function to set the sale start time
* @param startTime The new start time
*/
function setSaleStart(uint256 startTime) external onlyOwner {
saleStart = startTime;
}
/**
* @notice Admin function to pause or unpause the sale
* @param paused If the sale is paused
*/
function setSalePaused(bool paused) external onlyOwner {
salePaused = paused;
}
/**
* @notice Admin function to set an EIP-712 signer address
* @param signer The address of the new signer
* @param approved If the signer is approved
*/
function setSigner(address signer, bool approved) external onlyOwner {
approvedSigners[signer] = approved;
}
/**
* @notice Admin function to update the sale price of a token
* @param id The token ID to update
* @param price The new price
*/
function updatePrice(uint256 id, uint96 price) external onlyOwner {
saleData[id].price = price;
}
/**
* @notice Admin function to update when a token should be available until
* @param id The token ID to update
* @param availableUntil The new available date
*/
function updateAvailableUntil(uint256 id, uint32 availableUntil) external onlyOwner {
saleData[id].availableUntil = availableUntil;
}
/**
* @notice Admin function to update the token address if it changes
* @param id The token ID to update
* @param tokenAddress The new token address
*/
function updateTokenAddress(uint256 id, address tokenAddress) external onlyOwner {
saleData[id].tokenAddress = tokenAddress;
}
/* ------------------------------------------------------------------------
W I T H D R A W
------------------------------------------------------------------------ */
/**
* @notice Withdraw ETH from this contract to the payments address
* @dev Withdraws to the `payments` address. Reverts if the payments address is set to zero.
*/
function withdrawETH() public {
_withdrawETH(payments);
}
/**
* @notice Withdraw ETH from this contract and release from payments contract
* @dev Withdraws to the `payments` address, then calls `releaseAllETH` as a splitter.
* Reverts if the payments address is set to zero.
*/
function withdrawAndReleaseAllETH() public {
_withdrawETH(payments);
IPayments(payments).releaseAllETH();
}
/**
* @notice Withdraw ERC-20 tokens from this contract
* @param to The address to send the ERC-20 tokens to
*/
function withdrawTokens(address tokenAddress, address to) public onlyOwner {
_withdrawToken(tokenAddress, to);
}
}
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.15;
import "openzeppelin/token/ERC20/IERC20.sol";
import "openzeppelin/token/ERC721/IERC721.sol";
import "openzeppelin/token/ERC1155/IERC1155.sol";
/**
* @author Sam King (samkingstudio.eth) for Fount Gallery
* @title Withdraw ETH and tokens module
* @notice Allows the withdrawal of ETH, ERC20, ERC721, an ERC1155 tokens
*/
abstract contract Withdraw {
/* ------------------------------------------------------------------------
E R R O R S
------------------------------------------------------------------------ */
error CannotWithdrawToZeroAddress();
error WithdrawFailed();
error BalanceTooLow();
error ZeroBalance();
/* ------------------------------------------------------------------------
W I T H D R A W
------------------------------------------------------------------------ */
function _withdrawETH(address to) internal {
// Prevent withdrawing to the zero address
if (to == address(0)) revert CannotWithdrawToZeroAddress();
// Check there is eth to withdraw
uint256 balance = address(this).balance;
if (balance == 0) revert ZeroBalance();
// Transfer funds
(bool success, ) = payable(to).call{value: balance}("");
if (!success) revert WithdrawFailed();
}
function _withdrawToken(address tokenAddress, address to) internal {
// Prevent withdrawing to the zero address
if (to == address(0)) revert CannotWithdrawToZeroAddress();
// Check there are tokens to withdraw
uint256 balance = IERC20(tokenAddress).balanceOf(address(this));
if (balance == 0) revert ZeroBalance();
// Transfer tokens
bool success = IERC20(tokenAddress).transfer(to, balance);
if (!success) revert WithdrawFailed();
}
function _withdrawERC721Token(
address tokenAddress,
uint256 id,
address to
) internal {
// Prevent withdrawing to the zero address
if (to == address(0)) revert CannotWithdrawToZeroAddress();
// Check the NFT is in this contract
address owner = IERC721(tokenAddress).ownerOf(id);
if (owner != address(this)) revert ZeroBalance();
// Transfer NFT
IERC721(tokenAddress).transferFrom(address(this), to, id);
}
function _withdrawERC1155Token(
address tokenAddress,
uint256 id,
uint256 amount,
address to
) internal {
// Prevent withdrawing to the zero address
if (to == address(0)) revert CannotWithdrawToZeroAddress();
// Check the tokens are owned by this contract, and there's at least `amount`
uint256 balance = IERC1155(tokenAddress).balanceOf(address(this), id);
if (balance == 0) revert ZeroBalance();
if (amount > balance) revert BalanceTooLow();
// Transfer tokens
IERC1155(tokenAddress).safeTransferFrom(address(this), to, id, amount, "");
}
}
{
"compilationTarget": {
"packages/contracts/src/TormiusSale.sol": "TormiusSale"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "none"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": [
":closedsea/=packages/contracts/lib/closedsea/src/",
":ds-test/=packages/contracts/lib/ds-test/src/",
":erc4626-tests/=packages/contracts/lib/closedsea/lib/openzeppelin-contracts/lib/erc4626-tests/",
":erc721a-upgradeable/=packages/contracts/lib/closedsea/lib/erc721a-upgradeable/contracts/",
":erc721a/=packages/contracts/lib/closedsea/lib/erc721a/contracts/",
":ethier/=packages/contracts/lib/fount-contracts/lib/ethier/",
":forge-std/=packages/contracts/lib/forge-std/src/",
":fount-23/=packages/contracts/src/",
":fount-contracts/=packages/contracts/lib/fount-contracts/src/",
":openzeppelin-contracts-upgradeable/=packages/contracts/lib/closedsea/lib/openzeppelin-contracts-upgradeable/",
":openzeppelin-contracts/=packages/contracts/lib/openzeppelin-contracts/",
":openzeppelin/=packages/contracts/lib/openzeppelin-contracts/contracts/",
":operator-filter-registry/=packages/contracts/lib/closedsea/lib/operator-filter-registry/",
":solmate/=packages/contracts/lib/solmate/src/"
]
}
[{"inputs":[{"internalType":"address","name":"owner_","type":"address"},{"internalType":"address","name":"payments_","type":"address"},{"internalType":"address","name":"fountCard_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AlreadyCollected","type":"error"},{"inputs":[],"name":"BalanceTooLow","type":"error"},{"inputs":[],"name":"CannotWithdrawToZeroAddress","type":"error"},{"inputs":[{"internalType":"uint256","name":"required","type":"uint256"},{"internalType":"uint256","name":"owned","type":"uint256"}],"name":"DoesNotHoldEnoughFountCards","type":"error"},{"inputs":[],"name":"IncorrectPaymentAmount","type":"error"},{"inputs":[],"name":"InvalidSignature","type":"error"},{"inputs":[],"name":"InvalidTokenId","type":"error"},{"inputs":[],"name":"NoLongerCollectable","type":"error"},{"inputs":[],"name":"NotFountCardHolder","type":"error"},{"inputs":[],"name":"SalePausedOrNotStarted","type":"error"},{"inputs":[],"name":"SoldOut","type":"error"},{"inputs":[],"name":"WithdrawFailed","type":"error"},{"inputs":[],"name":"ZeroBalance","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"}],"name":"Collected","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"}],"name":"TokenListed","type":"event"},{"inputs":[],"name":"COLLECT_SIGNATURE_HASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SIGNATURE_VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SIGNING_DOMAIN","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"approvedSigners","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"artist","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"collectRedNight","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"collectRedNight","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"circularTension_","type":"address"},{"internalType":"address","name":"fallenAngel_","type":"address"},{"internalType":"address","name":"redNight_","type":"address"}],"name":"mapTokenIdsToAddresses","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"payments","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"purchase","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"saleData","outputs":[{"internalType":"uint96","name":"price","type":"uint96"},{"internalType":"uint8","name":"available","type":"uint8"},{"internalType":"uint32","name":"availableUntil","type":"uint32"},{"internalType":"address","name":"tokenAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"salePaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"saleStart","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"paused","type":"bool"}],"name":"setSalePaused","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"startTime","type":"uint256"}],"name":"setSaleStart","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"signer","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setSigner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint32","name":"availableUntil","type":"uint32"}],"name":"updateAvailableUntil","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint96","name":"price","type":"uint96"}],"name":"updatePrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"tokenAddress","type":"address"}],"name":"updateTokenAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawAndReleaseAllETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"address","name":"to","type":"address"}],"name":"withdrawTokens","outputs":[],"stateMutability":"nonpayable","type":"function"}]