Skip to main content
When you airdrop tokens to users, you may want to prevent them from immediately selling or transferring all their tokens. This policy ensures that airdrop recipients maintain a minimum balance, effectively locking a portion of their airdropped tokens.

How does it work?

By hooking the transfer and transferFrom functions of the ERC-20 contract into the Rules Engine with a policy that checks if the sender’s post-transfer balance would fall below the required minimum threshold.

Let’s break that down further.

When a user attempts to transfer tokens, the Rules Engine checks their current balance against the amount they’re trying to transfer. If completing the transfer would leave them with less than the minimum required balance (in this example, 1000 tokens), the transaction reverts. This is particularly useful for:
  • Vesting schedules: Lock initial airdrop amounts while allowing users to keep tokens they’ve purchased or earned
  • Anti-dumping protection: Prevent recipients from immediately selling all their airdropped tokens
  • Community engagement: Encourage long-term holding by requiring a minimum stake

Implementation

The policy uses a Foreign Call to check the sender’s balance before the transfer executes. You’ll need to:
  1. Set the minimum balance threshold (e.g., 1000 tokens)
  2. Replace 0xTokenContractAddress with your actual token contract address
  3. Optionally, combine this with time-based rules to gradually reduce the lockup amount over time

Policy JSON

{
  "Policy": "Lockup Airdropped Tokens",
  "Description": "Ensures users maintain a minimum balance to prevent immediate token dumps",
  "PolicyType": "open",
  "CallingFunctions": [
    {
      "Name": "transfer(address to, uint256 value)",
      "FunctionSignature": "transfer(address to, uint256 value)",
      "EncodedValues": "address to, uint256 value"
    },
    {
      "Name": "transferFrom(address from, address to, uint256 value)",
      "FunctionSignature": "transferFrom(address from, address to, uint256 value)",
      "EncodedValues": "address from, address to, uint256 value"
    }
  ],
  "ForeignCalls": [
    {
      "Name": "GetBalanceForTransfer",
      "Address": "0xTokenContractAddress",
      "Function": "balanceOf(address)",
      "ReturnType": "uint256",
      "ValuesToPass": "GV:MSG_SENDER",
      "MappedTrackerKeyValues": "",
      "CallingFunction": "transfer(address to, uint256 value)"
    },
    {
      "Name": "GetBalanceForTransferFrom",
      "Address": "0xTokenContractAddress",
      "Function": "balanceOf(address)",
      "ReturnType": "uint256",
      "ValuesToPass": "from",
      "MappedTrackerKeyValues": "",
      "CallingFunction": "transferFrom(address from, address to, uint256 value)"
    }
  ],
  "Trackers": [],
  "MappedTrackers": [],
  "Rules": [
    {
      "Name": "Ensure Min Balance for Transfer",
      "Description": "Prevents transfers that would leave sender below minimum balance threshold",
      "Condition": "(FC:GetBalanceForTransfer - value) >= 1000",
      "PositiveEffects": [],
      "NegativeEffects": ["revert(\"Transfer would violate minimum balance requirement\")"],
      "CallingFunction": "transfer(address to, uint256 value)"
    },
    {
      "Name": "Ensure Min Balance for TransferFrom",
      "Description": "Prevents transferFrom operations that would leave sender below minimum balance threshold",
      "Condition": "(FC:GetBalanceForTransferFrom - value) >= 1000",
      "PositiveEffects": [],
      "NegativeEffects": ["revert(\"Transfer would violate minimum balance requirement\")"],
      "CallingFunction": "transferFrom(address from, address to, uint256 value)"
    }
  ]
}
It’s critical that you add the rule to both the transfer and transferFrom functions to ensure the minimum balance requirement cannot be bypassed through allowance-based transfers.
I