PresaleContract
Token sale contract for distributing SYL in a capped presale with optional whitelist, soft/hard caps, per‑wallet limits, and a post‑sale claim/refund window. Payments are made in a configured ERC‑20 (e.g., USDC/USDT/DAI). The sale can operate in mint‑on‑claim mode or using pre‑funded SYL.
Units: sale times use seconds (
block.timestamp). Payment amounts use the payment token’s decimals. SYL uses 18 decimals.
Responsibilities
- Accept payments during the sale window, enforcing hard cap and per‑wallet min/max.
- Optionally enforce a whitelist.
- After the sale, finalize → set result (
success = totalRaised ≥ softCap) and set a claim deadline. - If success: forward raised funds to
fundsWallet, and (if pre‑funded) burn immediate unsold SYL. - If failed: enable refunds.
- Let buyers claim SYL until
claimDeadline; after that, allow the owner (or anyone) to burn remaining unclaimed tokens.
Key parameters (initialize)
initialize(
address paymentToken, // e.g. USDC/USDT/DAI (ERC20)
address saleToken_, // SYL proxy
address fundsWallet_, // where raised funds are sent on success
address initialOwner, // proxy owner
uint64 startTime_,
uint64 endTime_,
uint256 softCap_, // in payment token units
uint256 hardCap_, // in payment token units
uint256 minBuy_, // per-wallet min (0 to disable)
uint256 maxBuy_, // per-wallet max (0 to disable)
uint256 rate_, // **tokens per 1 payment token**, scaled 1e18
bool mintOnClaim_ // if true, contract must hold MINTER_ROLE on SYL
)Rate scaling
tokens = paymentAmount * rate / 1e18
To get X SYL per 1.0 payment token, set:
rate = X * 1e18 / 10**payment.decimals()
Example: USDC (6 decimals), 100 SYL / 1 USDC → rate = 100 * 1e18 / 1e6.
State (selected)
payment(ERC20Upgradeable) — the payment tokensaleToken(SylanToken) — SYL token contractfundsWallet(address)startTime,endTime(uint64 s)softCap,hardCap(uint256 in payment units)minBuy,maxBuy(uint256 per‑wallet limits, in payment units)rate(uint256, scaled as above)totalRaised(uint256)finalized,success(bool)claimDeadline(uint64 s)whitelistEnabled(bool),isWhitelisted(address) → boolmintOnClaim(bool)contributed(address) → uint256— amount a buyer paidclaimedOrRefunded(address) → bool
Lifecycle
1) Buy
buy(uint256 paymentAmount) — whenNotPaused
Requirements:
startTime ≤ now ≤ endTime- If
whitelistEnabled,isWhitelisted[msg.sender] == true paymentAmount > 0totalRaised + paymentAmount ≤ hardCap- Per‑wallet:
minBuyandmaxBuyenforced on the new cumulative total
Effects:
- Records
contributed[msg.sender] += paymentAmount,totalRaised += paymentAmount - Pulls funds:
payment.safeTransferFrom(msg.sender, address(this), paymentAmount) - Emits
Contributed(buyer, paymentAmount, newTotal)
Buyers must
approve(presale, paymentAmount)on the payment token before callingbuy.
2) Finalize
finalize(uint64 claimDeadline_) — onlyOwner
Requirements: sale ended (now > endTime or hard cap reached), not already finalized, and claimDeadline_ > now.
Effects:
finalized = true; success = (totalRaised ≥ softCap); claimDeadline = claimDeadline_- If success:
- Forwards payment:
payment.safeTransfer(fundsWallet, totalRaised) - If pre‑funded (
mintOnClaim == false): computessold = totalRaised * rate / 1e18; burns any immediate unsold from the contract balance and emitsUnsoldBurned(unsold)
- Forwards payment:
- Emits
Finalized(success, totalRaised)
3a) Claim (success)
claim() — whenNotPaused
Requirements: finalized && success, now ≤ claimDeadline, and not already claimed.
Effects:
tokens = contributed[msg.sender] * rate / 1e18- If
mintOnClaim→saleToken.mint(msg.sender, tokens); else →saleToken.transfer(msg.sender, tokens) - Marks
claimedOrRefunded[msg.sender] = true - Emits
Claimed(buyer, tokens)
3b) Refund (failure)
refund() — whenNotPaused
Requirements: finalized && !success and not already refunded.
Effects:
- Transfers back the contributed amount:
payment.safeTransfer(msg.sender, contributed[msg.sender]) - Marks
claimedOrRefunded[msg.sender] = true - Emits
Refunded(buyer, paymentAmount)
4) Burn leftovers
burnUnsold() — whenNotPaused
Requirements: finalized and now > claimDeadline.
Effects:
- Burns any remaining SYL held by the contract (unclaimed)
- Emits
UnsoldBurned(amount)
Admin surface (onlyOwner)
setWhitelistEnabled(bool)setWhitelisted(address user, bool allowed)setTimes(uint64 start, uint64 end)— only before sale startssetCaps(uint256 soft, uint256 hard)— only before sale startssetLimits(uint256 min, uint256 max)— only before sale startssetRate(uint256 rate)— only before sale starts;rate > 0setFundsWallet(address)— non‑zero addresssetMintOnClaim(bool)— toggle minting vs pre‑funded modepause()/unpause()and UUPSupgradeTo/upgradeToAndCall(...)
Events
Contributed(address indexed buyer, uint256 paymentAmount, uint256 newTotal)Finalized(bool success, uint256 totalRaised)Claimed(address indexed buyer, uint256 tokensAmount)Refunded(address indexed buyer, uint256 paymentAmount)WhitelistSet(address indexed user, bool allowed)ParamsUpdated()UnsoldBurned(uint256 amount)
Minimal ABI (buyer‑facing)
[
{"type":"function","stateMutability":"view","name":"payment","inputs":[],"outputs":[{"type":"address"}]},
{"type":"function","stateMutability":"view","name":"saleToken","inputs":[],"outputs":[{"type":"address"}]},
{"type":"function","stateMutability":"view","name":"fundsWallet","inputs":[],"outputs":[{"type":"address"}]},
{"type":"function","stateMutability":"view","name":"startTime","inputs":[],"outputs":[{"type":"uint64"}]},
{"type":"function","stateMutability":"view","name":"endTime","inputs":[],"outputs":[{"type":"uint64"}]},
{"type":"function","stateMutability":"view","name":"softCap","inputs":[],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"hardCap","inputs":[],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"minBuy","inputs":[],"outputs"][{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"maxBuy","inputs":[],"outputs"][{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"rate","inputs":[],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"whitelistEnabled","inputs":[],"outputs":[{"type":"bool"}]},
{"type":"function","stateMutability":"view","name":"isWhitelisted","inputs":[{"name":"user","type":"address"}],"outputs":[{"type":"bool"}]},
{"type":"function","stateMutability":"view","name":"finalized","inputs":[],"outputs":[{"type":"bool"}]},
{"type":"function","stateMutability":"view","name":"success","inputs":[],"outputs":[{"type":"bool"}]},
{"type":"function","stateMutability":"view","name":"claimDeadline","inputs":[],"outputs":[{"type":"uint64"}]},
{"type":"function","stateMutability":"view","name":"totalRaised","inputs":[],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"contributed","inputs":[{"name":"buyer","type":"address"}],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"claimedOrRefunded","inputs":[{"name":"buyer","type":"address"}],"outputs":[{"type":"bool"}]},
{"type":"function","stateMutability":"nonpayable","name":"buy","inputs":[{"name":"paymentAmount","type":"uint256"}],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"claim","inputs":[],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"refund","inputs":[],"outputs":[]}
]Integration tips
- Approvals: buyers must approve the payment token for each purchase.
- Decimals: display amounts in payment token units; compute SYL using the rate formula above.
- Whitelist UX: if enabled, surface
isWhitelisted(user)and disable the buy button accordingly. - Claim timer: show a countdown to
claimDeadline; after expiry,claim()reverts andburnUnsold()can be called. - Mint vs pre‑funded:
mintOnClaim = true→ presale contract must holdMINTER_ROLEon SYL; no prefunding needed.mintOnClaim = false→ deposit enough SYL before finalization; finalize will burn immediate unsold;burnUnsold()later removes unclaimed leftovers.
Admin & security notes
setTimes,setCaps,setLimits, andsetRateare locked once the sale starts.fundsWalletmust be a non‑zero address (ideally a multisig).- Contract is pausable; UIs should reflect paused state.
- Uses ReentrancyGuard and SafeERC20 for transfers; claims/refunds are pull‑style from escrowed funds.
Out of scope for this page
- Addresses & ABIs → Architecture → Addresses & ABIs
- Token/vesting allocations → Sylan Token / Sylan Vesting
- Marketplace protocol (escrow/consensus) — unrelated to presale