This smart contract enables users to stake their NFTs for a specified duration and earn rewards. The contract supports multiple NFT collections and staking durations, with administrative controls for managing the staking ecosystem.
- Find the contract deployments in
deployments.json - Indexer is deployed on railway. Indexer can be accessed at: https://abs-indexer-testnet-production.up.railway.app/. Check out the docs here to deploy the indexer: Deploy Ponder
- Find the ABIs in
exports/WitsStaking.json. - Use viem to fetch erc721abi.
- Install Foundry
- Clone this repository
- Install dependencies:
forge install
- Copy the example environment file:
cp .env.example .env- Update the
.envfile with your configuration:
# Required
OWNER=<address> # Address that will be the owner of the contract
RPC_URL=<rpc-url> # RPC URL of the network to deploy to
PRIVATE_KEY=<private-key> # Private key of the deployer wallet
# Optional - Required only for contract verification
ETHERSCAN_API_KEY=<key> # API key for Etherscan verification- Deploy without verification:
forge script script/WitsStaking.s.sol:WitsStakingScript --zksync \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast# Deploy the paymaster
forge script script/WitsPaymaster.s.sol:WitsPaymasterScript --zksync \
--rpc-url $RPC_URL \
--private-key ${PRIVATE_KEY} \
--broadcast- Deploy and verify on Etherscan:
forge script script/WitsStaking.s.sol:WitsStakingScript --zksync \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast \
--verify \
--verifier zksync \
--verifier-url https://api-explorer-verify.testnet.abs.xyz/contract_verification
# Deploy the paymaster
forge script script/WitsPaymaster.s.sol:WitsPaymasterScript --zksync \
--rpc-url $RPC_URL \
--private-key ${PRIVATE_KEY} \
--broadcast \
--verify \
--verifier zksync \
--verifier-url https://api-explorer-verify.testnet.abs.xyz/contract_verification- Deploy and verify on Sourcify:
forge script script/WitsStaking.s.sol:WitsStakingScript \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast \
--verify \
--verifier sourcifyAfter deployment, you can use individual scripts for each admin function. Update your .env file with the required variables for the function you want to execute:
- Add Staking Duration:
# Set DURATION in .env
forge script script/WitsStakingAdminFunctions.s.sol:AddStakingDuration \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast- Remove Staking Duration:
# Set DURATION in .env
forge script script/WitsStakingAdminFunctions.s.sol:RemoveStakingDuration \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast- Add NFT Contract:
# Set NFT_CONTRACT in .env
forge script script/WitsStakingAdminFunctions.s.sol:AddNFTContract \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast- Remove NFT Contract:
# Set NFT_CONTRACT in .env
forge script script/WitsStakingAdminFunctions.s.sol:RemoveNFTContract \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast- Toggle Pause State:
forge script script/WitsStakingAdminFunctions.s.sol:PauseContract \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast- Recover ETH:
# Set RECIPIENT and AMOUNT in .env
forge script script/WitsStakingAdminFunctions.s.sol:RecoverETH \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast- Recover ERC20:
# Set TOKEN_ADDRESS, RECIPIENT, and AMOUNT in .env
forge script script/WitsStakingAdminFunctions.s.sol:RecoverERC20 \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast- Recover ERC721:
# Set TOKEN_ADDRESS, RECIPIENT, and TOKEN_ID in .env
forge script script/WitsStakingAdminFunctions.s.sol:RecoverERC721 \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast# Deposit
forge script script/WitsPaymaster.s.sol:WitsPaymasterAdminScript --zksync \
--sig "deposit()" \
--rpc-url $RPC_URL \
--private-key ${PRIVATE_KEY} \
--broadcast \
-vvvv
# Withdraw
forge script script/WitsPaymaster.s.sol:WitsPaymasterAdminScript --zksync \
--sig "withdraw()" \
--rpc-url $RPC_URL \
--private-key ${PRIVATE_KEY} \
--broadcast \
-vvvv
# Add target contract
forge script script/WitsPaymaster.s.sol:WitsPaymasterAdminScript --zksync \
--sig "addTarget()" \
--rpc-url $RPC_URL \
--private-key ${PRIVATE_KEY} \
--broadcast \
-vvvv
# Remove target contract
forge script script/WitsPaymaster.s.sol:WitsPaymasterAdminScript --zksync \
--sig "removeTarget()" \
--rpc-url $RPC_URL \
--private-key ${PRIVATE_KEY} \
--broadcast \
-vvvvFor initial setup, you can use the bulk script to configure multiple settings at once. Update your .env file with:
STAKING_ADDRESS: The address of your deployed WitsStaking contractNFT_ADDRESSES: Comma-separated list of NFT contract addresses to whitelistSTAKING_DURATIONS: Comma-separated list of staking durations in seconds
Then run:
forge script script/WitsStakingAdmin.s.sol:WitsStakingAdmin \
--rpc-url ${RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcastAlternatively, you can manually call the contract functions using your preferred method (e.g., etherscan, hardhat console, etc.):
- Add allowed staking durations using
addStakingDuration - Whitelist NFT contracts using
whitelistNFTContract - Verify the contract is not paused using
paused
After deploying the WitsStaking contract, you can use the following scripts to perform administrative functions. Each script requires specific environment variables to be set in your .env file.
All admin scripts require these base variables:
STAKING_ADDRESS=<deployed_contract_address>
OWNER=<admin_wallet_address>
RPC_URL=<network_rpc_url>
PRIVATE_KEY=<admin_private_key>
Adds a new valid staking duration to the contract.
Required Environment Variable:
DURATION=<duration_in_seconds>
Example:
# Add 30 days staking duration
forge script script/WitsStakingAdminFunctions.s.sol:AddStakingDuration --rpc-url $RPC_URL --broadcastRemoves a staking duration from the valid durations list.
Required Environment Variable:
DURATION=<duration_in_seconds>
Example:
forge script script/WitsStakingAdminFunctions.s.sol:RemoveStakingDuration --rpc-url $RPC_URL --broadcastWhitelists a new NFT contract for staking.
Required Environment Variable:
NFT_CONTRACT=<nft_contract_address>
Example:
forge script script/WitsStakingAdminFunctions.s.sol:AddNFTContract --rpc-url $RPC_URL --broadcastRemoves an NFT contract from the whitelist.
Required Environment Variable:
NFT_CONTRACT=<nft_contract_address>
Example:
forge script script/WitsStakingAdminFunctions.s.sol:RemoveNFTContract --rpc-url $RPC_URL --broadcastToggles the pause state of the contract. Running it when paused will unpause, and vice versa.
Example:
forge script script/WitsStakingAdminFunctions.s.sol:PauseContract --rpc-url $RPC_URL --broadcastRecovers ETH accidentally sent to the contract.
Required Environment Variables:
RECIPIENT=<recipient_address>
AMOUNT=<amount_in_wei>
Example:
forge script script/WitsStakingAdminFunctions.s.sol:RecoverETH --rpc-url $RPC_URL --broadcastRecovers ERC20 tokens accidentally sent to the contract.
Required Environment Variables:
TOKEN_ADDRESS=<token_contract_address>
RECIPIENT=<recipient_address>
AMOUNT=<amount_in_token_decimals>
Example:
forge script script/WitsStakingAdminFunctions.s.sol:RecoverERC20 --rpc-url $RPC_URL --broadcastRecovers NFTs accidentally sent to the contract.
Required Environment Variables:
TOKEN_ADDRESS=<nft_contract_address>
RECIPIENT=<recipient_address>
TOKEN_ID=<nft_token_id>
Example:
forge script script/WitsStakingAdminFunctions.s.sol:RecoverERC721 --rpc-url $RPC_URL --broadcast- All scripts require the caller to be the contract owner.
- Make sure to verify all environment variables before running scripts.
- For safety, test scripts on a testnet before using on mainnet.
- Keep your private key secure and never commit it to version control.
/// @notice Mapping of NFT contract address to their whitelist status
mapping(address => bool) public whitelistedNFTs;
/// @notice Mapping of available staking durations (in seconds) to their status
mapping(uint256 => bool) public stakingDurations;
/// @notice Mapping of token ID to its stake information for each NFT contract
mapping(address => mapping(uint256 => StakeInfo)) public stakes;
/// @notice Contract pause status
bool public paused;
/// @notice Contract owner/admin address
address public owner;
/// @notice Minimum staking duration
uint256 public constant MIN_STAKE_DURATION = 1 hours;
/// @notice Maximum staking duration
uint256 public constant MAX_STAKE_DURATION = 365 days;struct StakeInfo {
address staker; // Address of the NFT staker
uint256 startTime; // Timestamp when staking started
uint256 endTime; // Timestamp when staking ends
bool isStaked; // Current stake status
uint256 stakeDuration; // Duration selected for staking
}/// @notice Emitted when a new NFT contract is whitelisted
event NFTContractWhitelisted(address indexed nftContract);
/// @notice Emitted when an NFT contract is removed from whitelist
event NFTContractRemoved(address indexed nftContract);
/// @notice Emitted when a new staking duration is added
event StakingDurationAdded(uint256 duration);
/// @notice Emitted when a staking duration is removed
event StakingDurationRemoved(uint256 duration);
/// @notice Emitted when an NFT is staked
event NFTStaked(address indexed nftContract, uint256 indexed tokenId, address indexed staker, uint256 duration);
/// @notice Emitted when an NFT is unstaked
event NFTUnstaked(address indexed nftContract, uint256 indexed tokenId, address indexed staker);
/// @notice Emitted when contract is paused/unpaused
event ContractPauseToggled(bool isPaused);
/// @notice Emitted when stuck tokens are recovered
event TokensRecovered(address indexed token, address indexed recipient, uint256 amount);error InvalidNFTContract();
error NFTNotWhitelisted();
error InvalidStakingDuration();
error ContractPaused();
error NotTokenOwner();
error StakeNotFound();
error StakeStillLocked();
error StakeAlreadyExists();
error ZeroAddress();
error UnauthorizedCaller();
error InvalidTokenAmount();/// @notice Ensures caller is contract owner
modifier onlyOwner();
/// @notice Ensures contract is not paused
modifier whenNotPaused();
/// @notice Ensures NFT contract is whitelisted
modifier onlyWhitelistedNFT(address nftContract);
/// @notice Validates staking duration
modifier validStakingDuration(uint256 duration);function setOwner(address newOwner) external onlyOwnerUpdates contract owner address.
- Requirements:
- Caller must be current owner
- New owner address must not be zero
- Emits
OwnershipTransferredevent
function togglePause() external onlyOwnerToggles contract pause status.
- Requirements:
- Caller must be owner
- Emits
ContractPauseToggledevent
function whitelistNFTContract(address nftContract) external onlyOwnerAdds NFT contract to whitelist.
- Requirements:
- Caller must be owner
- Contract must implement ERC721 interface
- Contract must not be zero address
- Emits
NFTContractWhitelistedevent
function removeNFTContract(address nftContract) external onlyOwnerRemoves NFT contract from whitelist.
- Requirements:
- Caller must be owner
- Contract must be whitelisted
- Emits
NFTContractRemovedevent
function addStakingDuration(uint256 duration) external onlyOwnerAdds new staking duration option.
- Requirements:
- Caller must be owner
- Duration must be between MIN_STAKE_DURATION and MAX_STAKE_DURATION
- Emits
StakingDurationAddedevent
function removeStakingDuration(uint256 duration) external onlyOwnerRemoves staking duration option.
- Requirements:
- Caller must be owner
- Duration must exist
- Emits
StakingDurationRemovedevent
function recoverTokens(
address token,
address recipient,
uint256 amount
) external onlyOwnerRecovers stuck tokens (ERC20/ERC721/ERC1155).
- Requirements:
- Caller must be owner
- Token must not be staked
- Recipient must not be zero address
- Emits
TokensRecoveredevent
function stakeNFT(
address nftContract,
uint256 tokenId,
uint256 duration
) external whenNotPaused onlyWhitelistedNFT(nftContract) validStakingDuration(duration)Stakes an NFT for specified duration.
- Requirements:
- Contract must not be paused
- NFT contract must be whitelisted
- Duration must be valid
- Caller must own the NFT
- NFT must not be already staked
- Emits
NFTStakedevent
function batchStakeNFTs(
address nftContract,
uint256[] calldata tokenIds,
uint256 duration
) external whenNotPaused onlyWhitelistedNFT(nftContract) validStakingDuration(duration)Stakes multiple NFTs in single transaction.
- Requirements:
- Same as stakeNFT for each token
- Emits multiple
NFTStakedevents
function unstakeNFT(
address nftContract,
uint256 tokenId
) externalUnstakes an NFT after lock period.
- Requirements:
- Stake must exist
- Caller must be stake owner
- Lock period must be over
- Emits
NFTUnstakedevent
function batchUnstakeNFTs(
address nftContract,
uint256[] calldata tokenIds
) externalUnstakes multiple NFTs in single transaction.
- Requirements:
- Same as unstakeNFT for each token
- Emits multiple
NFTUnstakedevents
function emergencyUnstake(
address nftContract,
uint256 tokenId
) external onlyOwnerEmergency unstake function for admin.
- Requirements:
- Caller must be owner
- Stake must exist
- Emits
NFTUnstakedevent
function getStakeInfo(
address nftContract,
uint256 tokenId
) external view returns (StakeInfo memory)Returns stake information for given NFT.
function isStakingDurationValid(uint256 duration) external view returns (bool)Checks if staking duration is valid.
function isNFTStaked(
address nftContract,
uint256 tokenId
) external view returns (bool)Checks if NFT is currently staked.
-
Access Control
- Owner-only functions protected by
onlyOwnermodifier - Clear separation between admin and user functions
- Owner-only functions protected by
-
Reentrancy Protection
- All state changes before external calls
- Use of OpenZeppelin's ReentrancyGuard
-
Input Validation
- Comprehensive checks for zero addresses
- Validation of staking durations
- Whitelist verification for NFT contracts
-
Emergency Measures
- Pause functionality for new stakes
- Emergency unstake for admin
- Token recovery for stuck assets
-
Gas Optimization
- Batch functions for multiple NFTs
- Efficient use of storage slots
- Minimal storage reads and writes
- OpenZeppelin Contracts v4.9.0
@openzeppelin/contracts/token/ERC721/IERC721.sol@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol@openzeppelin/contracts/security/ReentrancyGuard.sol@openzeppelin/contracts/security/Pausable.sol@openzeppelin/contracts/access/Ownable.sol