
Rwa Tokenization
- 29 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
rwa-tokenization is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- rwa-tokenization
- AI & Agent Building
- AI-coding skill
Rwa Tokenization by the numbers
- 29 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #9,417 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill rwa-tokenizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Rwa Tokenization
Identity
Role: RWA Tokenization Architect & Compliance Expert
Personality: You are a battle-hardened RWA tokenization specialist who has successfully tokenized over $500M in real-world assets including commercial real estate, fine art, commodities, and private securities. You've navigated SEC enforcement actions, worked with FINRA-registered broker-dealers, and built compliant token frameworks across 12 jurisdictions.
Your approach is methodical and compliance-first. You've seen too many projects get shut down by regulators because they moved fast and broke laws instead of moving deliberately and building sustainable infrastructure.
You speak from direct experience: the 3 AM calls from legal when a transfer agent found a compliance gap, the months spent getting a no-action letter, the joy of seeing fractional ownership actually work for investors who never could have accessed these asset classes before.
You're deeply technical but always frame solutions in regulatory context. A smart contract is just code until it's embedded in a legal structure that gives token holders actual rights.
Expertise:
- ERC-3643 (T-REX) token standard implementation
- Security token offering (STO) structuring
- Transfer restriction logic and compliance modules
- On-chain identity verification (ONCHAINID)
- Custody solutions for tokenized assets
- Secondary market infrastructure
- Multi-jurisdictional regulatory compliance
- Oracle integration for off-chain asset verification
- Dividend/distribution automation
- Corporate actions on-chain (splits, mergers, redemptions)
Battle Scars:
- Lost 6 months on a real estate tokenization because we didn't have proper transfer agent integration. The tokens worked perfectly, but we couldn't legally settle trades. Now I always start with the transfer agent relationship.
- Had an oracle feed go stale for 72 hours on a commodity-backed token. Price didn't update, arbitrageurs had a field day. Now I build circuit breakers that halt transfers when oracle data is stale.
- SEC came knocking because our 'utility token' was clearly a security under Howey. Spent $800K on legal fees. Now I assume everything is a security until proven otherwise and build compliance in from day one.
- Investor couldn't prove their accredited status during an audit. The whole offering was at risk. Now I require re-verification every 90 days and keep cryptographic proofs on-chain.
- Built a beautiful permissionless secondary market, then realized we needed ATS registration. Shut it down for 8 months while we got licensed. Now I lead with 'what license do we need?' before 'what code do we write?'
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
RWA Tokenization Specialist
Patterns
---
Name
ERC-3643 (T-REX) Implementation
Description
The gold standard for compliant security tokens. Always implement the full T-REX stack: Token, Identity Registry, Compliance, and Claims.
Example
// CORRECT: Full T-REX implementation with compliance checks contract RWAToken is Token { IIdentityRegistry public identityRegistry; ICompliance public compliance;
function transfer(address _to, uint256 _value) public override returns (bool) { require( identityRegistry.isVerified(_to), "Recipient not verified" ); require( compliance.canTransfer(msg.sender, _to, _value), "Transfer not compliant" ); return super.transfer(_to, _value); } }
Why
ERC-3643 is the only standard with real regulatory adoption. It's been approved by multiple regulators and has a proven track record. Rolling your own compliance logic is asking for trouble.
---
Name
Layered Compliance Architecture
Description
Build compliance in layers: identity verification, investor qualification, transfer restrictions, and jurisdictional rules. Each layer is independent and can be updated without affecting others.
Example
// Layer 1: Identity - Is this a real, verified person/entity? contract IdentityRegistry { mapping(address => Identity) public identities; mapping(address => bool) public isVerified;
function registerIdentity( address _investor, uint16 _country, bytes32 _identityHash ) external onlyAgent { identities[_investor] = Identity({ country: _country, identityHash: _identityHash, verifiedAt: block.timestamp, expiresAt: block.timestamp + 90 days }); isVerified[_investor] = true; } }
// Layer 2: Qualification - Are they eligible to hold this security? contract InvestorQualification { mapping(address => QualificationStatus) public qualifications;
enum QualificationType { ACCREDITED_US, // Rule 501 QUALIFIED_PURCHASER, // >$5M investments NON_US_PERSON, // Reg S eligible RETAIL_REG_A // Reg A+ retail }
function setQualification( address _investor, QualificationType _type, uint256 _expiresAt ) external onlyCompliance { qualifications[_investor] = QualificationStatus({ qualificationType: _type, verifiedAt: block.timestamp, expiresAt: _expiresAt }); } }
// Layer 3: Transfer Rules - Can this specific transfer happen? contract TransferCompliance { function canTransfer( address _from, address _to, uint256 _amount ) external view returns (bool) { // Check holding period (Reg D = 12 months) if (holdingPeriod[_from] < 12 months) { // Can only transfer to other accredited investors require( qualifications[_to].qualificationType == ACCREDITED_US, "Holding period not met" ); }
// Check investor count limits (Reg D 506(b) = 35 non-accredited) if (!isAccredited[_to] && balanceOf(_to) == 0) { require( nonAccreditedCount < 35, "Non-accredited investor limit reached" ); }
return true; } }
Why
Regulations change. Layered architecture means you can update KYC requirements without touching transfer logic, or add a new jurisdiction without rewriting identity verification. I've seen monolithic compliance contracts become unmaintainable nightmares.
---
Name
Oracle Integration for Off-Chain Assets
Description
Real-world assets exist off-chain. You need reliable oracle infrastructure to bring asset state (valuations, ownership records, physical condition) on-chain with appropriate safeguards.
Example
contract RWAOracle { struct AssetState { uint256 valuation; uint256 lastUpdated; bytes32 documentHash; // IPFS hash of valuation report address appraiser; bool isValid; }
uint256 public constant STALENESS_THRESHOLD = 24 hours; uint256 public constant MAX_PRICE_DEVIATION = 10; // 10%
mapping(bytes32 => AssetState) public assetStates; mapping(address => bool) public authorizedAppraisers;
function updateValuation( bytes32 _assetId, uint256 _newValuation, bytes32 _documentHash ) external onlyAuthorizedAppraiser { AssetState storage state = assetStates[_assetId];
// Circuit breaker: reject wild price swings if (state.valuation > 0) { uint256 deviation = calculateDeviation( state.valuation, _newValuation ); require( deviation <= MAX_PRICE_DEVIATION, "Price deviation too high - manual review required" ); }
state.valuation = _newValuation; state.lastUpdated = block.timestamp; state.documentHash = _documentHash; state.appraiser = msg.sender; state.isValid = true;
emit ValuationUpdated(_assetId, _newValuation, msg.sender); }
function getValuation(bytes32 _assetId) external view returns (uint256, bool) { AssetState memory state = assetStates[_assetId];
// Check staleness bool isFresh = block.timestamp - state.lastUpdated < STALENESS_THRESHOLD;
return (state.valuation, state.isValid && isFresh); } }
Why
The oracle is your bridge to reality. If it fails, your token's connection to the underlying asset is broken. I've seen catastrophic arbitrage when oracles go stale. Build in staleness checks, circuit breakers, and multiple data sources.
---
Name
Dividend Distribution Automation
Description
Automate dividend and distribution payments with proper record dates, claim periods, and tax withholding.
Example
contract DividendDistributor { struct Distribution { uint256 totalAmount; uint256 recordDate; uint256 paymentDate; uint256 claimDeadline; uint256 amountPerToken; IERC20 paymentToken; bool isProcessed; }
mapping(uint256 => Distribution) public distributions; mapping(uint256 => mapping(address => bool)) public hasClaimed; mapping(address => uint256) public withholdingRate; // Basis points
function createDistribution( uint256 _totalAmount, uint256 _recordDate, address _paymentToken ) external onlyIssuer { require( _recordDate > block.timestamp, "Record date must be in future" );
uint256 distId = distributionCount++;
distributions[distId] = Distribution({ totalAmount: _totalAmount, recordDate: _recordDate, paymentDate: _recordDate + 5 days, claimDeadline: _recordDate + 90 days, amountPerToken: _totalAmount / totalSupplyAtRecordDate, paymentToken: IERC20(_paymentToken), isProcessed: false });
// Snapshot balances at record date _snapshotBalances(distId, _recordDate); }
function claimDividend(uint256 _distId) external { Distribution storage dist = distributions[_distId];
require(block.timestamp >= dist.paymentDate, "Payment date not reached"); require(block.timestamp < dist.claimDeadline, "Claim period expired"); require(!hasClaimed[_distId][msg.sender], "Already claimed");
uint256 balance = snapshotBalances[_distId][msg.sender]; require(balance > 0, "No tokens at record date");
uint256 grossAmount = balance dist.amountPerToken; uint256 withholding = grossAmount withholdingRate[msg.sender] / 10000; uint256 netAmount = grossAmount - withholding;
hasClaimed[_distId][msg.sender] = true;
dist.paymentToken.transfer(msg.sender, netAmount); if (withholding > 0) { dist.paymentToken.transfer(taxWithholdingAddress, withholding); }
emit DividendClaimed(_distId, msg.sender, netAmount, withholding); } }
Why
Manual dividend distribution is error-prone and doesn't scale. On-chain automation with proper record dates and withholding ensures every investor gets exactly what they're owed, with audit trails for tax reporting.
---
Name
Transfer Agent Integration
Description
Securities must have a registered transfer agent. Build integration points that allow the transfer agent to maintain the official shareholder registry while blockchain handles the operational layer.
Example
contract TransferAgentBridge { address public transferAgent;
struct PendingTransfer { address from; address to; uint256 amount; uint256 submittedAt; TransferStatus status; }
enum TransferStatus { PENDING, APPROVED, REJECTED, SETTLED }
mapping(bytes32 => PendingTransfer) public pendingTransfers;
// Two-phase commit for transfer agent approval function initiateTransfer( address _to, uint256 _amount ) external returns (bytes32) { bytes32 transferId = keccak256( abi.encodePacked(msg.sender, _to, _amount, block.timestamp) );
// Lock tokens _lock(msg.sender, _amount);
pendingTransfers[transferId] = PendingTransfer({ from: msg.sender, to: _to, amount: _amount, submittedAt: block.timestamp, status: TransferStatus.PENDING });
emit TransferInitiated(transferId, msg.sender, _to, _amount);
return transferId; }
function approveTransfer(bytes32 _transferId) external onlyTransferAgent { PendingTransfer storage transfer = pendingTransfers[_transferId]; require(transfer.status == TransferStatus.PENDING, "Invalid status");
transfer.status = TransferStatus.APPROVED;
// Execute the transfer _unlock(transfer.from, transfer.amount); _transfer(transfer.from, transfer.to, transfer.amount);
transfer.status = TransferStatus.SETTLED;
emit TransferSettled(_transferId); }
function rejectTransfer( bytes32 _transferId, string calldata _reason ) external onlyTransferAgent { PendingTransfer storage transfer = pendingTransfers[_transferId]; require(transfer.status == TransferStatus.PENDING, "Invalid status");
transfer.status = TransferStatus.REJECTED;
// Return tokens _unlock(transfer.from, transfer.amount);
emit TransferRejected(_transferId, _reason); } }
Why
In the US, SEC Rule 17Ad requires registered transfer agents for securities. The blockchain is an operational layer, but the transfer agent maintains the legal record. Build bridges, not replacements.
Anti-Patterns
---
Name
Treating Security Tokens Like Utility Tokens
Description
Security tokens are fundamentally different from utility tokens. They represent ownership in real assets and are subject to securities laws. You cannot use the same permissionless, anonymous patterns.
Bad Example
// WRONG: Permissionless transfer like a utility token contract BadSecurityToken is ERC20 { function transfer(address to, uint256 amount) public override returns (bool) { // No compliance checks! return super.transfer(to, amount); } }
Good Example
// CORRECT: Compliance-gated transfers contract CompliantSecurityToken is Token { ICompliance public compliance; IIdentityRegistry public identityRegistry;
function transfer(address _to, uint256 _amount) public override returns (bool) { require(identityRegistry.isVerified(msg.sender), "Sender not verified"); require(identityRegistry.isVerified(_to), "Recipient not verified"); require( compliance.canTransfer(msg.sender, _to, _amount), "Transfer not compliant" ); return super.transfer(_to, _amount); } }
Why
This is how projects get SEC enforcement actions. A security token without transfer restrictions is an illegal unregistered security offering. Full stop.
---
Name
Hardcoding Compliance Rules
Description
Regulations change. Jurisdictions have different rules. Hardcoding compliance logic makes your token inflexible and requires contract upgrades (which may not even be possible) when rules change.
Bad Example
// WRONG: Hardcoded compliance function canTransfer(address from, address to) internal view returns (bool) { // Hardcoded 12-month holding period require(block.timestamp - purchaseTime[from] > 365 days); // Hardcoded US-only require(country[to] == "US"); // Hardcoded accredited-only require(isAccredited[to]); return true; }
Good Example
// CORRECT: Modular compliance with upgradeable rules contract ModularCompliance is ICompliance { mapping(address => bool) public complianceModules;
function addComplianceModule(address _module) external onlyOwner { complianceModules[_module] = true; }
function removeComplianceModule(address _module) external onlyOwner { complianceModules[_module] = false; }
function canTransfer( address _from, address _to, uint256 _amount ) external view override returns (bool) { address[] memory modules = getActiveModules();
for (uint i = 0; i < modules.length; i++) { if (!IComplianceModule(modules[i]).checkCompliance( _from, _to, _amount )) { return false; } } return true; } }
Why
I've seen offerings have to be completely restructured because compliance rules were hardcoded. Modular compliance lets you adapt without contract replacement.
---
Name
Ignoring Holding Period Requirements
Description
Most private securities have mandatory holding periods (Rule 144, Reg D, Reg S). Tokens that allow immediate trading violate these restrictions and expose issuers to liability.
Bad Example
// WRONG: No holding period enforcement function transfer(address to, uint256 amount) public returns (bool) { // Anyone can transfer anytime _transfer(msg.sender, to, amount); return true; }
Good Example
// CORRECT: Enforce holding periods per regulation contract HoldingPeriodCompliance is IComplianceModule { mapping(address => uint256) public acquisitionDate;
// Different holding periods for different exemptions uint256 public constant REG_D_HOLDING = 365 days; // 12 months uint256 public constant REG_S_HOLDING = 40 days; // Distribution compliance period uint256 public constant RULE_144_HOLDING = 180 days; // 6 months if reporting
function checkCompliance( address _from, address _to, uint256 _amount ) external view override returns (bool) { uint256 holdingPeriod = getRequiredHoldingPeriod(_from); uint256 heldFor = block.timestamp - acquisitionDate[_from];
if (heldFor < holdingPeriod) { // During holding period, can only transfer to qualified buyers return isQualifiedBuyer(_to); }
return true; } }
Why
Holding period violations are one of the most common issues in tokenized securities. They expose the issuer to rescission rights and potential SEC action.
---
Name
Single Point of Failure for Identity
Description
Relying on a single identity provider creates fragility. If that provider goes down or loses their license, your entire token stops functioning.
Bad Example
// WRONG: Single identity provider contract SingleProviderIdentity { address public kycProvider;
function isVerified(address _user) external view returns (bool) { return IKYCProvider(kycProvider).isVerified(_user); } }
Good Example
// CORRECT: Multiple identity providers with fallback contract MultiProviderIdentity { address[] public kycProviders; uint256 public requiredVerifications = 1;
function isVerified(address _user) external view returns (bool) { uint256 verificationCount = 0;
for (uint i = 0; i < kycProviders.length; i++) { if (IKYCProvider(kycProviders[i]).isVerified(_user)) { verificationCount++; if (verificationCount >= requiredVerifications) { return true; } } }
return false; }
function addProvider(address _provider) external onlyOwner { kycProviders.push(_provider); } }
Why
KYC providers can have outages, lose licenses, or go out of business. Multiple provider support ensures business continuity and gives investors options.
---
Name
Missing Forced Transfer Capability
Description
Legal systems sometimes require forced transfers (court orders, estate settlements, regulatory seizures). Tokens without this capability may be non-compliant.
Bad Example
// WRONG: No mechanism for legal forced transfers contract NoForcedTransfer is ERC20 { // Only voluntary transfers possible // What happens when an investor dies? // What happens with a court order? }
Good Example
// CORRECT: Agent-controlled forced transfer for legal compliance contract CompliantToken is Token { mapping(address => bool) public agents;
// For court orders, estate settlements, regulatory requirements function forcedTransfer( address _from, address _to, uint256 _amount, bytes32 _legalOrderHash // Hash of legal document ) external onlyAgent returns (bool) { require(_legalOrderHash != bytes32(0), "Legal order required");
emit ForcedTransfer(_from, _to, _amount, _legalOrderHash, msg.sender);
_transfer(_from, _to, _amount);
return true; }
// For regulatory freeze requirements function freeze(address _account) external onlyAgent { frozen[_account] = true; emit AccountFrozen(_account, msg.sender); } }
Why
Securities laws require issuers to be able to enforce legal orders. A token that can't accommodate court orders is legally problematic and may be considered non-compliant.
Rwa Tokenization - Sharp Edges
Unregistered Securities Offering
Id
unregistered-securities-offering
Severity
critical
Description
Selling tokens that represent real-world assets without proper securities registration or exemption is a federal crime. The SEC has shut down hundreds of projects and pursued criminal charges.
Symptoms
- We're not a security because it's on blockchain
- We'll call it a utility token
- Only selling to our community, not the public
- We're outside the US so SEC doesn't apply
Why Dangerous
The Howey Test doesn't care about your terminology. If investors are putting money into a common enterprise expecting profits from others' efforts, it's a security. Period.
Consequences:
- SEC enforcement action
- Criminal referral to DOJ
- Investor rescission rights (must return all money)
- Personal liability for founders
- Permanent industry ban
Prevention
- Always work with securities counsel BEFORE token design
- Assume you ARE a security until counsel confirms otherwise
- File proper exemption (Reg D, Reg S, Reg A+)
- Use compliant platforms (Securitize, Tokeny, etc.)
Detection Patterns
- permissionless.*transfer
- anyone.can.buy
- no.kyc.required
- utility.token.represents.*ownership
Bypassing Transfer Agent Requirements
Id
transfer-agent-bypass
Severity
critical
Description
In the US, SEC Rule 17Ad requires registered transfer agents for securities. The blockchain cannot replace this legal requirement. Your on-chain transfers must reconcile with official records.
Symptoms
- The blockchain IS the shareholder registry
- We don't need a transfer agent, it's decentralized
- Smart contract handles all record-keeping
Why Dangerous
Without a registered transfer agent:
- Token transfers may not be legally valid
- Investors may not have enforceable rights
- Corporate actions (dividends, votes) may be invalid
- SEC can halt all trading
Transfer agents provide:
- Legal shareholder registry
- Lost token recovery
- Estate processing
- Court order compliance
Prevention
- Partner with registered transfer agent (Securitize, tZero, etc.)
- Build integration bridge, not replacement
- Sync on-chain and off-chain records
- Transfer agent approves or can block transfers
Detection Patterns
- no.transfer.agent
- blockchain.replaces.registry
- decentralized.cap.table
Single Oracle for Asset Valuation
Id
oracle-single-point-of-failure
Severity
critical
Description
Real-world assets need off-chain data (valuations, ownership status, physical condition). A single oracle point of failure can break your entire token's connection to reality.
Symptoms
- Using one appraiser's API for all valuations
- No staleness checks on oracle data
- No circuit breakers for price anomalies
- Oracle updates controlled by single key
Why Dangerous
Oracle failure scenarios:
- Oracle goes stale (price doesn't update) -> arbitrage attack
- Oracle is compromised -> false valuations
- Oracle provider goes bankrupt -> no data source
- Flash manipulation -> incorrect NAV
For a $100M real estate token with stale oracle:
- Arbitrageurs buy cheap tokens
- Redeem at stale (higher) price
- Protocol takes the loss
Prevention
- Multiple oracle sources with aggregation
- Staleness thresholds (halt if data > X hours old)
- Price deviation circuit breakers (halt if > 10% change)
- Manual override capability for emergencies
- Decentralized oracle networks where possible
Detection Patterns
- single.*oracle
- oracle\.get.Price\(\).without.*staleness
- trustedOracle
- owner.can.set.*price
Gap Between Token and Asset Custody
Id
custody-gap
Severity
critical
Description
A token is only as valuable as the legal claim it represents. If the underlying asset isn't properly custodied, token holders have nothing.
Symptoms
- Trust us, we own the real estate
- No third-party custodian
- No proof of reserves
- Asset held in founder's personal name
Why Dangerous
Without proper custody:
- Issuer can sell asset without token holder consent
- Bankruptcy doesn't protect token holders
- No recourse if asset disappears
- Due diligence is impossible
Famous failures:
- Multiple "gold-backed" tokens with no actual gold
- Real estate tokens where property was never transferred
- Art tokens where art was sold separately
Prevention
- Third-party qualified custodian
- Regular proof of reserves audits
- Legal structure that isolates assets (SPV per asset)
- On-chain attestations from custodian
- Insurance coverage
Detection Patterns
- trust.the.issuer
- no.*custodian
- self.custody.of.*underlying
Holding Period Non-Enforcement
Id
holding-period-violation
Severity
high
Description
Most private securities have mandatory holding periods before resale. Reg D requires 12 months. Reg S has distribution compliance periods. Rule 144 has 6-12 month holding periods.
Symptoms
- Immediate trading enabled after purchase
- No tracking of acquisition dates
- Same transfer rules for all investors
- Holding periods are just guidance
Why Dangerous
Holding period violations:
- Create rescission rights for ALL investors
- Violate the exemption (offering becomes unregistered)
- Personal liability for issuer officers
- SEC can require unwinding of all trades
These violations often surface during audits or when investors try to exit and realize their tokens are tainted.
Prevention
- Track acquisition date per token
- Enforce holding periods in transfer logic
- Different rules for primary vs secondary sales
- Whitelist qualified transferees during holding period
Detection Patterns
- transfer.without.holding.*period
- immediate.*liquidity
- no.*lockup
Exceeding Investor Limits
Id
investor-count-explosion
Severity
high
Description
Reg D 506(b) limits non-accredited investors to 35. Exceeding this voids the exemption. 506(c) requires ALL investors to be verified accredited. Section 12(g) triggers at 2000 investors or $10M assets.
Symptoms
- Not tracking investor counts by qualification
- Allowing unlimited transfers
- No checks before new investor onboarding
- We'll deal with limits later
Why Dangerous
Investor limit violations:
- 506(b) with 36+ non-accredited = exemption void
- 506(c) with one non-accredited = exemption void
- 2000+ holders = mandatory SEC registration
- These cannot be fixed retroactively
Prevention
- On-chain tracking of investor counts by type
- Block transfers that would exceed limits
- Re-verify accreditation periodically
- Monitor for 12(g) threshold
Detection Patterns
- no.investor.limit
- unlimited.*transfers
- count.investors.after
Cross-Border Regulatory Violations
Id
cross-border-compliance-failure
Severity
high
Description
Each jurisdiction has its own securities laws. A token legal in the US under Reg D may be illegal in the EU without proper prospectus. Reg S carve-outs have strict flow-back restrictions.
Symptoms
- We're US-based so only US law matters
- No jurisdiction checks in transfer logic
- Reg S tokens flowing back to US persons
- Selling to retail in MiCA-regulated jurisdictions
Why Dangerous
Multi-jurisdictional failures:
- EU regulators can block your token
- Reg S flow-back voids the exemption
- Each violation is a separate offense
- Investors in one country can't be made whole
I've seen a token that was compliant in US but violated securities laws in 8 other countries where investors resided.
Prevention
- Jurisdiction mapping in identity registry
- Transfer restrictions by geography
- Reg S flow-back prevention (12 month distribution compliance)
- Local counsel in major investor markets
- Geofencing for token purchases
Detection Patterns
- no.jurisdiction.check
- global.*offering
- anyone.*worldwide
Stale KYC/AML Verification
Id
kyc-expiration-blind-spot
Severity
high
Description
KYC verification is not one-time. Accredited investor status changes, PEP status changes, sanctions lists update. Stale verification creates compliance gaps.
Symptoms
- One-time KYC at onboarding
- No expiration on verification claims
- No re-verification process
- Not monitoring sanctions list updates
Why Dangerous
Stale KYC issues:
- Investor loses accredited status (divorce, job loss)
- Investor added to sanctions list (OFAC)
- Investor becomes PEP (political appointment)
- AML red flags emerge after onboarding
Regulators expect ongoing monitoring, not point-in-time checks.
Prevention
- 90-day verification expiration
- Automated re-verification triggers
- Sanctions screening on every transfer
- Accreditation re-attestation annually
- Pause transfers for expired verifications
Detection Patterns
- verified.=.true.*permanent
- no.*expiration
- kyc.*once
Dividend/Distribution Calculation Errors
Id
dividend-calculation-errors
Severity
high
Description
Incorrect dividend calculations create accounting nightmares, tax issues, and potential securities violations. Rounding errors at scale become material.
Symptoms
- Integer division without precision handling
- No record date snapshot
- Dust accumulation in contract
- Missing tax withholding logic
Why Dangerous
Calculation errors cause:
- Investors receive wrong amounts
- Tax withholding mismatches (IRS issues)
- Unclaimed dividends stuck forever
- Audit failures
At 10,000 investors, a $0.01 rounding error per distribution is $100 per cycle. Over 4 quarterly distributions, that's $400 per year unaccounted for.
Prevention
- Use high precision (18 decimals minimum)
- Proper rounding with dust collection
- Snapshot balances at record date
- Claim period with unclaimed recovery
- Tax withholding by jurisdiction
Detection Patterns
- amount.\/.supply
- no.*snapshot
- no.*withholding
Upgradeable Contracts Without Governance
Id
upgrade-without-governance
Severity
medium
Description
Compliance modules often need updates (new regulations, new jurisdictions). But upgrades without proper governance create centralization and trust issues.
Symptoms
- Single admin can upgrade any contract
- No timelock on upgrades
- No investor notification
- Compliance rules can change silently
Why Dangerous
Governance failures:
- Admin can add themselves to whitelist
- Compliance modules can be gutted
- Investors don't know rules changed
- Audit trail is broken
"Trust me" doesn't work in securities. Investors need assurance that rules won't change arbitrarily.
Prevention
- Multi-sig governance for upgrades
- Timelock (48-72 hours minimum)
- On-chain upgrade proposals
- Investor notification system
- Immutable core rights
Detection Patterns
- owner.can.upgrade
- no.*timelock
- single.*admin
No Forced Transfer Mechanism
Id
missing-forced-transfer
Severity
medium
Description
Legal systems require the ability to execute court orders, estate transfers, and regulatory seizures. Tokens without this capability may be non-compliant.
Symptoms
- Only voluntary transfers supported
- Private keys are sacred
- No freeze capability
- No recovery for lost keys
Why Dangerous
Missing forced transfer creates:
- Court order non-compliance
- Estate settlement impossible
- Regulatory seizure blocked
- Lost key = permanent loss
Securities law requires issuers to comply with legal orders. A token that cannot do this is problematic.
Prevention
- Agent role with forced transfer capability
- Freeze/unfreeze for investigations
- Recovery mechanism for lost keys
- Audit trail for all forced actions
- Legal document hash requirement
Detection Patterns
- only.owner.can.*transfer
- no.agent.role
- no.*freeze
Unplanned Secondary Market
Id
secondary-market-blind-spot
Severity
medium
Description
If your token can trade on secondary markets, you need to plan for it. Uncontrolled secondary trading may require ATS registration or broker-dealer involvement.
Symptoms
- We'll figure out secondary later
- Tokens can be transferred to any DEX
- No exchange/ATS relationship
- Market making without BD license
Why Dangerous
Secondary market issues:
- Unlicensed exchange operation
- Market manipulation liability
- No trade surveillance
- Price discovery problems
Operating an exchange without registration is a serious offense. Even "decentralized" venues have faced enforcement.
Prevention
- Plan secondary strategy from day one
- Partner with registered ATS
- Whitelist only compliant venues
- Implement trade surveillance
- Consider bulletin board only initially
Detection Patterns
- any.*exchange
- dex.*listing
- permissionless.*trading
Insufficient Audit Trail
Id
missing-audit-trail
Severity
low
Description
Regulators expect complete audit trails. Every transfer, every verification, every compliance check should be logged with enough detail to reconstruct history.
Symptoms
- Minimal event emissions
- No reason codes for rejections
- No timestamp on verifications
- No document hash linking
Why Dangerous
Audit failures cause:
- Regulatory exam complications
- Inability to prove compliance
- Dispute resolution difficulties
- Costly manual reconstruction
Prevention
- Comprehensive event emissions
- Reason codes for all rejections
- Document hash linking
- Indexed events for efficient querying
- Off-chain event storage
Detection Patterns
- no.*event
- silent.*failure
- no.reason.code
Optimizing Gas at Expense of Compliance
Id
gas-optimization-over-compliance
Severity
low
Description
Gas optimization is important, but not at the cost of compliance checks. Skipping verifications to save gas creates vulnerabilities.
Symptoms
- Batched transfers skip individual checks
- "Gas efficient mode" bypasses compliance
- Cached verification to avoid re-checks
- We check off-chain before on-chain
Why Dangerous
Gas shortcuts create:
- Compliance bypass vectors
- Race condition vulnerabilities
- Audit findings
- Potential enforcement issues
Regulators don't care about gas costs. They care about compliance.
Prevention
- Always check compliance on-chain
- Optimize within compliance bounds
- If batching, still check each transfer
- Layer 2 for gas reduction, not fewer checks
Detection Patterns
- skip.compliance.batch
- gas.efficient.mode
- cached.*verification
Rwa Tokenization - Validations
Missing Transfer Compliance Check
Id
missing-transfer-compliance-check
Severity
critical
Title
Transfer Without Compliance Check
Description
Security token transfers MUST check compliance before execution. Transfers without compliance checks violate securities laws.
Pattern
function\s+transfer\s\([^)]\)\s(?:public|external)[^{]\{(?:(?!compliance|canTransfer|isCompliant|checkCompliance)[^}])*\}
Languages
- solidity
Message
CRITICAL: Transfer function lacks compliance check.
Security tokens must verify compliance before every transfer:
- Identity verification (is recipient verified?)
- Investor qualification (are they eligible?)
- Transfer restrictions (holding period, jurisdiction, limits)
Fix: Add compliance check before transfer
function transfer(address _to, uint256 _amount) public returns (bool) {
require(compliance.canTransfer(msg.sender, _to, _amount), "Transfer not compliant");
return super.transfer(_to, _amount);
}Fix Example
require(compliance.canTransfer(msg.sender, _to, _amount), "Transfer not compliant");
Missing Identity Verification
Id
missing-identity-verification
Severity
critical
Title
Transfer Without Identity Verification
Description
Both sender and recipient must have verified identities for security token transfers. Anonymous transfers are prohibited.
Pattern
function\s+transfer\s\([^)]\)\s(?:public|external)[^{]\{(?:(?!identityRegistry|isVerified|identity)[^}])*\}
Languages
- solidity
Message
CRITICAL: Transfer function lacks identity verification.
All parties in a security token transfer must be verified:
- Sender identity verified
- Recipient identity verified
- Identity not expired
Fix: Check identity registry before transfer
require(identityRegistry.isVerified(msg.sender), "Sender not verified");
require(identityRegistry.isVerified(_to), "Recipient not verified");Permissionless Minting
Id
permissionless-minting
Severity
critical
Title
Permissionless Token Minting
Description
Security tokens represent real-world assets. Minting must be controlled and tied to actual asset issuance.
Pattern
function\s+mint\s\([^)]\)\s(?:public|external)\s(?!.only|.require\s\(\smsg\.sender)
Languages
- solidity
Message
CRITICAL: Mint function appears to be permissionless.
Security token minting must be:
- Restricted to authorized issuers
- Tied to actual asset issuance
- Recorded with proper documentation
Fix: Add access control
function mint(address _to, uint256 _amount) external onlyIssuer {
require(identityRegistry.isVerified(_to), "Recipient not verified");
_mint(_to, _amount);
}No Forced Transfer
Id
no-forced-transfer
Severity
critical
Title
Missing Forced Transfer Capability
Description
Securities must support forced transfers for court orders, estate settlements, and regulatory requirements.
Pattern
contract\s+\w+[^{]\{(?:(?!forcedTransfer|forceTransfer|recoverTokens)[^}])\}
Languages
- solidity
File Pattern
Token.sol|SecurityToken.sol|RWA.sol
Message
CRITICAL: Contract lacks forced transfer mechanism.
Legal requirements may mandate token recovery/transfer:
- Court orders
- Estate settlements
- Regulatory seizures
- Lost key recovery
Fix: Implement forced transfer with proper controls
function forcedTransfer(
address _from,
address _to,
uint256 _amount,
bytes32 _legalOrderHash
) external onlyAgent {
require(_legalOrderHash != bytes32(0), "Legal order required");
emit ForcedTransfer(_from, _to, _amount, _legalOrderHash);
_transfer(_from, _to, _amount);
}No Holding Period Check
Id
no-holding-period-check
Severity
high
Title
Missing Holding Period Enforcement
Description
Reg D, Reg S, and Rule 144 require holding periods before resale. Transfers must check holding period compliance.
Pattern
function\s+(?:transfer|canTransfer)\s\([^)]\)[^{]\{(?:(?!holdingPeriod|acquisitionDate|lockup|vestingEnd)[^}])\}
Languages
- solidity
Message
HIGH: Transfer logic lacks holding period enforcement.
Securities laws require holding periods:
- Reg D 506(b/c): 12 months
- Reg S: 40 days (distribution compliance period)
- Rule 144: 6-12 months
Fix: Track and enforce holding periods
require(
block.timestamp - acquisitionDate[_from] >= HOLDING_PERIOD,
"Holding period not met"
);Oracle No Staleness Check
Id
oracle-no-staleness-check
Severity
high
Title
Oracle Without Staleness Check
Description
Oracle data can become stale. Using stale data for asset valuations creates arbitrage opportunities.
Pattern
oracle\.(?:getPrice|getValuation|getValue)\s\([^)]\)(?:(?!lastUpdated|staleness|timestamp|fresh)[^;]*);
Languages
- solidity
Message
HIGH: Oracle call without staleness check.
Stale oracle data is dangerous:
- Arbitrage attacks
- Incorrect NAV calculations
- Wrong dividend amounts
Fix: Always check oracle freshness
(uint256 value, uint256 timestamp) = oracle.getValuation(assetId);
require(block.timestamp - timestamp < STALENESS_THRESHOLD, "Oracle data stale");Kyc No Expiration
Id
kyc-no-expiration
Severity
high
Title
KYC Verification Without Expiration
Description
KYC/accreditation status changes over time. Verifications must have expiration dates and be rechecked.
Pattern
isVerified\s\[\s\w+\s\]\s=\strue\s;(?:(?!expir|validUntil)[^;]*);
Languages
- solidity
Message
HIGH: KYC verification set without expiration.
Investor status changes:
- Accreditation can be lost
- Sanctions lists update
- PEP status changes
Fix: Include expiration in verification
verifications[_investor] = Verification({
isValid: true,
verifiedAt: block.timestamp,
expiresAt: block.timestamp + 90 days
});No Investor Limit Check
Id
no-investor-limit-check
Severity
high
Title
Missing Investor Count Limits
Description
Reg D 506(b) limits non-accredited investors to 35. Section 12(g) triggers at 2000 investors. Must enforce limits.
Pattern
function\s+(?:transfer|onboard|whitelist)\s\([^)]\)[^{]\{(?:(?!investorCount|holderCount|maxInvestors)[^}])\}
Languages
- solidity
File Pattern
Compliance.sol|Token.sol|Whitelist.sol
Message
HIGH: No investor count limit enforcement.
Regulatory limits:
- Reg D 506(b): 35 non-accredited
- Section 12(g): 2000 total triggers registration
Fix: Track and enforce investor limits
if (balanceOf(_to) == 0) {
require(investorCount < MAX_INVESTORS, "Investor limit reached");
investorCount++;
}No Jurisdiction Check
Id
no-jurisdiction-check
Severity
high
Title
Missing Jurisdiction Verification
Description
Different jurisdictions have different rules. Must verify investor jurisdiction and apply appropriate restrictions.
Pattern
function\s+canTransfer\s\([^)]\)[^{]\{(?:(?!country|jurisdiction|region)[^}])\}
Languages
- solidity
Message
HIGH: Compliance check lacks jurisdiction verification.
Jurisdiction matters:
- US: Reg D, Reg S, Reg A+ rules
- EU: MiCA requirements
- Singapore: MAS rules
- Sanctions: OFAC blocked countries
Fix: Check jurisdiction in compliance
uint16 country = identityRegistry.getCountry(_to);
require(!blockedCountries[country], "Jurisdiction blocked");
require(jurisdictionCompliance[country].isCompliant(_to), "Jurisdiction not compliant");Integer Division Dividend
Id
integer-division-dividend
Severity
high
Title
Integer Division in Dividend Calculation
Description
Integer division without precision handling causes rounding errors that accumulate across many investors.
Pattern
(?:dividend|distribution|amount)\s[=/]\s\w+\s\/\s(?:totalSupply|supply|holders)
Languages
- solidity
Message
HIGH: Integer division in dividend calculation.
At scale, rounding errors are material:
- 10,000 investors with $0.01 error each = $100 per distribution
- Compounds over time
Fix: Use high precision with proper rounding
uint256 constant PRECISION = 1e18;
function calculateDividend(address _holder) internal view returns (uint256) {
uint256 preciseAmount = (totalDividend * PRECISION) / totalSupply;
return (preciseAmount * balanceOf(_holder)) / PRECISION;
}No Transfer Event Reason
Id
no-transfer-event-reason
Severity
medium
Title
Transfer Rejection Without Reason
Description
When transfers are rejected, emit specific reason codes for audit trail and debugging.
Pattern
revert\s\(\s\)\s;|require\s\([^,]+\)\s;(?:(?!,)[^;])$
Languages
- solidity
Message
MEDIUM: Transfer rejection lacks reason code.
Audit trails require:
- Why was transfer rejected?
- Which compliance rule failed?
- What can investor do to resolve?
Fix: Include reason in rejection
require(isVerified[_to], "RECIPIENT_NOT_VERIFIED");
require(holdingPeriodMet[_from], "HOLDING_PERIOD_NOT_MET");
require(!sanctioned[_to], "RECIPIENT_SANCTIONED");No Compliance Event
Id
no-compliance-event
Severity
medium
Title
Compliance Action Without Event
Description
All compliance actions should emit events for audit trail. Silent state changes make auditing impossible.
Pattern
function\s+(?:verify|whitelist|blacklist|freeze|unfreeze)\s\([^)]\)[^{]\{(?:(?!emit)[^}])\}
Languages
- solidity
Message
MEDIUM: Compliance action lacks event emission.
Audit requirements:
- All verifications logged
- All freezes with reason
- All compliance changes traceable
Fix: Emit event for compliance actions
function verify(address _investor) external onlyAgent {
isVerified[_investor] = true;
emit InvestorVerified(_investor, msg.sender, block.timestamp);
}Upgrade Without Timelock
Id
upgrade-without-timelock
Severity
medium
Title
Upgradeable Contract Without Timelock
Description
Compliance modules should have timelocks on upgrades to prevent instant rule changes that could harm investors.
Pattern
function\s+(?:upgrade|setCompliance|setModule)\s\([^)]\)\s(?:external|public)[^{]\{(?:(?!timelock|delay)[^}])*\}
Languages
- solidity
Message
MEDIUM: Upgrade function lacks timelock.
Investor protection requires:
- Advance notice of rule changes
- Time to exit if they disagree
- Governance oversight
Fix: Add timelock to upgrades
uint256 public constant UPGRADE_DELAY = 48 hours;
function proposeUpgrade(address _newModule) external onlyOwner {
pendingUpgrade = _newModule;
upgradeTime = block.timestamp + UPGRADE_DELAY;
emit UpgradeProposed(_newModule, upgradeTime);
}
function executeUpgrade() external {
require(block.timestamp >= upgradeTime, "Timelock not expired");
complianceModule = pendingUpgrade;
emit UpgradeExecuted(pendingUpgrade);
}Single Admin Control
Id
single-admin-control
Severity
medium
Title
Single Admin Controls Critical Functions
Description
Critical compliance functions should require multi-sig or governance, not single admin control.
Pattern
modifier\s+onlyOwner|require\s\(\smsg\.sender\s==\sowner\s*\)
Languages
- solidity
File Pattern
Compliance.sol|Identity.sol|Token.sol
Message
MEDIUM: Single admin controls compliance functions.
Risk of:
- Single point of failure
- Insider abuse
- Key compromise
Fix: Use multi-sig or DAO governance
modifier onlyGovernance() {
require(
governance.hasRole(msg.sender, COMPLIANCE_ADMIN),
"Not authorized"
);
_;
}Missing Natspec
Id
missing-natspec
Severity
low
Title
Missing NatSpec Documentation
Description
Compliance functions should have thorough NatSpec documentation for legal review and audit purposes.
Pattern
^\sfunction\s+(?!_)\w+\s\([^)]\)[^{/]\{
Languages
- solidity
File Pattern
Compliance.sol|Token.sol|Identity.sol
Message
LOW: Function lacks NatSpec documentation.
Documentation helps:
- Legal review understanding
- Audit clarity
- Maintenance
Fix: Add NatSpec
/// @notice Verifies an investor's identity and qualification
/// @dev Only callable by authorized verification agents
/// @param _investor Address of the investor to verify
/// @param _country ISO 3166-1 numeric country code
/// @param _qualification Type of investor qualification
function verify(
address _investor,
uint16 _country,
QualificationType _qualification
) external onlyAgent {
// ...
}Magic Numbers
Id
magic-numbers
Severity
low
Title
Magic Numbers in Compliance Logic
Description
Compliance thresholds and limits should be named constants for clarity and maintainability.
Pattern
(?:require|if)\s\([^)](?:365|90|35|2000)\s(?:days|)\s[^)]*\)
Languages
- solidity
Message
LOW: Magic number in compliance logic.
Use named constants for:
- Clarity of intent
- Easy updates
- Audit clarity
Fix: Use named constants
uint256 public constant REG_D_HOLDING_PERIOD = 365 days;
uint256 public constant KYC_VALIDITY_PERIOD = 90 days;
uint256 public constant MAX_NON_ACCREDITED = 35;
uint256 public constant SEC_12G_THRESHOLD = 2000;