ERC-5528: Refundable Fungible Token
ERC-5528 is the ERC-20 counterpart to ERC-5507: a fungible token with a refund window during which buyers can return tokens for their original payment. Used for:
- Initial token sales with cooling-off periods.
- Crowdfunding tokens that refund if a milestone isn't met.
- Subscription tokens with per-period refundability.
- Compliance-driven refunds in regulated token sales.
The contract escrows the payment; the issuer can only withdraw funds after refund deadlines pass.
Required Interface (delta from ERC-20)
solidity
interface IERC5528 {
event Refund(address indexed buyer, uint256 amount);
function refund(uint256 amount) external;
function refundDeadlineOf(address buyer) external view returns (uint64);
function refundOf(address buyer) external view returns (uint256);
}The refund window is per-buyer, set at the time of purchase (typically by the contract recording the purchase + setting the deadline based on the campaign's policy).
Neo Equivalent: NEP-17 + Per-Buyer Refund Window + Escrow
csharp
public static void RecordPurchase(UInt160 buyer, BigInteger amount, BigInteger windowSeconds)
{
if (!Runtime.CheckWitness(GetIssuer())) throw new Exception("NEP17:NotIssuer");
var entry = new Map<string, object>
{
["amount"] = amount,
["deadline"] = Runtime.Time + windowSeconds * 1000,
};
Storage.Put(Storage.CurrentContext, RefundKey(buyer), StdLib.Serialize(entry));
Mint(buyer, amount);
}
public static void Refund(BigInteger amount)
{
var buyer = Runtime.CallingScriptHash;
if (!Runtime.CheckWitness(buyer)) throw new Exception("NEP17:NoAuth");
var raw = Storage.Get(Storage.CurrentContext, RefundKey(buyer));
if (raw is null) throw new Exception("NEP17:NotRefundable");
var entry = (Map<string, object>)StdLib.Deserialize((ByteString)raw);
if ((BigInteger)entry["deadline"] < Runtime.Time) throw new Exception("NEP17:RefundExpired");
if (amount > (BigInteger)entry["amount"]) throw new Exception("NEP17:RefundExceedsBalance");
// Update or clear the refund record.
if (amount == (BigInteger)entry["amount"])
{
Storage.Delete(Storage.CurrentContext, RefundKey(buyer));
}
else
{
entry["amount"] = (BigInteger)entry["amount"] - amount;
Storage.Put(Storage.CurrentContext, RefundKey(buyer), StdLib.Serialize(entry));
}
Burn(buyer, amount);
// Pay refund (NEO/GAS or whatever paymentToken).
Contract.Call(GAS.Hash, "transfer", CallFlags.All,
new object[] { Runtime.ExecutingScriptHash, buyer, amount, null });
OnRefund(buyer, amount);
}| ERC-5528 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
refund(amount) | Refund(amount) buyer-witness-checked | Burns tokens + pays refund |
refundDeadlineOf(buyer) | RefundDeadlineOf(buyer) view | |
refundOf(buyer) | RefundOf(buyer) view | |
| Per-buyer refund record | (buyer → amount, deadline) storage | Direct port |
| Escrow until deadline | Contract holds payment until issuer withdraws | Withdrawal gated per-buyer |
Composition
- ERC-5507 — refundable NFTs. ERC-5528 is the fungible counterpart; both share the time-bounded escrow shape.
- ERC-3643 — T-REX. Regulated token sales pair refundability with compliance gates.
- ERC-7144 — transfer validation. Lock transfers during the refund window so refundable tokens aren't traded before being refunded.
Migration Notes
For token sales / crowdfunding contracts:
- Set per-buyer refund window at purchase based on the campaign's policy.
- Lock transfers during the refund window if anti-flip is desired (combine with ERC-7144 transfer-validation hook).
- Issuer's withdrawal flow checks all per-buyer deadlines have passed before allowing fund extraction.
For partial refunds (buyer wants to refund half their purchase), update the storage entry rather than delete; the refund window stays intact for the remaining balance.
