// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.4;
/// ============ Libraries ============
/// @notice OpenZeppelin: MerkleProof
/// @dev The hashing algorithm should be keccak256 and pair sorting should be enabled.
library MerkleProof {
/// @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree defined by `root`.
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
/// @dev Returns the rebuilt hash obtained by traversing a Merklee tree up from `leaf` using `proof`.
function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
if (computedHash <= proofElement) {
// Hash(current computed hash + current element of the proof)
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
// Hash(current element of the proof + current computed hash)
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}
}
return computedHash;
}
}
/// ============ Interfaces ============
interface IERC721 {
/// @notice ERC721 transfer from (from) to (to)
function transferFrom(address from, address to, uint256 tokenId) external;
}
contract SantaSwapExchange {
/// ============ Structs ============
/// @notice Individual ERC721 NFT details
struct SantaNFT {
/// @notice NFT contract
address nftContract;
/// @notice NFT tokenId
uint256 tokenId;
}
/// ============ Immutable storage ============
/// @notice Timestamp when reclaim is enabled
uint256 immutable public RECLAIM_OPEN;
/// ============ Mutable storage ============
/// @notice Contract owner
address public owner;
/// @notice Merkle root of santa => giftee
/// @dev keccak256(giftee, santa, santaNFTIndex)
bytes32 public merkle;
/// @notice Helper to iterate nfts for front-en
/// @dev Keeps santaNFTIndex count
mapping(address => uint256) public nftCount;
/// @notice Address to deposited NFTs
mapping(address => SantaNFT[]) public nfts;
// ============ Errors ============
/// @notice Thrown if caller is not owner
error NotOwner();
/// @notice Thrown if cannot claim NFT
error NotClaimable();
/// ============ Constructor ============
constructor() {
// Update contract owner
owner = msg.sender;
// Allow reclaiming 7 days after depositing
RECLAIM_OPEN = block.timestamp + 604_800;
}
/// @notice Computes leaf of merkle tree by hashing params
/// @param giftee address receiving NFT gift
/// @param santa address giving NFT gift
/// @param santaIndex index of gift (for multiple tickets)
/// @return hash of leaf
function _leaf(
address giftee,
address santa,
uint256 santaIndex
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(giftee, santa, santaIndex));
}
/// @notice Verifies that leaf matches provided proof
/// @param leaf (computed in contract)
/// @param proof (provided externally)
/// @return whether santa => giftee matches up
function _verify(
bytes32 leaf,
bytes32[] calldata proof
) internal view returns (bool) {
return MerkleProof.verify(proof, merkle, leaf);
}
/// @notice Allows santas to deposit NFTs to contract
/// @param nftContract of NFT being deposited
/// @param tokenId being deposited
function santaDepositNFT(address nftContract, uint256 tokenId) external {
// Transfer NFT to contract
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
// Mark as deposited by santa
nftCount[msg.sender]++;
nfts[msg.sender].push(SantaNFT(nftContract, tokenId));
}
/// @notice Allows giftees to claim NFTs
/// @param santa who is gifting them
/// @param santaIndex index of gift in santa's array
/// @param proof merkle proof of claim
function gifteeClaimNFT(
address santa,
uint256 santaIndex,
bytes32[] calldata proof
) external {
// Require merkle to be set
if (merkle == 0) revert NotClaimable();
// Require giftee to be claiming correct santa NFT
if (!_verify(_leaf(msg.sender, santa, santaIndex), proof)) revert NotClaimable();
// Collect NFT
SantaNFT memory nft = nfts[santa][santaIndex];
// Transfer NFT
IERC721(nft.nftContract).transferFrom(address(this), msg.sender, nft.tokenId);
}
/// @notice ALlows santas to reclaim their NFTs given reclaim period is active
/// @param index of gift in santa's array (fka: santaIndex)
function santaReclaimNFT(uint256 index) external {
// Require reclaim period to be active
if (block.timestamp < RECLAIM_OPEN) revert NotClaimable();
// Require provided index to be in range of owned NFTs
if (index + 1 > nfts[msg.sender].length) revert NotClaimable();
// Collect NFT
SantaNFT memory nft = nfts[msg.sender][index];
// Transfer unclaimed NFT
IERC721(nft.nftContract).transferFrom(address(this), msg.sender, nft.tokenId);
}
/// @notice Allows contract owner to withdraw any single NFT
/// @notice nftContract of NFT to withdraw
/// @notice tokenId to withdraw
/// @notice recipient of withdrawn NFT
function adminWithdrawNFT(
address nftContract,
uint256 tokenId,
address recipient
) external {
// Require caller to be owner
if (msg.sender != owner) revert NotOwner();
IERC721(nftContract).transferFrom(
// From this contract
address(this),
// To provided recipient
recipient,
// Transfer specified NFT tokenId
tokenId
);
}
/// @notice Allows contract owner to withdraw bulk NFTs
/// @notice contracts of NFTs to withdraw
/// @notice tokenIds to withdraw
/// @notice recipients of withdrawn NFT
/// @dev Does not check for array length equality
function adminWithdrawNFTBulk(
address[] calldata contracts,
uint256[] calldata tokenIds,
address[] calldata recipients
) external {
// Require caller to be owner
if (msg.sender != owner) revert NotOwner();
// For each provided contract
for (uint256 i = 0; i < contracts.length; i++) {
IERC721(contracts[i]).transferFrom(
// From contract
address(this),
// To provided recipient
recipients[i],
// Transfer specified NFT tokenId
tokenIds[i]
);
}
}
/// @notice Allows owner to update merkle
/// @param merkleRoot to update
function adminUpdateMerkle(bytes32 merkleRoot) external {
// Require caller to be owner
if (msg.sender != owner) revert NotOwner();
// Update merkle root
merkle = merkleRoot;
}
/// @notice Allows owner to update new owner
/// @param newOwner to update
function adminUpdateOwner(address newOwner) external {
// Require caller to be owner
if (msg.sender != owner) revert NotOwner();
// Update to new owner
owner = newOwner;
}
}
{
"compilationTarget": {
"contracts/SantaSwapExchange.sol": "SantaSwapExchange"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"NotClaimable","type":"error"},{"inputs":[],"name":"NotOwner","type":"error"},{"inputs":[],"name":"RECLAIM_OPEN","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"merkleRoot","type":"bytes32"}],"name":"adminUpdateMerkle","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"adminUpdateOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"adminWithdrawNFT","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"contracts","type":"address[]"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"},{"internalType":"address[]","name":"recipients","type":"address[]"}],"name":"adminWithdrawNFTBulk","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"santa","type":"address"},{"internalType":"uint256","name":"santaIndex","type":"uint256"},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"}],"name":"gifteeClaimNFT","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"merkle","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nftCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"nfts","outputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"santaDepositNFT","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"santaReclaimNFT","outputs":[],"stateMutability":"nonpayable","type":"function"}]