Description:
Non-Fungible Token (NFT) contract following ERC721 standard.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"contracts/MetaWalletNFT.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title Omni NFT Wallet
*/
import "contracts/IERC721.sol";
import "contracts/utils/cryptography/ECDSA.sol";
import "contracts/RlpEncode.sol";
contract MetaWalletNFT {
uint64 public constant MAX_UINT64 = 2 ** 64 - 1;
uint128 public constant NONCE_TS_SHIFT = 1000000000000;
address public owner;
address public verifyAddress;
uint64 public chainId;
uint128 public nonceMax;
uint256 public fees;
mapping(uint128 => bytes32) public deposits;
mapping(uint128 => bool) public usedNonces;
event NftDeposit(uint128 nonce, uint256 tokenId, bytes contract_id, bytes receiver);
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can call this function.");
_;
}
/**
* @notice Withdraw NFT from the contract to the owner's address.
* @dev Only callable by the owner.
* @param nftAddress The address of the NFT contract to withdraw from.
* @param tokenId The ID of the NFT to withdraw.
*/
function withdrawNFT(address nftAddress, uint256 tokenId)
public
onlyOwner
{
IERC721(nftAddress).transferFrom(address(this), owner, tokenId);
}
/**
* @notice Withdraw Ether from the contract to the owner's address.
* @dev Only callable by the owner.
* @param amount The amount of Ether to withdraw (in wei).
*/
function withdrawEth(uint256 amount) public onlyOwner {
(bool success,) = payable(owner).call{value: amount}("");
require(success, "Transfer failed.");
}
/**
* @notice Withdraw accumulated fees from the contract to the owner's address.
* @dev Only callable by the owner. Resets the fees to zero after withdrawal.
*/
function withdrawFees() public onlyOwner {
(bool success,) = payable(owner).call{value: fees}("");
require(success, "Transfer failed.");
fees = 0;
}
/**
* @notice Change ownership of the contract to a new address.
* @dev Only callable by the current owner.
* @param newOwner The address of the new owner.
*/
function changeOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
/**
* @notice Fallback function to receive Ether.
*/
receive() external payable {}
/**
* @notice Initializes the MetaWalletNFT contract.
* @param _verifyAddress The address used to verify signatures.
* @param _chainId The chain ID of the blockchain network.
*/
constructor(
address _verifyAddress,
uint64 _chainId
) payable {
owner = msg.sender;
verifyAddress = _verifyAddress;
chainId = _chainId;
fees = 0;
}
/**
* @notice Change the address used for signature verification.
* @dev Only callable by the owner.
* @param _verifyAddress The new address to use for verifying signatures.
*/
function changeVerifyAddress(address _verifyAddress) public onlyOwner {
verifyAddress = _verifyAddress;
}
/**
* @notice Execute a withdrawal from the contract to the specified receiver.
* @param nonce The unique nonce associated with the withdrawal.
* @param contract_id The address of the NFT contract.
* @param receiver_id The address of the receiver.
* @param tokenId The ID of the NFT to withdraw.
* @param signature The signature proving authorization for the withdrawal.
*
* @dev Verifies the provided signature and ensures that the nonce is valid.
* Increments the fees by the amount of Ether sent with the transaction.
*/
function withdraw(
uint128 nonce,
address contract_id,
address receiver_id,
uint256 tokenId,
bytes memory signature
) public payable {
uint128 nonce_ts = nonce / NONCE_TS_SHIFT;
require(!usedNonces[nonce], "Nonce already used");
require(
verify(
nonce,
abi.encodePacked(contract_id),
abi.encodePacked(receiver_id),
tokenId,
signature
),
"Invalid signature"
);
usedNonces[nonce] = true;
IERC721(contract_id).transferFrom(address(this), receiver_id, tokenId);
fees += msg.value;
}
/**
* @notice Deposit NFT into the contract on behalf of a receiver.
* @param receiver The receiver's identifier (in bytes).
* @param contract_id The address of the NFT contract.
* @param tokenId The ID of the NFT to deposit.
*
* @dev Transfers NFT from the sender to the contract and records the deposit.
* Increments the fees by the amount of Ether sent with the transaction.
*/
function deposit(
bytes memory receiver,
address contract_id,
uint256 tokenId
) public payable {
IERC721(contract_id).transferFrom(
msg.sender,
address(this),
tokenId
);
uint128 nonce = uint128(block.timestamp) * NONCE_TS_SHIFT + nonceMax;
emit NftDeposit(nonce, tokenId, abi.encodePacked(contract_id), receiver);
bytes32 origMessageHash = getMessageHash(
nonce,
abi.encodePacked(contract_id),
receiver,
tokenId
);
deposits[nonce] = origMessageHash;
nonceMax += 1;
fees += msg.value;
}
/**
* @notice Deposit multiple NFTs into the contract on behalf of a receiver.
* @param receiver The receiver's identifier (in bytes).
* @param contract_id The address of the NFT contract.
* @param tokenIds An array of NFT IDs to deposit.
* @dev Iterates through the array of token IDs and calls the deposit function for each.
* Increments the fees by the total amount of Ether sent with the transaction.
*/
function multi_deposit(
bytes memory receiver,
address contract_id,
uint256[] memory tokenIds
) public payable {
require(tokenIds.length > 0, "No token IDs provided");
for (uint256 i = 0; i < tokenIds.length; i++) {
deposit(
receiver,
contract_id,
tokenIds[i]
);
}
}
/**
* @notice Verifies that the message hash can be signed by HOT Validators.
* @param msg_hash The hash of the message to verify.
* @param userPayload: (uint128 nonce, uint8 type_) encoded in ABI. Type_ = 0 for deposit, 1 for refund
* @return True if the message hash can be signed; false otherwise.
*
* @dev Used by HOT Validators to verify messages before signing.
*/
function hot_verify(
bytes32 msg_hash,
bytes memory _walletId,
bytes memory userPayload,
bytes memory _metadata
) public view returns (bool) {
(uint128 nonce) = abi.decode(
userPayload,
(uint128)
);
require(deposits[nonce] == msg_hash, "Deposit hash mismatch");
return true;
}
/**
* @notice Verifies the signature of a message.
* @param nonce The unique nonce associated with the message.
* @param contract_id The encoded contract address.
* @param receiver_id The encoded receiver address.
* @param tokenId The ID of the NFT involved in the transaction.
* @param signature The signature to verify.
* @return True if the signature is valid; false otherwise.
*
* @dev Uses ECDSA to recover the signer's address and compares it with the verifyAddress.
*/
function verify(
uint128 nonce,
bytes memory contract_id,
bytes memory receiver_id,
uint256 tokenId,
bytes memory signature
) internal view returns (bool) {
bytes32 messageHash = getMessageHash(
nonce,
contract_id,
receiver_id,
tokenId
);
return ECDSA.recover(messageHash, signature) == verifyAddress;
}
/**
* @notice Generates the SHA-256 hash of the raw message data.
* @param nonce The unique nonce associated with the message.
* @param contract_id The encoded contract address.
* @param receiver_id The encoded receiver address.
* @param tokenId The ID of the NFT involved in the transaction.
* @return The SHA-256 hash of the message data.
*/
function getMessageHash(
uint128 nonce,
bytes memory contract_id,
bytes memory receiver_id,
uint256 tokenId
) public view returns (bytes32) {
return sha256(getMessageRaw(nonce, contract_id, receiver_id, tokenId));
}
/**
* @notice Constructs the raw data for a message.
* @param nonce The unique nonce associated with the message.
* @param contract_id The encoded contract address.
* @param receiver_id The encoded receiver address.
* @param tokenId The ID of the NFT involved in the transaction.
* @return The RLP-encoded raw message data.
*/
function getMessageRaw(
uint128 nonce,
bytes memory contract_id,
bytes memory receiver_id,
uint256 tokenId
) public view returns (bytes memory) {
bytes[] memory rlpList = new bytes[](5);
rlpList[0] = RLPEncode.encodeUint128(nonce, 16);
rlpList[1] = RLPEncode.encodeUint64(chainId, 8);
rlpList[2] = RLPEncode.encodeBytes(contract_id);
rlpList[3] = RLPEncode.encodeBytes(receiver_id);
rlpList[4] = RLPEncode.encodeBytes(abi.encodePacked(tokenId));
return RLPEncode.encodeList(rlpList);
}
} "
},
"contracts/IERC721.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title IERC721
* @dev Interface for ERC721 tokens
*/
interface IERC721 {
/**
* @dev Transfers the ownership of a given token ID to another address
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
*/
function transferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Gets the approved address to take ownership of a given token ID
* @param tokenId uint256 ID of the token to query the approval of
* @return address currently approved for the given token ID
*/
function getApproved(uint256 tokenId) external view returns (address);
/**
* @dev Approves another address to transfer the given token ID
* @param to address to be approved for the given token ID
* @param tokenId uint256 ID of the token to be approved
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Returns the number of tokens in the given owner's account
* @param owner address of the owner to query
* @return uint256 representing the amount of tokens owned by the passed address
*/
function balanceOf(address owner) external view returns (uint256);
/**
* @dev Returns the owner of the given token ID
* @param tokenId uint256 ID of the token to query the owner of
* @return address currently marked as the owner of the given token ID
*/
function ownerOf(uint256 tokenId) external view returns (address);
/**
* @dev Safely transfers the ownership of a given token ID to another address
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
*/
function safeTransferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Safely transfers the ownership of a given token ID to another address
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
* @param data bytes data to send along with a safe transfer check
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
/**
* @dev Returns the total amount of tokens stored by the contract
* @return uint256 representing the total amount of tokens
*/
function totalSupply() external view returns (uint256);
} "
},
"contracts/utils/cryptography/ECDSA.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @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
}
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");
} else if (error == RecoverError.InvalidSignatureV) {
revert("ECDSA: invalid signature 'v' 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) {
// Check the signature length
// - case 65: r,s,v signature (standard)
// - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._
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 if (signature.length == 64) {
bytes32 r;
bytes32 vs;
// 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))
vs := mload(add(signature, 0x40))
}
return tryRecover(hash, r, vs);
} 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 (v != 27 && v != 28) {
return (address(0), RecoverError.InvalidSignatureV);
}
// 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:\
32", 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:\
", 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));
}
}
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
/**
* @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);
}
} "
},
"contracts/RlpEncode.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
library RLPEncode {
function encodeUint(uint256 _uint, uint8 byteLength) internal pure returns (bytes memory) {
bytes memory b = new bytes(byteLength);
for (uint8 i = 0; i < byteLength; i++) {
b[byteLength - 1 - i] = bytes1(uint8(_uint >> (8 * i)));
}
return encodeBytes(b);
}
function encodeUint128(uint128 _uint, uint8 byteLength) internal pure returns (bytes memory) {
return encodeUint(uint256(_uint), byteLength);
}
function encodeUint64(uint64 _uint, uint8 byteLength) internal pure returns (bytes memory) {
return encodeUint(uint256(_uint), byteLength);
}
function encodeBytes(bytes memory input) internal pure returns (bytes memory) {
uint256 length = input.length;
if (length == 1 && uint8(input[0]) <= 0x7f) {
// Если длина 1 и значение <= 0x7f, возвращаем как есть
return input;
} else if (length <= 55) {
// Если длина <= 55, используем короткий формат
bytes memory output = new bytes(1 + length);
output[0] = bytes1(uint8(0x80 + length));
for (uint256 i = 0; i < length; i++) {
output[i + 1] = input[i];
}
return output;
} else {
// Если длина > 55, используем длинный формат
uint256 tempLength = length;
uint256 byteLength = 0;
while (tempLength != 0) {
byteLength++;
tempLength >>= 8;
}
bytes memory output = new bytes(1 + byteLength + length);
output[0] = bytes1(uint8(0xb7 + byteLength));
// Исправлено: запись байтов длины в правильном порядке
for (uint256 i = 0; i < byteLength; i++) {
output[1 + byteLength - 1 - i] = bytes1(uint8(length >> (8 * i)));
}
// Копирование данных
for (uint256 i = 0; i < length; i++) {
output[1 + byteLength + i] = input[i];
}
return output;
}
}
function encodeList(bytes[] memory _items) internal pure returns (bytes memory) {
bytes memory concatenated;
for (uint256 i = 0; i < _items.length; i++) {
concatenated = concatenate(concatenated, _items[i]);
}
return concatenate(encodeLength(concatenated.length, 192), concatenated);
}
function encodeLength(uint256 _length, uint256 _offset) internal pure returns (bytes memory) {
if (_length < 56) {
return bytes(abi.encodePacked(uint8(_length + _offset)));
} else {
uint256 lenLen;
uint256 i = 1;
while (_length / i != 0) {
lenLen++;
i *= 256;
}
bytes memory b = bytes(abi.encodePacked(uint8(lenLen + _offset + 55)));
for (i = (lenLen - 1) * 8; i > 0; i -= 8) {
b = concatenate(b, bytes(abi.encodePacked(uint8(_length / (2 ** i)))));
}
return concatenate(b, bytes(abi.encodePacked(uint8(_length))));
}
}
function concatenate(bytes memory a, bytes memory b) internal pure returns (bytes memory) {
bytes memory result = new bytes(a.length + b.length);
assembly {
let resultPtr := add(result, 0x20)
let aPtr := add(a, 0x20)
let bPtr := add(b, 0x20)
let aLength := mload(a)
let bLength := mload(b)
// Copy a to result
for { let i := 0 } lt(i, aLength) { i := add(i, 0x20) } {
mstore(add(resultPtr, i), mload(add(aPtr, i)))
}
// Copy b to result
for { let i := 0 } lt(i, bLength) { i := add(i, 0x20) } {
mstore(add(resultPtr, add(aLength, i)), mload(add(bPtr, i)))
}
}
return result;
}
}
"
}
},
"settings": {
"evmVersion": "istanbul",
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
}
}
}}
Submitted on: 2025-10-23 17:08:08
Comments
Log in to comment.
No comments yet.