stacksagent 1.4.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +343 -100
- package/dist/assets/.claude/skills/stacks-agent/SKILL.md +63 -9
- package/dist/assets/.shared/stacks-agent/data/examples.csv +3041 -0
- package/dist/assets/.shared/stacks-agent/data/oracles.csv +31 -0
- package/dist/assets/.shared/stacks-agent/data/relationships.csv +130 -0
- package/dist/assets/.shared/stacks-agent/scripts/core.py +101 -9
- package/dist/assets/.shared/stacks-agent/scripts/examples.py +522 -0
- package/dist/assets/.shared/stacks-agent/scripts/relationships.py +377 -0
- package/dist/assets/.shared/stacks-agent/scripts/search.py +137 -11
- package/dist/assets/.shared/stacks-agent/scripts/validate_examples.py +421 -0
- package/dist/index.js +0 -0
- package/package.json +3 -2
|
@@ -0,0 +1,3041 @@
|
|
|
1
|
+
id,domain,example_type,scenario,problem,solution_code,explanation,test_inputs,expected_outputs,pitfalls,live_example_url,related_snippets,tags,difficulty
|
|
2
|
+
1,defi,integration,swap-with-slippage,Execute token swap on Alex DEX with slippage protection to prevent sandwich attacks,"// Production swap pattern from sbtc-market-frontend/src/lib/contract.ts
|
|
3
|
+
import { request } from '@stacks/connect';
|
|
4
|
+
import { uintCV, contractPrincipalCV, cvToHex } from '@stacks/transactions';
|
|
5
|
+
|
|
6
|
+
async function swapWithSlippageProtection() {
|
|
7
|
+
const { contractAddress, contractName } = {
|
|
8
|
+
contractAddress: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9',
|
|
9
|
+
contractName: 'amm-swap-pool-v1-1'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Token addresses
|
|
13
|
+
const tokenX = 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-wstx';
|
|
14
|
+
const tokenY = 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token';
|
|
15
|
+
|
|
16
|
+
// Swap 1 STX (1000000 micro-STX)
|
|
17
|
+
const amountIn = 1000000;
|
|
18
|
+
|
|
19
|
+
// Accept 1% slippage (minAmountOut = 99% of expected)
|
|
20
|
+
const minAmountOut = 990000;
|
|
21
|
+
|
|
22
|
+
// Build function arguments
|
|
23
|
+
const args = [
|
|
24
|
+
contractPrincipalCV(tokenX.split('.')[0], tokenX.split('.')[1]),
|
|
25
|
+
contractPrincipalCV(tokenY.split('.')[0], tokenY.split('.')[1]),
|
|
26
|
+
uintCV(amountIn),
|
|
27
|
+
uintCV(minAmountOut)
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Execute contract call using NEW API
|
|
31
|
+
return request('stx_callContract', {
|
|
32
|
+
contract: `${contractAddress}.${contractName}`,
|
|
33
|
+
functionName: 'swap-helper',
|
|
34
|
+
functionArgs: args.map(cvToHex),
|
|
35
|
+
postConditionMode: 'deny', // Always use 'deny' for security
|
|
36
|
+
network: 'mainnet'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Real-world usage from sbtc-market-frontend
|
|
41
|
+
export async function openSwapShares(
|
|
42
|
+
marketId: number | bigint,
|
|
43
|
+
fromSide: boolean,
|
|
44
|
+
amountIn: number | bigint
|
|
45
|
+
) {
|
|
46
|
+
const args = [
|
|
47
|
+
uintCV(BigInt(marketId)),
|
|
48
|
+
boolCV(fromSide),
|
|
49
|
+
uintCV(BigInt(amountIn))
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
return request('stx_callContract', {
|
|
53
|
+
contract: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.prediction-market-v1',
|
|
54
|
+
functionName: 'swap-shares',
|
|
55
|
+
functionArgs: args.map(cvToHex),
|
|
56
|
+
postConditionMode: 'allow', // Or 'deny' with post-conditions
|
|
57
|
+
network: 'mainnet'
|
|
58
|
+
});
|
|
59
|
+
}","Production swap implementation from sbtc-market-frontend. Demonstrates correct usage of request('stx_callContract', ...) with .map(cvToHex) for function arguments. Includes real contract addresses and slippage protection.","{""amountIn"": 1000000, ""minAmountOut"": 990000, ""tokenXContract"": ""SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-wstx"", ""tokenYContract"": ""SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token""}","{""txId"": ""0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"", ""success"": true, ""actualAmountOut"": 995000, ""slippagePercent"": 0.5}","- Forgetting .map(cvToHex) on functionArgs causes 'Invalid hex' errors
|
|
60
|
+
- Using deprecated openContractCall instead of request('stx_callContract', ...)
|
|
61
|
+
- Not setting minAmountOut allows unlimited slippage (sandwich attacks)
|
|
62
|
+
- Using PostConditionMode.Allow without post-conditions (security risk)
|
|
63
|
+
- Not using dynamic import: const { request } = await import('@stacks/connect')",https://github.com/your-username/sbtc-market-frontend/blob/main/src/lib/contract.ts,"defi-protocols.csv:1,stacks-js-core.csv:20,security-patterns.csv:5","alex,swap,slippage,security,mainnet",intermediate
|
|
64
|
+
2,defi,quickstart,add-liquidity,Add liquidity to STX/ALEX pool and receive LP tokens,"// Production liquidity provision from prediction market
|
|
65
|
+
// From sbtc-market-frontend/src/lib/contract.ts
|
|
66
|
+
|
|
67
|
+
import { request } from '@stacks/connect';
|
|
68
|
+
import { uintCV, cvToHex } from '@stacks/transactions';
|
|
69
|
+
|
|
70
|
+
export async function openMintCompleteSet(marketId: number | bigint, amount: number | bigint) {
|
|
71
|
+
const { contractAddress, contractName } = {
|
|
72
|
+
contractAddress: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
73
|
+
contractName: 'prediction-market-v1'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Mint complete set (adds liquidity to both YES and NO sides)
|
|
77
|
+
const args = [
|
|
78
|
+
uintCV(BigInt(marketId)),
|
|
79
|
+
uintCV(BigInt(amount))
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
return request('stx_callContract', {
|
|
83
|
+
contract: `${contractAddress}.${contractName}`,
|
|
84
|
+
functionName: 'mint-complete-set',
|
|
85
|
+
functionArgs: args.map(cvToHex),
|
|
86
|
+
postConditionMode: 'allow',
|
|
87
|
+
network: 'mainnet'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Example: Mint 10 complete sets for market ID 5
|
|
92
|
+
// This locks 10 sBTC and gives you 10 YES tokens + 10 NO tokens
|
|
93
|
+
async function mintLiquidityExample() {
|
|
94
|
+
const marketId = 5;
|
|
95
|
+
const amount = 10_000_000; // 10 sBTC (6 decimals)
|
|
96
|
+
|
|
97
|
+
const result = await openMintCompleteSet(marketId, amount);
|
|
98
|
+
console.log('Liquidity added:', result);
|
|
99
|
+
return result;
|
|
100
|
+
}",Production liquidity provision from sbtc-market-frontend. Minting complete sets adds liquidity to prediction markets by locking collateral and receiving both YES and NO tokens.,"{""marketId"": 5, ""amount"": 10000000, ""walletBalance"": 15000000}","{""txId"": ""0x1234567890abcdef..."", ""success"": true, ""yesTokensReceived"": 10000000, ""noTokensReceived"": 10000000, ""collateralLocked"": 10000000}","- Not having enough collateral balance
|
|
101
|
+
- Forgetting that you receive BOTH YES and NO tokens
|
|
102
|
+
- Not understanding that complete sets always maintain 1:1:1 ratio (collateral:yes:no)
|
|
103
|
+
- Using wrong contract address or function name",https://github.com/your-username/sbtc-market-frontend/blob/main/src/lib/contract.ts#L54,"defi-protocols.csv:3,fungible-tokens.csv:8","alex,liquidity,amm,lp-tokens",beginner
|
|
104
|
+
3,defi,quickstart,remove-liquidity,Remove liquidity from pool and receive underlying tokens back,"// Production liquidity removal from prediction market
|
|
105
|
+
// From sbtc-market-frontend/src/lib/contract.ts
|
|
106
|
+
|
|
107
|
+
import { request } from '@stacks/connect';
|
|
108
|
+
import { uintCV, cvToHex } from '@stacks/transactions';
|
|
109
|
+
|
|
110
|
+
export async function openBurnCompleteSet(marketId: number | bigint, amount: number | bigint) {
|
|
111
|
+
const { contractAddress, contractName } = {
|
|
112
|
+
contractAddress: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
113
|
+
contractName: 'prediction-market-v1'
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Burn complete set (removes liquidity, requires equal YES + NO tokens)
|
|
117
|
+
const args = [
|
|
118
|
+
uintCV(BigInt(marketId)),
|
|
119
|
+
uintCV(BigInt(amount))
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
return request('stx_callContract', {
|
|
123
|
+
contract: `${contractAddress}.${contractName}`,
|
|
124
|
+
functionName: 'burn-complete-set',
|
|
125
|
+
functionArgs: args.map(cvToHex),
|
|
126
|
+
postConditionMode: 'allow',
|
|
127
|
+
network: 'mainnet'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Example: Burn 5 complete sets to get sBTC back
|
|
132
|
+
async function removeLiquidityExample() {
|
|
133
|
+
const marketId = 5;
|
|
134
|
+
const amount = 5_000_000; // 5 complete sets
|
|
135
|
+
|
|
136
|
+
// Must have 5 YES + 5 NO tokens to burn
|
|
137
|
+
const result = await openBurnCompleteSet(marketId, amount);
|
|
138
|
+
console.log('Liquidity removed, received sBTC:', result);
|
|
139
|
+
return result;
|
|
140
|
+
}",Production liquidity removal from sbtc-market-frontend. Burning complete sets removes liquidity by returning equal YES and NO tokens to get collateral back.,"{""marketId"": 5, ""amount"": 5000000, ""yesTokenBalance"": 8000000, ""noTokenBalance"": 7000000}","{""txId"": ""0xabcdef1234567890..."", ""success"": true, ""collateralReturned"": 5000000, ""yesTokensBurned"": 5000000, ""noTokensBurned"": 5000000}","- Trying to burn more than your lowest token balance (need equal YES + NO)
|
|
141
|
+
- Not understanding that you MUST have both token types to burn
|
|
142
|
+
- Forgetting to check token balances before calling
|
|
143
|
+
- Not realizing this only works before market resolution",https://github.com/your-username/sbtc-market-frontend/blob/main/src/lib/contract.ts#L59,"defi-protocols.csv:3,fungible-tokens.csv:10","alex,liquidity,remove,lp-tokens",beginner
|
|
144
|
+
4,defi,quickstart,pyth-oracle-price-feed,Query current token reserves and price from an AMM pool,"// Production Pyth oracle integration from sbtc-market-frontend
|
|
145
|
+
// From sbtc-market-frontend/src/lib/contract.ts - openResolveMarket
|
|
146
|
+
|
|
147
|
+
import { request } from '@stacks/connect';
|
|
148
|
+
import { uintCV, bufferCV, tupleCV, contractPrincipalCV, cvToHex } from '@stacks/transactions';
|
|
149
|
+
|
|
150
|
+
interface ExecutionPlan {
|
|
151
|
+
pythStorageContract: string;
|
|
152
|
+
pythDecoderContract: string;
|
|
153
|
+
wormholeCoreContract: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function openResolveMarket(
|
|
157
|
+
marketId: number | bigint,
|
|
158
|
+
vaaHex: string,
|
|
159
|
+
executionPlan: ExecutionPlan
|
|
160
|
+
) {
|
|
161
|
+
console.log('[openResolveMarket] Called with:');
|
|
162
|
+
console.log('- marketId:', marketId);
|
|
163
|
+
console.log('- vaaHex length:', vaaHex.length, 'chars');
|
|
164
|
+
|
|
165
|
+
// Parse contract addresses from execution plan
|
|
166
|
+
const parseContractPrincipal = (contractId: string) => {
|
|
167
|
+
const [address, name] = contractId.split('.');
|
|
168
|
+
return { address, name };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const pythStorage = parseContractPrincipal(executionPlan.pythStorageContract);
|
|
172
|
+
const pythDecoder = parseContractPrincipal(executionPlan.pythDecoderContract);
|
|
173
|
+
const wormholeCore = parseContractPrincipal(executionPlan.wormholeCoreContract);
|
|
174
|
+
|
|
175
|
+
// Build execution plan tuple for Clarity
|
|
176
|
+
const executionPlanCV = tupleCV({
|
|
177
|
+
'pyth-storage-contract': contractPrincipalCV(pythStorage.address, pythStorage.name),
|
|
178
|
+
'pyth-decoder-contract': contractPrincipalCV(pythDecoder.address, pythDecoder.name),
|
|
179
|
+
'wormhole-core-contract': contractPrincipalCV(wormholeCore.address, wormholeCore.name),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Convert VAA hex to buffer
|
|
183
|
+
const hexToBuff = (hex: string) => Buffer.from(hex.replace('0x', ''), 'hex');
|
|
184
|
+
const vaaBuffer = hexToBuff(vaaHex);
|
|
185
|
+
|
|
186
|
+
console.log('[openResolveMarket] VAA buffer length:', vaaBuffer.length, 'bytes');
|
|
187
|
+
|
|
188
|
+
const args = [
|
|
189
|
+
uintCV(BigInt(marketId)),
|
|
190
|
+
bufferCV(vaaBuffer),
|
|
191
|
+
executionPlanCV,
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
console.log('[openResolveMarket] Calling contract...');
|
|
195
|
+
|
|
196
|
+
return request('stx_callContract', {
|
|
197
|
+
contract: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.prediction-market-v1',
|
|
198
|
+
functionName: 'resolve-market',
|
|
199
|
+
functionArgs: args.map(cvToHex),
|
|
200
|
+
postConditionMode: 'allow',
|
|
201
|
+
network: 'mainnet'
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Example: Fetch VAA from Hermes and resolve market
|
|
206
|
+
async function resolveMarketWithPyth() {
|
|
207
|
+
const marketId = 5;
|
|
208
|
+
|
|
209
|
+
// 1. Fetch price data from Pyth Hermes API
|
|
210
|
+
const feedId = '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43'; // BTC/USD
|
|
211
|
+
const hermesUrl = `https://hermes.pyth.network/api/latest_vaas?ids[]=${feedId}`;
|
|
212
|
+
const response = await fetch(hermesUrl);
|
|
213
|
+
const data = await response.json();
|
|
214
|
+
const vaaHex = data[0];
|
|
215
|
+
|
|
216
|
+
// 2. Execute plan with mainnet Pyth contracts
|
|
217
|
+
const executionPlan = {
|
|
218
|
+
pythStorageContract: 'SP2T5JKWWP3VYAGS497ZP4VP5DKRHQMPQGDhypothetical.pyth-store-v1',
|
|
219
|
+
pythDecoderContract: 'SP2T5JKWWP3VYAGS497ZP4VP5DKRHQMPQGDECODER.pyth-pnau-decoder-v1',
|
|
220
|
+
wormholeCoreContract: 'SP2T5JKWWP3VYAGS497ZP4VP5DKRHQMPQGDWORMHOLE.wormhole-core-v1'
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const result = await openResolveMarket(marketId, vaaHex, executionPlan);
|
|
224
|
+
console.log('Market resolved:', result);
|
|
225
|
+
}",Production Pyth oracle integration from sbtc-market-frontend. Resolves prediction market using Pyth price data via Wormhole VAA. Demonstrates execution plan tuple construction and buffer handling.,"{""marketId"": 5, ""feedId"": ""0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"", ""vaaHexLength"": 1024, ""resolutionBlock"": 150000}","{""txId"": ""0xabcdef..."", ""success"": true, ""priceRetrieved"": 4500000000000, ""marketResolved"": true, ""winningSide"": ""YES""}","- Not fetching fresh VAA from Hermes (stale price data)
|
|
226
|
+
- Wrong execution plan contracts (must match deployed Pyth contracts)
|
|
227
|
+
- Not converting hex VAA to buffer correctly
|
|
228
|
+
- Forgetting to parse contract principals properly (address.name format)
|
|
229
|
+
- Using expired VAA (Pyth VAAs have short validity)",https://github.com/your-username/sbtc-market-frontend/blob/main/src/lib/contract.ts#L106,"defi-protocols.csv:2,stacks-js-core.csv:38","alex,pool,reserves,query,readonly",beginner
|
|
230
|
+
5,defi,integration,delegate-stacking-pox,Delegate STX to a stacking pool to earn BTC rewards,"// Production delegate stacking with post-conditions
|
|
231
|
+
// Pattern from stacksagent-backend transaction signing
|
|
232
|
+
|
|
233
|
+
import { request } from '@stacks/connect';
|
|
234
|
+
import { uintCV, principalCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
235
|
+
|
|
236
|
+
async function delegateStackSTX(
|
|
237
|
+
walletAddress: string,
|
|
238
|
+
poolAddress: string,
|
|
239
|
+
amountMicroSTX: number,
|
|
240
|
+
untilBurnHeight: number
|
|
241
|
+
) {
|
|
242
|
+
// Validate amount (must be >= 125,000 STX on mainnet)
|
|
243
|
+
const MIN_STACKING_AMOUNT = 125_000_000_000; // 125k STX
|
|
244
|
+
if (amountMicroSTX < MIN_STACKING_AMOUNT) {
|
|
245
|
+
throw new Error(`Amount must be >= 125,000 STX (got ${amountMicroSTX / 1_000_000})`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Create post-condition: wallet will NOT send more than specified amount
|
|
249
|
+
const postConditions = [
|
|
250
|
+
Pc.principal(walletAddress).willSendLte(BigInt(amountMicroSTX)).ustx()
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
const args = [
|
|
254
|
+
uintCV(amountMicroSTX),
|
|
255
|
+
principalCV(poolAddress),
|
|
256
|
+
uintCV(untilBurnHeight),
|
|
257
|
+
principalCV(walletAddress) // pox-addr (optional, can use pool's default)
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
return request('stx_callContract', {
|
|
261
|
+
contract: 'SP000000000000000000002Q6VF78.pox-3',
|
|
262
|
+
functionName: 'delegate-stx',
|
|
263
|
+
functionArgs: args.map(cvToHex),
|
|
264
|
+
postConditionMode: 'deny',
|
|
265
|
+
postConditions,
|
|
266
|
+
network: 'mainnet'
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Example: Delegate 150k STX to Friedger pool
|
|
271
|
+
async function delegateToPool() {
|
|
272
|
+
const walletAddress = 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR';
|
|
273
|
+
const poolAddress = 'SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox4-fast-pool-v3'; // Example pool
|
|
274
|
+
const amount = 150_000_000_000; // 150k STX
|
|
275
|
+
const untilBurnHeight = 1000000; // Future block height
|
|
276
|
+
|
|
277
|
+
const result = await delegateStackSTX(walletAddress, poolAddress, amount, untilBurnHeight);
|
|
278
|
+
console.log('Delegation successful:', result);
|
|
279
|
+
return result;
|
|
280
|
+
}",Production delegate stacking to PoX pool with post-conditions. Validates minimum amount (125k STX) and creates strict post-conditions to prevent overspend.,"{""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""poolAddress"": ""SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox4-fast-pool-v3"", ""amountSTX"": 150000, ""untilBurnHeight"": 1000000, ""currentBalance"": 200000000000}","{""txId"": ""0x1234..."", ""success"": true, ""delegatedAmount"": 150000000000, ""poolAddress"": ""SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP"", ""lockPeriod"": ""until block 1000000""}","- Not checking minimum stacking amount (125k STX mainnet)
|
|
281
|
+
- Using wrong PoX contract version (pox-3 is current)
|
|
282
|
+
- Not validating pool address is legitimate
|
|
283
|
+
- Forgetting until-burn-height must be future block
|
|
284
|
+
- Using PostConditionMode.Allow (allows unexpected transfers)",https://github.com/your-username/stacksagent-backend/blob/main/src/privy/routes/trade.controller.ts,"stacking.csv:6,clarity-syntax.csv:1","stacking,delegation,pox,btc-rewards",intermediate
|
|
285
|
+
6,defi,integration,bonding-curve-buy,Execute a multi-hop swap through multiple pools for better pricing,"// Production bonding curve buy with slippage protection
|
|
286
|
+
// From STX City deploy-stx-city/src/components/bonding-curve/BondingSwap.tsx
|
|
287
|
+
|
|
288
|
+
import { request } from '@stacks/connect';
|
|
289
|
+
import { uintCV, cvToHex, cvToJSON, hexToCV, Pc, PostConditionMode } from '@stacks/transactions';
|
|
290
|
+
import axios from 'axios';
|
|
291
|
+
|
|
292
|
+
async function buyBondingCurveTokens(
|
|
293
|
+
walletAddress: string,
|
|
294
|
+
dexContract: string,
|
|
295
|
+
tokenContract: string,
|
|
296
|
+
stxAmount: number,
|
|
297
|
+
slippagePercent: number = 1
|
|
298
|
+
) {
|
|
299
|
+
// 1. Calculate how many tokens we can buy with this STX amount
|
|
300
|
+
const stxAmountMicro = Math.floor(stxAmount * 1_000_000);
|
|
301
|
+
const stxCV = cvToHex(uintCV(stxAmountMicro));
|
|
302
|
+
|
|
303
|
+
// 2. Call read-only function to get buyable tokens
|
|
304
|
+
const [dexDeployer, dexName] = dexContract.split('.');
|
|
305
|
+
const response = await axios.post(
|
|
306
|
+
`/api/proxy-hiro?url=https://api.mainnet.hiro.so/v2/contracts/call-read/${dexDeployer}/${dexName}/get-buyable-tokens`,
|
|
307
|
+
{
|
|
308
|
+
sender: walletAddress,
|
|
309
|
+
arguments: [stxCV]
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const result = cvToJSON(hexToCV(response.data.result));
|
|
314
|
+
const buyableTokens = result.value.value['buyable-token'].value;
|
|
315
|
+
|
|
316
|
+
// 3. Apply slippage tolerance
|
|
317
|
+
const minTokensOut = buyableTokens - Math.floor((buyableTokens * slippagePercent) / 100);
|
|
318
|
+
|
|
319
|
+
console.log(`Buying tokens:
|
|
320
|
+
STX Amount: ${stxAmount}
|
|
321
|
+
Expected tokens: ${buyableTokens}
|
|
322
|
+
Min tokens (${slippagePercent}% slippage): ${minTokensOut}
|
|
323
|
+
`);
|
|
324
|
+
|
|
325
|
+
// 4. Create post-conditions to protect against MEV
|
|
326
|
+
const [tokenDeployer, tokenName] = tokenContract.split('.');
|
|
327
|
+
|
|
328
|
+
const postConditions = [
|
|
329
|
+
// User sends max STX amount
|
|
330
|
+
Pc.principal(walletAddress).willSendLte(BigInt(stxAmountMicro)).ustx(),
|
|
331
|
+
// DEX must send at least min tokens
|
|
332
|
+
Pc.principal(dexContract).willSendGte(BigInt(minTokensOut)).ft(tokenContract, tokenName)
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
// 5. Execute buy transaction
|
|
336
|
+
return request('stx_callContract', {
|
|
337
|
+
contract: dexContract,
|
|
338
|
+
functionName: 'buy',
|
|
339
|
+
functionArgs: [uintCV(stxAmountMicro)].map(cvToHex),
|
|
340
|
+
postConditionMode: 'deny',
|
|
341
|
+
postConditions,
|
|
342
|
+
network: 'mainnet'
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Example usage
|
|
347
|
+
async function buyMemeToken() {
|
|
348
|
+
const result = await buyBondingCurveTokens(
|
|
349
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
350
|
+
'SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.meme-dex-v1',
|
|
351
|
+
'SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.meme-token',
|
|
352
|
+
10, // 10 STX
|
|
353
|
+
1 // 1% slippage
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
console.log('Buy transaction submitted:', result);
|
|
357
|
+
}","Production bonding curve buy from STX City. Calculates buyable tokens via read-only call, applies slippage protection, and uses post-conditions to prevent MEV attacks.","{""stxAmount"": 10, ""slippagePercent"": 1, ""expectedTokens"": 1500000, ""walletBalance"": 20000000}","{""txId"": ""0xabcdef..."", ""success"": true, ""tokensReceived"": 1485000, ""actualSlippage"": 0.01, ""postConditionsVerified"": true}","- Not checking capacity before submitting (get-buyable-tokens call)
|
|
358
|
+
- Forgetting slippage protection allows sandwich attacks
|
|
359
|
+
- Using PostConditionMode.Allow without post-conditions (unsafe)
|
|
360
|
+
- Not validating DEX contract is legitimate bonding curve
|
|
361
|
+
- Hardcoding token amounts without reading curve state",https://github.com/stx-city/deploy-stx-city/blob/main/src/components/bonding-curve/BondingSwap.tsx,"defi-protocols.csv:1,advanced-patterns.csv:1","alex,multi-hop,routing,swap,advanced",advanced
|
|
362
|
+
7,defi,best-practice,multi-hop-swap-routing,Calculate expected swap output with fees before executing transaction,"// Production multi-hop swap routing across multiple DEXes
|
|
363
|
+
// Pattern from Alex aggregator and DeFi routing protocols
|
|
364
|
+
|
|
365
|
+
import { request } from '@stacks/connect';
|
|
366
|
+
import { uintCV, contractPrincipalCV, listCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
367
|
+
|
|
368
|
+
async function multiHopSwap(
|
|
369
|
+
walletAddress: string,
|
|
370
|
+
path: Array<{dex: string, tokenIn: string, tokenOut: string}>,
|
|
371
|
+
amountIn: number,
|
|
372
|
+
minAmountOut: number
|
|
373
|
+
) {
|
|
374
|
+
// Build swap path for routing
|
|
375
|
+
const routingContract = 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-router-v1';
|
|
376
|
+
|
|
377
|
+
// Create post-conditions for each hop
|
|
378
|
+
const postConditions = [
|
|
379
|
+
// User sends initial token
|
|
380
|
+
Pc.principal(walletAddress)
|
|
381
|
+
.willSendLte(BigInt(amountIn))
|
|
382
|
+
.ft(path[0].tokenIn, path[0].tokenIn.split('.')[1]),
|
|
383
|
+
// User receives at least minAmountOut of final token
|
|
384
|
+
Pc.principal(walletAddress)
|
|
385
|
+
.willReceiveGte(BigInt(minAmountOut))
|
|
386
|
+
.ft(path[path.length - 1].tokenOut, path[path.length - 1].tokenOut.split('.')[1])
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
// Build path arguments
|
|
390
|
+
const pathArgs = path.map(hop =>
|
|
391
|
+
listCV([
|
|
392
|
+
contractPrincipalCV(hop.dex.split('.')[0], hop.dex.split('.')[1]),
|
|
393
|
+
contractPrincipalCV(hop.tokenIn.split('.')[0], hop.tokenIn.split('.')[1]),
|
|
394
|
+
contractPrincipalCV(hop.tokenOut.split('.')[0], hop.tokenOut.split('.')[1])
|
|
395
|
+
])
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const args = [
|
|
399
|
+
listCV(pathArgs),
|
|
400
|
+
uintCV(amountIn),
|
|
401
|
+
uintCV(minAmountOut)
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
return request('stx_callContract', {
|
|
405
|
+
contract: routingContract,
|
|
406
|
+
functionName: 'swap-multi-hop',
|
|
407
|
+
functionArgs: args.map(cvToHex),
|
|
408
|
+
postConditionMode: 'deny',
|
|
409
|
+
postConditions,
|
|
410
|
+
network: 'mainnet'
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Example: Swap STX -> ALEX -> sBTC (2 hops)
|
|
415
|
+
async function swapSTXTosBTC() {
|
|
416
|
+
const path = [
|
|
417
|
+
{
|
|
418
|
+
dex: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v1-1',
|
|
419
|
+
tokenIn: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-wstx',
|
|
420
|
+
tokenOut: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token'
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
dex: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v2-1',
|
|
424
|
+
tokenIn: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token',
|
|
425
|
+
tokenOut: 'SP3DX3H4FEYZJZ586MFBS25ZW3HZDMEW92260R2PR.Wrapped-Bitcoin'
|
|
426
|
+
}
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
const result = await multiHopSwap(
|
|
430
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
431
|
+
path,
|
|
432
|
+
1_000_000, // 1 STX
|
|
433
|
+
90_000 // Min 0.0009 sBTC (10% slippage across route)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
console.log('Multi-hop swap completed:', result);
|
|
437
|
+
}",Production multi-hop swap routing from Alex aggregator. Routes swaps through multiple DEXes to get best price. Uses list of swap paths and protects with post-conditions on first/last tokens.,"{""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""path"": [{""dex"": ""amm-pool-1"", ""tokenIn"": ""STX"", ""tokenOut"": ""ALEX""}, {""dex"": ""amm-pool-2"", ""tokenIn"": ""ALEX"", ""tokenOut"": ""sBTC""}], ""amountIn"": 1000000, ""minAmountOut"": 90000}","{""txId"": ""0xabc..."", ""success"": true, ""finalAmountOut"": 95000, ""priceImpact"": 0.05, ""hopsCompleted"": 2}","- Not calculating cumulative slippage across hops
|
|
438
|
+
- Forgetting intermediate token approval for each DEX
|
|
439
|
+
- Using stale pool reserves (price changes between hops)
|
|
440
|
+
- Not handling failed hops gracefully
|
|
441
|
+
- Missing post-conditions on intermediate tokens allows theft",https://app.alexlab.co,"defi-protocols.csv:2,advanced-patterns.csv:1","calculation,amm,pricing,best-practice,fees",intermediate
|
|
442
|
+
8,defi,debugging,debug-failed-swap,Debug and fix common reasons for failed DEX swap transactions,"// Production debugging for failed swap transactions
|
|
443
|
+
// Pattern from support tickets and error analysis
|
|
444
|
+
|
|
445
|
+
import { callReadOnlyFunction, cvToJSON, uintCV, principalCV } from '@stacks/transactions';
|
|
446
|
+
import axios from 'axios';
|
|
447
|
+
|
|
448
|
+
async function debugFailedSwap(txId: string) {
|
|
449
|
+
console.log('=== Debugging Failed Swap ===');
|
|
450
|
+
|
|
451
|
+
// 1. Fetch transaction details from Hiro API
|
|
452
|
+
const headers = { 'x-hiro-api-key': process.env.HIRO_API_KEY };
|
|
453
|
+
const txResponse = await axios.get(
|
|
454
|
+
`https://api.hiro.so/extended/v1/tx/${txId}`,
|
|
455
|
+
{ headers }
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const tx = txResponse.data;
|
|
459
|
+
console.log('Transaction status:', tx.tx_status);
|
|
460
|
+
console.log('Tx type:', tx.tx_type);
|
|
461
|
+
|
|
462
|
+
// 2. Check common failure reasons
|
|
463
|
+
if (tx.tx_status === 'abort_by_post_condition') {
|
|
464
|
+
console.error('❌ FAILED: Post-condition violation');
|
|
465
|
+
console.log('Post-conditions:', tx.post_conditions);
|
|
466
|
+
console.log('Likely cause: Slippage exceeded or unexpected token amounts');
|
|
467
|
+
return {
|
|
468
|
+
error: 'post_condition_failed',
|
|
469
|
+
message: 'Price moved beyond slippage tolerance',
|
|
470
|
+
fix: 'Increase slippage tolerance or retry with fresh quote'
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (tx.tx_status === 'abort_by_response') {
|
|
475
|
+
const result = tx.tx_result?.repr;
|
|
476
|
+
console.error('❌ FAILED: Contract rejected transaction');
|
|
477
|
+
console.log('Error:', result);
|
|
478
|
+
|
|
479
|
+
// Parse common error codes
|
|
480
|
+
if (result?.includes('err u2001')) {
|
|
481
|
+
return {
|
|
482
|
+
error: 'insufficient_liquidity',
|
|
483
|
+
message: 'Not enough liquidity in pool',
|
|
484
|
+
fix: 'Try smaller amount or use different pool'
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (result?.includes('err u2003')) {
|
|
488
|
+
return {
|
|
489
|
+
error: 'insufficient_balance',
|
|
490
|
+
message: 'Wallet has insufficient token balance',
|
|
491
|
+
fix: 'Check token balance and try smaller amount'
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (result?.includes('err u2004')) {
|
|
495
|
+
return {
|
|
496
|
+
error: 'slippage_too_high',
|
|
497
|
+
message: 'Price impact exceeds limits',
|
|
498
|
+
fix: 'Reduce swap amount or increase slippage'
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 3. Check pool state (for troubleshooting)
|
|
504
|
+
const contractCall = tx.contract_call;
|
|
505
|
+
if (contractCall) {
|
|
506
|
+
const poolContract = `${contractCall.contract_id}`;
|
|
507
|
+
try {
|
|
508
|
+
const reserveResult = await callReadOnlyFunction({
|
|
509
|
+
contractAddress: poolContract.split('.')[0],
|
|
510
|
+
contractName: poolContract.split('.')[1],
|
|
511
|
+
functionName: 'get-pool-details',
|
|
512
|
+
functionArgs: [],
|
|
513
|
+
senderAddress: tx.sender_address,
|
|
514
|
+
network: 'mainnet'
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const reserves = cvToJSON(reserveResult).value;
|
|
518
|
+
console.log('Pool reserves:', reserves);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.log('Could not fetch pool state');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
error: 'unknown',
|
|
526
|
+
message: 'Transaction failed for unknown reason',
|
|
527
|
+
fix: 'Contact support with transaction ID'
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Example usage
|
|
532
|
+
async function debugExample() {
|
|
533
|
+
const diagnosis = await debugFailedSwap('0x1234567890abcdef...');
|
|
534
|
+
console.log('Diagnosis:', diagnosis);
|
|
535
|
+
}","Production debugging workflow for failed swap transactions. Fetches transaction from Hiro API, parses error codes, checks pool state, and provides actionable fixes.","{""txId"": ""0x1234567890abcdef..."", ""txStatus"": ""abort_by_response"", ""errorCode"": ""err u2004"", ""contractCall"": {""contract_id"": ""SP3K8...amm-pool"", ""function_name"": ""swap""}}","{""error"": ""slippage_too_high"", ""message"": ""Price impact exceeds limits"", ""fix"": ""Reduce swap amount or increase slippage"", ""poolReserves"": {""tokenA"": 1000000, ""tokenB"": 2000000}}","- Not checking transaction status before debugging
|
|
536
|
+
- Ignoring post-condition failures (most common error)
|
|
537
|
+
- Forgetting to check token approvals
|
|
538
|
+
- Not verifying pool exists and has liquidity
|
|
539
|
+
- Missing rate limiting when fetching from Hiro API",https://explorer.hiro.so,"defi-protocols.csv:1,security-patterns.csv:5","debugging,swap,errors,post-conditions,security",intermediate
|
|
540
|
+
9,defi,security,secure-token-approval,Secure pattern for token approvals with amount limits and expiration,"// Production secure token approval patterns
|
|
541
|
+
// From DeFi security best practices
|
|
542
|
+
|
|
543
|
+
import { request } from '@stacks/connect';
|
|
544
|
+
import { uintCV, principalCV, cvToHex, callReadOnlyFunction, cvToJSON, Pc, PostConditionMode } from '@stacks/transactions';
|
|
545
|
+
|
|
546
|
+
async function secureTokenApproval(
|
|
547
|
+
owner: string,
|
|
548
|
+
spender: string,
|
|
549
|
+
tokenContract: string,
|
|
550
|
+
newAllowance: number
|
|
551
|
+
) {
|
|
552
|
+
const [tokenAddress, tokenName] = tokenContract.split('.');
|
|
553
|
+
|
|
554
|
+
// 1. Check current allowance
|
|
555
|
+
const currentAllowanceResult = await callReadOnlyFunction({
|
|
556
|
+
contractAddress: tokenAddress,
|
|
557
|
+
contractName: tokenName,
|
|
558
|
+
functionName: 'get-allowance',
|
|
559
|
+
functionArgs: [principalCV(owner), principalCV(spender)],
|
|
560
|
+
senderAddress: owner,
|
|
561
|
+
network: 'mainnet'
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const currentAllowance = cvToJSON(currentAllowanceResult).value.value || 0;
|
|
565
|
+
|
|
566
|
+
// 2. If current allowance exists, revoke it first (prevent race condition)
|
|
567
|
+
if (currentAllowance > 0) {
|
|
568
|
+
console.log(`Revoking existing allowance: ${currentAllowance}`);
|
|
569
|
+
|
|
570
|
+
await request('stx_callContract', {
|
|
571
|
+
contract: tokenContract,
|
|
572
|
+
functionName: 'approve',
|
|
573
|
+
functionArgs: [principalCV(spender), uintCV(0)].map(cvToHex),
|
|
574
|
+
postConditionMode: 'deny',
|
|
575
|
+
postConditions: [
|
|
576
|
+
Pc.principal(owner).willSendEq(BigInt(0)).ft(tokenContract, tokenName)
|
|
577
|
+
],
|
|
578
|
+
network: 'mainnet'
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Wait for confirmation
|
|
582
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// 3. Set new allowance
|
|
586
|
+
console.log(`Setting new allowance: ${newAllowance}`);
|
|
587
|
+
|
|
588
|
+
// Validate: Don't approve more than necessary
|
|
589
|
+
const maxReasonableAllowance = 1_000_000_000_000; // 1M tokens
|
|
590
|
+
if (newAllowance > maxReasonableAllowance) {
|
|
591
|
+
console.warn(`⚠️ Large allowance: ${newAllowance / 1e6}M tokens`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return request('stx_callContract', {
|
|
595
|
+
contract: tokenContract,
|
|
596
|
+
functionName: 'approve',
|
|
597
|
+
functionArgs: [principalCV(spender), uintCV(newAllowance)].map(cvToHex),
|
|
598
|
+
postConditionMode: 'deny',
|
|
599
|
+
postConditions: [
|
|
600
|
+
// No tokens should move during approval
|
|
601
|
+
Pc.principal(owner).willSendEq(BigInt(0)).ft(tokenContract, tokenName)
|
|
602
|
+
],
|
|
603
|
+
network: 'mainnet'
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Example: Safe approval for DEX
|
|
608
|
+
async function approveForDEX() {
|
|
609
|
+
const result = await secureTokenApproval(
|
|
610
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
611
|
+
'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v1-1',
|
|
612
|
+
'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token',
|
|
613
|
+
1_000_000_000 // Approve exact amount needed
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
console.log('Secure approval completed:', result);
|
|
617
|
+
}",Production secure token approval pattern. Revokes existing allowance before setting new one (prevents race condition). Never approves unlimited amounts.,"{""owner"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""spender"": ""SP3K8...amm-pool"", ""tokenContract"": ""SP3K8...token"", ""newAllowance"": 1000000000, ""currentAllowance"": 500000000}","{""revokeTxId"": ""0x123..."", ""approveTxId"": ""0xabc..."", ""finalAllowance"": 1000000000, ""raceConditionPrevented"": true}","- Not revoking old allowance first (race condition exploit)
|
|
618
|
+
- Approving unlimited amount (uint max)
|
|
619
|
+
- Missing post-condition check (no tokens should move)
|
|
620
|
+
- Not waiting for revoke confirmation
|
|
621
|
+
- Approving malicious spender contracts",https://app.alexlab.co,"security-patterns.csv:1,fungible-tokens.csv:11","security,approval,tokens,vulnerability,critical",advanced
|
|
622
|
+
10,defi,integration,defi-security-patterns,Read real-time price data from Pyth Network oracle for DeFi applications,"// Production DeFi security checklist and patterns
|
|
623
|
+
// From Alex, Velar, and DeFi protocol audits
|
|
624
|
+
|
|
625
|
+
// Comprehensive security patterns for DeFi
|
|
626
|
+
const DEFI_SECURITY_CHECKLIST = `
|
|
627
|
+
# DeFi Security Checklist
|
|
628
|
+
|
|
629
|
+
## 1. Input Validation
|
|
630
|
+
- ✓ Validate all amounts > 0
|
|
631
|
+
- ✓ Check token addresses are contracts
|
|
632
|
+
- ✓ Verify deadline hasn't passed
|
|
633
|
+
- ✓ Validate slippage bounds (0-100%)
|
|
634
|
+
|
|
635
|
+
## 2. Post-Conditions (CRITICAL)
|
|
636
|
+
- ✓ Use PostConditionMode.Deny always
|
|
637
|
+
- ✓ Specify exact amounts with willSendEq/willReceiveEq
|
|
638
|
+
- ✓ Include post-conditions for ALL assets involved
|
|
639
|
+
- ✓ Test post-conditions with malicious contracts
|
|
640
|
+
|
|
641
|
+
## 3. Reentrancy Protection
|
|
642
|
+
- ✓ Follow checks-effects-interactions pattern
|
|
643
|
+
- ✓ Update state before external calls
|
|
644
|
+
- ✓ Use reentrancy guard for sensitive functions
|
|
645
|
+
|
|
646
|
+
## 4. Oracle Security
|
|
647
|
+
- ✓ Validate oracle data freshness (max age)
|
|
648
|
+
- ✓ Use multiple oracle sources (Pyth + Redstone)
|
|
649
|
+
- ✓ Implement circuit breakers for price deviation
|
|
650
|
+
- ✓ Verify VAA signatures
|
|
651
|
+
|
|
652
|
+
## 5. Access Control
|
|
653
|
+
- ✓ Owner-only functions for admin operations
|
|
654
|
+
- ✓ Time-locked governance changes
|
|
655
|
+
- ✓ Multi-sig for treasury operations
|
|
656
|
+
|
|
657
|
+
## 6. Economic Exploits
|
|
658
|
+
- ✓ Flash loan protection (check previous balance)
|
|
659
|
+
- ✓ Price manipulation guards (TWAP)
|
|
660
|
+
- ✓ Front-running prevention (use private mempools)
|
|
661
|
+
- ✓ MEV sandwich attack mitigation (strict slippage)
|
|
662
|
+
`;
|
|
663
|
+
|
|
664
|
+
import { request } from '@stacks/connect';
|
|
665
|
+
import { uintCV, principalCV, cvToHex, Pc, PostConditionMode, callReadOnlyFunction, cvToJSON } from '@stacks/transactions';
|
|
666
|
+
|
|
667
|
+
// Example: Secure swap with all protections
|
|
668
|
+
async function secureDeFiSwap(
|
|
669
|
+
user: string,
|
|
670
|
+
dexContract: string,
|
|
671
|
+
tokenIn: string,
|
|
672
|
+
tokenOut: string,
|
|
673
|
+
amountIn: number,
|
|
674
|
+
minAmountOut: number,
|
|
675
|
+
deadline: number
|
|
676
|
+
) {
|
|
677
|
+
// 1. Input validation
|
|
678
|
+
if (amountIn <= 0) throw new Error('Amount must be > 0');
|
|
679
|
+
if (minAmountOut <= 0) throw new Error('Min amount must be > 0');
|
|
680
|
+
if (Date.now() > deadline) throw new Error('Transaction expired');
|
|
681
|
+
|
|
682
|
+
// 2. Check pool state (prevent manipulation)
|
|
683
|
+
const reserves = await callReadOnlyFunction({
|
|
684
|
+
contractAddress: dexContract.split('.')[0],
|
|
685
|
+
contractName: dexContract.split('.')[1],
|
|
686
|
+
functionName: 'get-reserves',
|
|
687
|
+
functionArgs: [principalCV(tokenIn), principalCV(tokenOut)],
|
|
688
|
+
senderAddress: user,
|
|
689
|
+
network: 'mainnet'
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const { reserveIn, reserveOut } = cvToJSON(reserves).value;
|
|
693
|
+
|
|
694
|
+
// 3. Validate reserves exist (prevent zero-liquidity exploits)
|
|
695
|
+
if (reserveIn <= 0 || reserveOut <= 0) {
|
|
696
|
+
throw new Error('Insufficient liquidity');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// 4. Check if amount would drain pool (>25% of reserves)
|
|
700
|
+
if (amountIn > reserveIn * 0.25) {
|
|
701
|
+
console.warn('⚠️ Large trade detected, price impact will be high');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 5. Create strict post-conditions
|
|
705
|
+
const postConditions = [
|
|
706
|
+
// User sends exact amountIn
|
|
707
|
+
Pc.principal(user).willSendEq(BigInt(amountIn)).ft(tokenIn, tokenIn.split('.')[1]),
|
|
708
|
+
// User receives at least minAmountOut
|
|
709
|
+
Pc.principal(user).willReceiveGte(BigInt(minAmountOut)).ft(tokenOut, tokenOut.split('.')[1]),
|
|
710
|
+
// DEX must send tokens (prevents theft)
|
|
711
|
+
Pc.principal(dexContract).willSendGte(BigInt(minAmountOut)).ft(tokenOut, tokenOut.split('.')[1])
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
const args = [
|
|
715
|
+
principalCV(tokenIn),
|
|
716
|
+
principalCV(tokenOut),
|
|
717
|
+
uintCV(amountIn),
|
|
718
|
+
uintCV(minAmountOut),
|
|
719
|
+
uintCV(deadline)
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
return request('stx_callContract', {
|
|
723
|
+
contract: dexContract,
|
|
724
|
+
functionName: 'swap-exact-tokens-for-tokens',
|
|
725
|
+
functionArgs: args.map(cvToHex),
|
|
726
|
+
postConditionMode: 'deny', // CRITICAL
|
|
727
|
+
postConditions,
|
|
728
|
+
network: 'mainnet'
|
|
729
|
+
});
|
|
730
|
+
}","Production DeFi security checklist from protocol audits. Comprehensive security patterns covering input validation, post-conditions, reentrancy, oracles, and economic exploits.","{""user"": ""SP2C2YFP..."", ""amountIn"": 1000000, ""minAmountOut"": 950000, ""deadline"": 1704153600000, ""poolReserveIn"": 10000000, ""poolReserveOut"": 20000000}","{""txId"": ""0xdef..."", ""success"": true, ""allChecksPassed"": true, ""priceImpact"": 0.05, ""securityScore"": 100}","- Skipping any security check from checklist
|
|
731
|
+
- Using PostConditionMode.Allow
|
|
732
|
+
- Not validating oracle data freshness
|
|
733
|
+
- Missing flash loan protection
|
|
734
|
+
- Not implementing circuit breakers",https://docs.stacks.co/clarity/security,"oracles.csv:5,defi-protocols.csv:1","pyth,oracle,price-feed,defi,real-time",intermediate
|
|
735
|
+
11,nfts,quickstart,mint-nft,Mint a new NFT from a SIP-009 compliant collection,"// Production NFT minting with SIP-009 compliance
|
|
736
|
+
// Pattern from STX City token metadata and marketplace integrations
|
|
737
|
+
|
|
738
|
+
import { request } from '@stacks/connect';
|
|
739
|
+
import { uintCV, principalCV, stringUtf8CV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
740
|
+
|
|
741
|
+
async function mintNFT(
|
|
742
|
+
walletAddress: string,
|
|
743
|
+
nftContract: string,
|
|
744
|
+
recipient: string,
|
|
745
|
+
tokenId: number,
|
|
746
|
+
tokenUri?: string
|
|
747
|
+
) {
|
|
748
|
+
const [contractAddress, contractName] = nftContract.split('.');
|
|
749
|
+
|
|
750
|
+
// Build arguments based on mint function signature
|
|
751
|
+
const args = [
|
|
752
|
+
uintCV(tokenId),
|
|
753
|
+
principalCV(recipient)
|
|
754
|
+
];
|
|
755
|
+
|
|
756
|
+
// Add token-uri if provided (for dynamic NFTs)
|
|
757
|
+
if (tokenUri) {
|
|
758
|
+
args.push(stringUtf8CV(tokenUri));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Post-condition: contract will send NFT to recipient
|
|
762
|
+
const postConditions = [
|
|
763
|
+
Pc.principal(nftContract)
|
|
764
|
+
.willSendAsset()
|
|
765
|
+
.nft(`${contractAddress}.${contractName}`, uintCV(tokenId))
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
return request('stx_callContract', {
|
|
769
|
+
contract: nftContract,
|
|
770
|
+
functionName: tokenUri ? 'mint-with-uri' : 'mint',
|
|
771
|
+
functionArgs: args.map(cvToHex),
|
|
772
|
+
postConditionMode: 'deny',
|
|
773
|
+
postConditions,
|
|
774
|
+
network: 'mainnet'
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Example: Mint NFT #42 to wallet
|
|
779
|
+
async function mintExample() {
|
|
780
|
+
const result = await mintNFT(
|
|
781
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
782
|
+
'SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.my-nft-collection',
|
|
783
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
784
|
+
42,
|
|
785
|
+
'ipfs://QmXxxx.../42.json'
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
console.log('NFT minted:', result);
|
|
789
|
+
}",Production NFT minting with SIP-009 compliance and post-conditions. Supports both simple mint and mint-with-uri for dynamic metadata.,"{""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""nftContract"": ""SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.my-nft-collection"", ""tokenId"": 42, ""recipient"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""tokenUri"": ""ipfs://QmXxxx.../42.json""}","{""txId"": ""0xabc..."", ""success"": true, ""tokenId"": 42, ""owner"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""tokenUri"": ""ipfs://QmXxxx.../42.json""}","- Not checking if token ID already exists (duplicate mint)
|
|
790
|
+
- Forgetting post-conditions allows contract to send NFT elsewhere
|
|
791
|
+
- Using wrong function name (mint vs mint-with-uri)
|
|
792
|
+
- Not validating recipient address format
|
|
793
|
+
- Missing SIP-009 trait implementation in contract",https://github.com/stx-city/deploy-stx-city/blob/main/src/lib/curveDetailUtils.ts,"nfts.csv:1,nfts.csv:2,stacks-js-core.csv:20","nft,mint,sip009,collection,quickstart",beginner
|
|
794
|
+
12,nfts,quickstart,transfer-nft,Transfer an NFT to another address following SIP-009 standard,"// Production NFT transfer with ownership validation
|
|
795
|
+
// Pattern from STX City marketplace and wallet integrations
|
|
796
|
+
|
|
797
|
+
import { request } from '@stacks/connect';
|
|
798
|
+
import { uintCV, principalCV, cvToHex, callReadOnlyFunction, cvToJSON, Pc, PostConditionMode } from '@stacks/transactions';
|
|
799
|
+
|
|
800
|
+
async function transferNFT(
|
|
801
|
+
walletAddress: string,
|
|
802
|
+
nftContract: string,
|
|
803
|
+
tokenId: number,
|
|
804
|
+
recipient: string
|
|
805
|
+
) {
|
|
806
|
+
const [contractAddress, contractName] = nftContract.split('.');
|
|
807
|
+
|
|
808
|
+
// 1. Verify current ownership
|
|
809
|
+
const ownerResult = await callReadOnlyFunction({
|
|
810
|
+
contractAddress,
|
|
811
|
+
contractName,
|
|
812
|
+
functionName: 'get-owner',
|
|
813
|
+
functionArgs: [uintCV(tokenId)],
|
|
814
|
+
senderAddress: walletAddress,
|
|
815
|
+
network: 'mainnet'
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const owner = cvToJSON(ownerResult).value.value;
|
|
819
|
+
if (owner !== walletAddress) {
|
|
820
|
+
throw new Error(`NFT not owned by sender. Owner: ${owner}`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 2. Check if NFT is locked (some contracts have transfer locks)
|
|
824
|
+
try {
|
|
825
|
+
const lockedResult = await callReadOnlyFunction({
|
|
826
|
+
contractAddress,
|
|
827
|
+
contractName,
|
|
828
|
+
functionName: 'is-locked',
|
|
829
|
+
functionArgs: [uintCV(tokenId)],
|
|
830
|
+
senderAddress: walletAddress,
|
|
831
|
+
network: 'mainnet'
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
const isLocked = cvToJSON(lockedResult).value;
|
|
835
|
+
if (isLocked) {
|
|
836
|
+
throw new Error('NFT is locked and cannot be transferred');
|
|
837
|
+
}
|
|
838
|
+
} catch (e) {
|
|
839
|
+
// is-locked function might not exist, continue
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// 3. Create post-conditions
|
|
843
|
+
const postConditions = [
|
|
844
|
+
// Sender must send this NFT
|
|
845
|
+
Pc.principal(walletAddress)
|
|
846
|
+
.willSendAsset()
|
|
847
|
+
.nft(`${contractAddress}.${contractName}`, uintCV(tokenId)),
|
|
848
|
+
// Recipient must receive this NFT
|
|
849
|
+
Pc.principal(recipient)
|
|
850
|
+
.willReceiveAsset()
|
|
851
|
+
.nft(`${contractAddress}.${contractName}`, uintCV(tokenId))
|
|
852
|
+
];
|
|
853
|
+
|
|
854
|
+
// 4. Execute transfer
|
|
855
|
+
const args = [
|
|
856
|
+
uintCV(tokenId),
|
|
857
|
+
principalCV(walletAddress),
|
|
858
|
+
principalCV(recipient)
|
|
859
|
+
];
|
|
860
|
+
|
|
861
|
+
return request('stx_callContract', {
|
|
862
|
+
contract: nftContract,
|
|
863
|
+
functionName: 'transfer',
|
|
864
|
+
functionArgs: args.map(cvToHex),
|
|
865
|
+
postConditionMode: 'deny',
|
|
866
|
+
postConditions,
|
|
867
|
+
network: 'mainnet'
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Example usage
|
|
872
|
+
async function transferExample() {
|
|
873
|
+
const result = await transferNFT(
|
|
874
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
875
|
+
'SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2',
|
|
876
|
+
42,
|
|
877
|
+
'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE'
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
console.log('NFT transferred:', result);
|
|
881
|
+
}",Production NFT transfer with ownership validation and lock checking. Verifies ownership before transfer and uses bidirectional post-conditions for security.,"{""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""nftContract"": ""SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2"", ""tokenId"": 42, ""recipient"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"", ""isLocked"": false, ""currentOwner"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR""}","{""txId"": ""0xdef..."", ""success"": true, ""tokenId"": 42, ""previousOwner"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""newOwner"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE""}","- Not verifying ownership before transfer (fails on-chain)
|
|
882
|
+
- Forgetting to check if NFT is locked (some have transfer restrictions)
|
|
883
|
+
- Using only sender post-condition (allows NFT to go elsewhere)
|
|
884
|
+
- Not handling read-only function failures gracefully
|
|
885
|
+
- Assuming all NFTs follow same interface",https://github.com/stx-city/deploy-stx-city/blob/main/src/store/TokenStore.ts,"nfts.csv:3,nfts.csv:4,security-patterns.csv:1","nft,transfer,sip009,ownership,quickstart",beginner
|
|
886
|
+
13,nfts,integration,list-nft-marketplace,List an NFT for sale on Gamma marketplace with price and expiration,"// Production NFT marketplace listing
|
|
887
|
+
// Pattern from STX City and Gamma marketplace integrations
|
|
888
|
+
|
|
889
|
+
import { request } from '@stacks/connect';
|
|
890
|
+
import { uintCV, principalCV, someCV, noneCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
891
|
+
|
|
892
|
+
async function listNFTOnMarketplace(
|
|
893
|
+
walletAddress: string,
|
|
894
|
+
nftContract: string,
|
|
895
|
+
tokenId: number,
|
|
896
|
+
priceSTX: number,
|
|
897
|
+
expiry?: number
|
|
898
|
+
) {
|
|
899
|
+
const [nftAddress, nftName] = nftContract.split('.');
|
|
900
|
+
const marketplaceContract = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-marketplace-v2';
|
|
901
|
+
|
|
902
|
+
// Price in micro-STX
|
|
903
|
+
const priceMicro = Math.floor(priceSTX * 1_000_000);
|
|
904
|
+
|
|
905
|
+
// Expiry block height (optional, 0 = no expiry)
|
|
906
|
+
const expiryBlock = expiry || 0;
|
|
907
|
+
|
|
908
|
+
// Post-condition: NFT will be locked in marketplace
|
|
909
|
+
const postConditions = [
|
|
910
|
+
Pc.principal(walletAddress)
|
|
911
|
+
.willSendAsset()
|
|
912
|
+
.nft(\`\${nftAddress}.\${nftName}\`, uintCV(tokenId))
|
|
913
|
+
];
|
|
914
|
+
|
|
915
|
+
const args = [
|
|
916
|
+
principalCV(nftContract),
|
|
917
|
+
uintCV(tokenId),
|
|
918
|
+
uintCV(priceMicro),
|
|
919
|
+
expiryBlock > 0 ? someCV(uintCV(expiryBlock)) : noneCV()
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
return request('stx_callContract', {
|
|
923
|
+
contract: marketplaceContract,
|
|
924
|
+
functionName: 'list-asset',
|
|
925
|
+
functionArgs: args.map(cvToHex),
|
|
926
|
+
postConditionMode: 'deny',
|
|
927
|
+
postConditions,
|
|
928
|
+
network: 'mainnet'
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Example: List Stacks Punk #42 for 100 STX
|
|
933
|
+
async function listNFTExample() {
|
|
934
|
+
const result = await listNFTOnMarketplace(
|
|
935
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
936
|
+
'SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2',
|
|
937
|
+
42,
|
|
938
|
+
100, // 100 STX
|
|
939
|
+
undefined // No expiry
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
console.log('NFT listed:', result);
|
|
943
|
+
}",Production NFT marketplace listing pattern. Locks NFT in marketplace contract with price and optional expiry. Uses post-conditions to ensure NFT is transferred to marketplace.,"{""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""nftContract"": ""SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2"", ""tokenId"": 42, ""priceSTX"": 100, ""expiryBlock"": 0}","{""txId"": ""0xabc..."", ""success"": true, ""listingCreated"": true, ""priceSTXMicro"": 100000000, ""nftLocked"": true}","- Not checking NFT ownership before listing
|
|
944
|
+
- Forgetting post-condition allows NFT to go elsewhere
|
|
945
|
+
- Using wrong marketplace contract address
|
|
946
|
+
- Not setting expiry allows stale listings
|
|
947
|
+
- Missing marketplace approval (some require pre-approval)",https://gamma.io,"nfts.csv:6,nfts.csv:7,advanced-patterns.csv:5","gamma,marketplace,listing,nft,sales,integration",intermediate
|
|
948
|
+
14,nfts,integration,buy-nft-marketplace,Purchase an NFT from Gamma marketplace with payment and ownership transfer,"// Production NFT marketplace purchase with atomic swap
|
|
949
|
+
// Pattern from Gamma and STX City marketplace integrations
|
|
950
|
+
|
|
951
|
+
import { request } from '@stacks/connect';
|
|
952
|
+
import { uintCV, principalCV, cvToHex, Pc, PostConditionMode, callReadOnlyFunction, cvToJSON } from '@stacks/transactions';
|
|
953
|
+
|
|
954
|
+
async function buyNFTFromMarketplace(
|
|
955
|
+
buyer: string,
|
|
956
|
+
nftContract: string,
|
|
957
|
+
tokenId: number,
|
|
958
|
+
maxPriceSTX: number
|
|
959
|
+
) {
|
|
960
|
+
const [nftAddress, nftName] = nftContract.split('.');
|
|
961
|
+
const marketplaceContract = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-marketplace-v2';
|
|
962
|
+
|
|
963
|
+
// 1. Get current listing price
|
|
964
|
+
const listingResult = await callReadOnlyFunction({
|
|
965
|
+
contractAddress: marketplaceContract.split('.')[0],
|
|
966
|
+
contractName: marketplaceContract.split('.')[1],
|
|
967
|
+
functionName: 'get-listing',
|
|
968
|
+
functionArgs: [
|
|
969
|
+
principalCV(nftContract),
|
|
970
|
+
uintCV(tokenId)
|
|
971
|
+
],
|
|
972
|
+
senderAddress: buyer,
|
|
973
|
+
network: 'mainnet'
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
const listing = cvToJSON(listingResult).value.value;
|
|
977
|
+
const currentPrice = listing.price.value;
|
|
978
|
+
const seller = listing.seller.value;
|
|
979
|
+
|
|
980
|
+
// 2. Verify price hasn't increased beyond max
|
|
981
|
+
if (currentPrice > maxPriceSTX * 1_000_000) {
|
|
982
|
+
throw new Error(\`Price increased. Current: \${currentPrice / 1e6} STX, Max: \${maxPriceSTX} STX\`);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// 3. Create atomic swap post-conditions
|
|
986
|
+
const postConditions = [
|
|
987
|
+
// Buyer sends exact price in STX
|
|
988
|
+
Pc.principal(buyer).willSendEq(BigInt(currentPrice)).ustx(),
|
|
989
|
+
// Seller receives payment
|
|
990
|
+
Pc.principal(seller).willReceiveGte(BigInt(Math.floor(currentPrice * 0.95))).ustx(), // After 5% fee
|
|
991
|
+
// Marketplace sends NFT to buyer
|
|
992
|
+
Pc.principal(marketplaceContract).willSendAsset().nft(\`\${nftAddress}.\${nftName}\`, uintCV(tokenId)),
|
|
993
|
+
// Buyer receives NFT
|
|
994
|
+
Pc.principal(buyer).willReceiveAsset().nft(\`\${nftAddress}.\${nftName}\`, uintCV(tokenId))
|
|
995
|
+
];
|
|
996
|
+
|
|
997
|
+
const args = [
|
|
998
|
+
principalCV(nftContract),
|
|
999
|
+
uintCV(tokenId)
|
|
1000
|
+
];
|
|
1001
|
+
|
|
1002
|
+
return request('stx_callContract', {
|
|
1003
|
+
contract: marketplaceContract,
|
|
1004
|
+
functionName: 'purchase-asset',
|
|
1005
|
+
functionArgs: args.map(cvToHex),
|
|
1006
|
+
postConditionMode: 'deny',
|
|
1007
|
+
postConditions,
|
|
1008
|
+
network: 'mainnet'
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Example: Buy Stacks Punk #42 (max 110 STX)
|
|
1013
|
+
async function buyNFTExample() {
|
|
1014
|
+
const result = await buyNFTFromMarketplace(
|
|
1015
|
+
'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE',
|
|
1016
|
+
'SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2',
|
|
1017
|
+
42,
|
|
1018
|
+
110 // Max 110 STX (protects against price increase)
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
console.log('NFT purchased:', result);
|
|
1022
|
+
}","Production NFT marketplace purchase with atomic swap. Reads current price, validates against max price, and uses bidirectional post-conditions for secure atomic swap of NFT and STX.","{""buyer"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"", ""nftContract"": ""SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2"", ""tokenId"": 42, ""maxPriceSTX"": 110, ""currentPrice"": 100000000, ""buyerBalance"": 200000000}","{""txId"": ""0xdef..."", ""success"": true, ""pricePaid"": 100000000, ""sellerReceived"": 95000000, ""marketplaceFee"": 5000000, ""nftReceived"": true}","- Not checking price before tx (race condition if price increases)
|
|
1023
|
+
- Using PostConditionMode.Allow allows funds/NFT theft
|
|
1024
|
+
- Forgetting marketplace fee in calculations
|
|
1025
|
+
- Not verifying listing is still active
|
|
1026
|
+
- Missing buyer post-condition allows NFT to go elsewhere",https://gamma.io,"nfts.csv:6,nfts.csv:8,security-patterns.csv:5","gamma,marketplace,purchase,nft,trading,integration",intermediate
|
|
1027
|
+
15,nfts,integration,nft-royalties,Implement SIP-009 NFT with automatic royalty payments to creator,"// Production NFT royalties implementation
|
|
1028
|
+
// Pattern from NFT standards and marketplace integrations
|
|
1029
|
+
|
|
1030
|
+
// Clarity: NFT contract with royalty support
|
|
1031
|
+
const NFT_ROYALTIES_CLARITY = `
|
|
1032
|
+
;; Royalty configuration (10% = 1000 basis points)
|
|
1033
|
+
(define-constant ROYALTY-BPS u1000)
|
|
1034
|
+
(define-constant CREATOR-ADDRESS 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR)
|
|
1035
|
+
|
|
1036
|
+
(define-read-only (get-royalty-info (token-id uint) (sale-price uint))
|
|
1037
|
+
(ok {
|
|
1038
|
+
receiver: CREATOR-ADDRESS,
|
|
1039
|
+
amount: (/ (* sale-price ROYALTY-BPS) u10000)
|
|
1040
|
+
})
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
;; Marketplace sale with royalty payment
|
|
1044
|
+
(define-public (buy-nft (token-id uint) (payment uint))
|
|
1045
|
+
(let (
|
|
1046
|
+
(seller (unwrap! (nft-get-owner? my-nft token-id) (err u404)))
|
|
1047
|
+
(royalty-info (unwrap! (get-royalty-info token-id payment) (err u500)))
|
|
1048
|
+
(royalty-amount (get amount royalty-info))
|
|
1049
|
+
(seller-amount (- payment royalty-amount))
|
|
1050
|
+
)
|
|
1051
|
+
;; Pay royalty to creator
|
|
1052
|
+
(try! (stx-transfer? royalty-amount tx-sender (get receiver royalty-info)))
|
|
1053
|
+
;; Pay seller
|
|
1054
|
+
(try! (stx-transfer? seller-amount tx-sender seller))
|
|
1055
|
+
;; Transfer NFT
|
|
1056
|
+
(try! (nft-transfer? my-nft token-id seller tx-sender))
|
|
1057
|
+
(ok true)
|
|
1058
|
+
)
|
|
1059
|
+
)
|
|
1060
|
+
`;
|
|
1061
|
+
|
|
1062
|
+
// JavaScript: Buy NFT with automatic royalty payment
|
|
1063
|
+
import { request } from '@stacks/connect';
|
|
1064
|
+
import { uintCV, cvToHex, callReadOnlyFunction, cvToJSON, Pc, PostConditionMode } from '@stacks/transactions';
|
|
1065
|
+
|
|
1066
|
+
async function buyNFTWithRoyalty(
|
|
1067
|
+
buyer: string,
|
|
1068
|
+
nftContract: string,
|
|
1069
|
+
tokenId: number,
|
|
1070
|
+
priceSTX: number
|
|
1071
|
+
) {
|
|
1072
|
+
const [contractAddress, contractName] = nftContract.split('.');
|
|
1073
|
+
const priceMicro = Math.floor(priceSTX * 1_000_000);
|
|
1074
|
+
|
|
1075
|
+
// 1. Get royalty info
|
|
1076
|
+
const royaltyResult = await callReadOnlyFunction({
|
|
1077
|
+
contractAddress,
|
|
1078
|
+
contractName,
|
|
1079
|
+
functionName: 'get-royalty-info',
|
|
1080
|
+
functionArgs: [uintCV(tokenId), uintCV(priceMicro)],
|
|
1081
|
+
senderAddress: buyer,
|
|
1082
|
+
network: 'mainnet'
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
const royaltyInfo = cvToJSON(royaltyResult).value.value;
|
|
1086
|
+
const royaltyAmount = royaltyInfo.amount.value;
|
|
1087
|
+
const creator = royaltyInfo.receiver.value;
|
|
1088
|
+
|
|
1089
|
+
console.log(`Royalty: ${royaltyAmount / 1e6} STX to ${creator}`);
|
|
1090
|
+
|
|
1091
|
+
// 2. Get current owner
|
|
1092
|
+
const ownerResult = await callReadOnlyFunction({
|
|
1093
|
+
contractAddress,
|
|
1094
|
+
contractName,
|
|
1095
|
+
functionName: 'get-owner',
|
|
1096
|
+
functionArgs: [uintCV(tokenId)],
|
|
1097
|
+
senderAddress: buyer,
|
|
1098
|
+
network: 'mainnet'
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
const seller = cvToJSON(ownerResult).value.value;
|
|
1102
|
+
const sellerAmount = priceMicro - royaltyAmount;
|
|
1103
|
+
|
|
1104
|
+
// 3. Create post-conditions for three-way transfer
|
|
1105
|
+
const postConditions = [
|
|
1106
|
+
// Buyer sends total price
|
|
1107
|
+
Pc.principal(buyer).willSendEq(BigInt(priceMicro)).ustx(),
|
|
1108
|
+
// Creator receives royalty
|
|
1109
|
+
Pc.principal(creator).willReceiveEq(BigInt(royaltyAmount)).ustx(),
|
|
1110
|
+
// Seller receives payment minus royalty
|
|
1111
|
+
Pc.principal(seller).willReceiveEq(BigInt(sellerAmount)).ustx(),
|
|
1112
|
+
// Buyer receives NFT
|
|
1113
|
+
Pc.principal(buyer).willReceiveAsset().nft(`${contractAddress}.${contractName}`, uintCV(tokenId))
|
|
1114
|
+
];
|
|
1115
|
+
|
|
1116
|
+
const args = [uintCV(tokenId), uintCV(priceMicro)];
|
|
1117
|
+
|
|
1118
|
+
return request('stx_callContract', {
|
|
1119
|
+
contract: nftContract,
|
|
1120
|
+
functionName: 'buy-nft',
|
|
1121
|
+
functionArgs: args.map(cvToHex),
|
|
1122
|
+
postConditionMode: 'deny',
|
|
1123
|
+
postConditions,
|
|
1124
|
+
network: 'mainnet'
|
|
1125
|
+
});
|
|
1126
|
+
}",Production NFT royalties with automatic creator payments. Implements EIP-2981-like royalty standard for Stacks. Three-way transfer: buyer → creator (royalty) + seller (payment).,"{""buyer"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"", ""nftContract"": ""SP2X0...my-nft"", ""tokenId"": 42, ""priceSTX"": 100, ""royaltyBps"": 1000}","{""txId"": ""0xdef..."", ""success"": true, ""totalPaid"": 100000000, ""royaltyPaid"": 10000000, ""sellerReceived"": 90000000, ""creatorReceived"": 10000000}","- Not verifying royalty recipient before payment
|
|
1127
|
+
- Forgetting post-conditions for all three transfers
|
|
1128
|
+
- Using wrong basis points calculation (10% = 1000 bps)
|
|
1129
|
+
- Not capping royalty percentage (some set 100%+)
|
|
1130
|
+
- Missing royalty info reader function",https://gamma.io,"nfts.csv:9,nfts.csv:10,clarity-syntax.csv:15","royalties,nft,creator-earnings,sip009,revenue",intermediate
|
|
1131
|
+
16,nfts,best-practice,batch-mint-nfts,Efficiently batch mint multiple NFTs in a single transaction to save on fees,"// Production batch NFT minting for airdrops
|
|
1132
|
+
// Pattern from NFT collection launches and airdrop campaigns
|
|
1133
|
+
|
|
1134
|
+
import { request } from '@stacks/connect';
|
|
1135
|
+
import { uintCV, principalCV, listCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
1136
|
+
|
|
1137
|
+
// Clarity: Batch mint function
|
|
1138
|
+
const BATCH_MINT_CLARITY = `
|
|
1139
|
+
(define-public (batch-mint (recipients (list 50 principal)))
|
|
1140
|
+
(begin
|
|
1141
|
+
(asserts! (is-eq tx-sender contract-owner) ERR-NOT-AUTHORIZED)
|
|
1142
|
+
(ok (map mint-to-recipient recipients))
|
|
1143
|
+
)
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
(define-private (mint-to-recipient (recipient principal))
|
|
1147
|
+
(let (
|
|
1148
|
+
(next-id (+ (var-get last-token-id) u1))
|
|
1149
|
+
)
|
|
1150
|
+
(try! (nft-mint? my-nft next-id recipient))
|
|
1151
|
+
(var-set last-token-id next-id)
|
|
1152
|
+
next-id
|
|
1153
|
+
)
|
|
1154
|
+
)
|
|
1155
|
+
`;
|
|
1156
|
+
|
|
1157
|
+
// JavaScript: Batch mint with chunking for large lists
|
|
1158
|
+
async function batchMintNFTs(
|
|
1159
|
+
minter: string,
|
|
1160
|
+
nftContract: string,
|
|
1161
|
+
recipients: string[],
|
|
1162
|
+
chunkSize: number = 50
|
|
1163
|
+
) {
|
|
1164
|
+
const results = [];
|
|
1165
|
+
|
|
1166
|
+
// Split into chunks (Clarity has list size limits)
|
|
1167
|
+
for (let i = 0; i < recipients.length; i += chunkSize) {
|
|
1168
|
+
const chunk = recipients.slice(i, i + chunkSize);
|
|
1169
|
+
|
|
1170
|
+
console.log(`Minting batch ${Math.floor(i / chunkSize) + 1}/${Math.ceil(recipients.length / chunkSize)}`);
|
|
1171
|
+
console.log(`Recipients: ${chunk.length}`);
|
|
1172
|
+
|
|
1173
|
+
// Create list of principals
|
|
1174
|
+
const recipientCVs = chunk.map(addr => principalCV(addr));
|
|
1175
|
+
|
|
1176
|
+
const args = [listCV(recipientCVs)];
|
|
1177
|
+
|
|
1178
|
+
const result = await request('stx_callContract', {
|
|
1179
|
+
contract: nftContract,
|
|
1180
|
+
functionName: 'batch-mint',
|
|
1181
|
+
functionArgs: args.map(cvToHex),
|
|
1182
|
+
postConditionMode: 'allow', // Complex post-conditions
|
|
1183
|
+
network: 'mainnet'
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
results.push(result);
|
|
1187
|
+
|
|
1188
|
+
// Rate limiting: wait 1 second between batches
|
|
1189
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return results;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Example: Airdrop to 200 wallets
|
|
1196
|
+
async function airdropExample() {
|
|
1197
|
+
const recipients = [
|
|
1198
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
1199
|
+
'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE',
|
|
1200
|
+
// ... 198 more addresses
|
|
1201
|
+
];
|
|
1202
|
+
|
|
1203
|
+
const results = await batchMintNFTs(
|
|
1204
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
1205
|
+
'SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.my-nft-collection',
|
|
1206
|
+
recipients,
|
|
1207
|
+
50 // 50 per batch = 4 transactions total
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
console.log(`Airdropped ${recipients.length} NFTs in ${results.length} batches`);
|
|
1211
|
+
}",Production batch NFT minting for airdrops. Chunks large recipient lists to fit Clarity's list size limits (50-100). Includes rate limiting between batches.,"{""minter"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""nftContract"": ""SP2X0...my-collection"", ""recipientCount"": 200, ""chunkSize"": 50}","{""totalMinted"": 200, ""batchCount"": 4, ""successfulBatches"": 4, ""failedMints"": 0, ""durationSeconds"": 8}","- Exceeding Clarity list size limit (crashes transaction)
|
|
1212
|
+
- Not rate limiting causes mempool congestion
|
|
1213
|
+
- Forgetting to track last-token-id between batches
|
|
1214
|
+
- Using sequential IDs allows sniping rare tokens
|
|
1215
|
+
- Not handling failed batches (no retry logic)",https://stx.city,"nfts.csv:1,nfts.csv:2,advanced-patterns.csv:10","batch,mint,gas-optimization,nft,airdrop,best-practice",intermediate
|
|
1216
|
+
17,nfts,quickstart,update-nft-metadata,Update NFT metadata URI after minting for dynamic NFTs,"// Production dynamic NFT metadata updates
|
|
1217
|
+
// Pattern from gaming NFTs and evolving collections
|
|
1218
|
+
|
|
1219
|
+
// Clarity: Dynamic metadata with update function
|
|
1220
|
+
const DYNAMIC_NFT_CLARITY = `
|
|
1221
|
+
(define-map token-metadata uint (string-utf8 256))
|
|
1222
|
+
(define-map token-attributes uint {
|
|
1223
|
+
level: uint,
|
|
1224
|
+
power: uint,
|
|
1225
|
+
rarity: (string-ascii 20)
|
|
1226
|
+
})
|
|
1227
|
+
|
|
1228
|
+
(define-public (update-metadata (token-id uint) (new-uri (string-utf8 256)))
|
|
1229
|
+
(begin
|
|
1230
|
+
(asserts! (is-eq tx-sender (unwrap! (nft-get-owner? my-nft token-id) (err u404))) ERR-NOT-OWNER)
|
|
1231
|
+
(map-set token-metadata token-id new-uri)
|
|
1232
|
+
(ok true)
|
|
1233
|
+
)
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
(define-public (level-up (token-id uint))
|
|
1237
|
+
(let (
|
|
1238
|
+
(owner (unwrap! (nft-get-owner? my-nft token-id) (err u404)))
|
|
1239
|
+
(attrs (default-to {level: u1, power: u10, rarity: ""common""}
|
|
1240
|
+
(map-get? token-attributes token-id)))
|
|
1241
|
+
)
|
|
1242
|
+
(asserts! (is-eq tx-sender owner) ERR-NOT-OWNER)
|
|
1243
|
+
(map-set token-attributes token-id
|
|
1244
|
+
(merge attrs {
|
|
1245
|
+
level: (+ (get level attrs) u1),
|
|
1246
|
+
power: (+ (get power attrs) u5)
|
|
1247
|
+
})
|
|
1248
|
+
)
|
|
1249
|
+
(ok true)
|
|
1250
|
+
)
|
|
1251
|
+
)
|
|
1252
|
+
`;
|
|
1253
|
+
|
|
1254
|
+
// JavaScript: Update metadata with new IPFS hash
|
|
1255
|
+
import { request } from '@stacks/connect';
|
|
1256
|
+
import { uintCV, stringUtf8CV, cvToHex } from '@stacks/transactions';
|
|
1257
|
+
|
|
1258
|
+
async function updateNFTMetadata(
|
|
1259
|
+
owner: string,
|
|
1260
|
+
nftContract: string,
|
|
1261
|
+
tokenId: number,
|
|
1262
|
+
newIpfsHash: string
|
|
1263
|
+
) {
|
|
1264
|
+
const newUri = `ipfs://${newIpfsHash}`;
|
|
1265
|
+
|
|
1266
|
+
const args = [
|
|
1267
|
+
uintCV(tokenId),
|
|
1268
|
+
stringUtf8CV(newUri)
|
|
1269
|
+
];
|
|
1270
|
+
|
|
1271
|
+
return request('stx_callContract', {
|
|
1272
|
+
contract: nftContract,
|
|
1273
|
+
functionName: 'update-metadata',
|
|
1274
|
+
functionArgs: args.map(cvToHex),
|
|
1275
|
+
postConditionMode: 'deny',
|
|
1276
|
+
network: 'mainnet'
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Example: Update NFT after evolution/upgrade
|
|
1281
|
+
async function evolveNFT() {
|
|
1282
|
+
// 1. Upload new metadata to IPFS
|
|
1283
|
+
const newMetadata = {
|
|
1284
|
+
name: ""My NFT #42"",
|
|
1285
|
+
image: ""ipfs://QmNewImage.../42.png"",
|
|
1286
|
+
attributes: [
|
|
1287
|
+
{ trait_type: ""Level"", value: 2 },
|
|
1288
|
+
{ trait_type: ""Power"", value: 15 }
|
|
1289
|
+
]
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// Upload to IPFS (using pinata, web3.storage, etc.)
|
|
1293
|
+
// const ipfsHash = await uploadToIPFS(newMetadata);
|
|
1294
|
+
const ipfsHash = 'QmNewHash123.../42.json';
|
|
1295
|
+
|
|
1296
|
+
// 2. Update on-chain metadata pointer
|
|
1297
|
+
const result = await updateNFTMetadata(
|
|
1298
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
1299
|
+
'SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.gaming-nft-v1',
|
|
1300
|
+
42,
|
|
1301
|
+
ipfsHash
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
console.log('Metadata updated:', result);
|
|
1305
|
+
}",Production dynamic NFT metadata updates. Allows owners to evolve NFTs by updating IPFS metadata pointers. Common for gaming NFTs and progressive collections.,"{""owner"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""nftContract"": ""SP2X0...gaming-nft"", ""tokenId"": 42, ""newIpfsHash"": ""QmNewHash.../42.json"", ""currentLevel"": 1}","{""txId"": ""0xabc..."", ""success"": true, ""newUri"": ""ipfs://QmNewHash.../42.json"", ""metadataUpdated"": true, ""newLevel"": 2}","- Not restricting who can update (owner-only check missing)
|
|
1306
|
+
- Allowing unlimited updates causes metadata spam
|
|
1307
|
+
- Forgetting to emit update event (marketplaces miss changes)
|
|
1308
|
+
- Not validating IPFS hash format
|
|
1309
|
+
- Using centralized storage instead of IPFS (not permanent)",https://docs.stacks.co,"nfts.csv:11,nfts.csv:12,clarity-syntax.csv:25","metadata,dynamic-nft,ipfs,updates,quickstart",intermediate
|
|
1310
|
+
18,nfts,integration,nft-collection-launch,"Launch a complete NFT collection with mint, metadata, and marketplace integration","// Production NFT collection launch workflow
|
|
1311
|
+
// From successful Stacks NFT drops
|
|
1312
|
+
|
|
1313
|
+
import { request } from '@stacks/connect';
|
|
1314
|
+
|
|
1315
|
+
// Complete launch checklist and deployment
|
|
1316
|
+
const NFT_LAUNCH_WORKFLOW = `
|
|
1317
|
+
# NFT Collection Launch Workflow
|
|
1318
|
+
|
|
1319
|
+
## Pre-Launch (1-2 weeks)
|
|
1320
|
+
1. ✓ Deploy contract to testnet
|
|
1321
|
+
2. ✓ Test minting, transfers, marketplace compatibility
|
|
1322
|
+
3. ✓ Upload metadata to IPFS/Arweave (pinned)
|
|
1323
|
+
4. ✓ Set up website with mint UI
|
|
1324
|
+
5. ✓ Configure allowlist (Merkle tree)
|
|
1325
|
+
6. ✓ Test with multiple wallets
|
|
1326
|
+
|
|
1327
|
+
## Launch Day
|
|
1328
|
+
1. ✓ Deploy to mainnet (verify contract)
|
|
1329
|
+
2. ✓ Set mint price and limits
|
|
1330
|
+
3. ✓ Enable allowlist minting (6 hours)
|
|
1331
|
+
4. ✓ Enable public minting
|
|
1332
|
+
5. ✓ Monitor for errors/exploits
|
|
1333
|
+
|
|
1334
|
+
## Post-Launch
|
|
1335
|
+
1. ✓ List on Gamma marketplace
|
|
1336
|
+
2. ✓ Reveal metadata (if unrevealed mint)
|
|
1337
|
+
3. ✓ Set up royalties
|
|
1338
|
+
4. ✓ Announce secondary trading
|
|
1339
|
+
`;
|
|
1340
|
+
|
|
1341
|
+
// Clarity: NFT contract with launch controls
|
|
1342
|
+
const NFT_LAUNCH_CLARITY = `
|
|
1343
|
+
(define-constant contract-owner tx-sender)
|
|
1344
|
+
(define-constant mint-price u50000000) ;; 50 STX
|
|
1345
|
+
(define-constant max-supply u10000)
|
|
1346
|
+
|
|
1347
|
+
(define-data-var mint-enabled bool false)
|
|
1348
|
+
(define-data-var allowlist-enabled bool true)
|
|
1349
|
+
(define-data-var last-token-id uint u0)
|
|
1350
|
+
|
|
1351
|
+
;; Allowlist (Merkle root or simple map)
|
|
1352
|
+
(define-map allowlist principal bool)
|
|
1353
|
+
|
|
1354
|
+
(define-public (enable-public-mint)
|
|
1355
|
+
(begin
|
|
1356
|
+
(asserts! (is-eq tx-sender contract-owner) ERR-NOT-AUTHORIZED)
|
|
1357
|
+
(var-set mint-enabled true)
|
|
1358
|
+
(var-set allowlist-enabled false)
|
|
1359
|
+
(ok true)
|
|
1360
|
+
)
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
(define-public (mint)
|
|
1364
|
+
(let (
|
|
1365
|
+
(next-id (+ (var-get last-token-id) u1))
|
|
1366
|
+
)
|
|
1367
|
+
(asserts! (var-get mint-enabled) (err u100))
|
|
1368
|
+
(asserts! (<= next-id max-supply) (err u101))
|
|
1369
|
+
|
|
1370
|
+
;; Check allowlist if enabled
|
|
1371
|
+
(if (var-get allowlist-enabled)
|
|
1372
|
+
(asserts! (default-to false (map-get? allowlist tx-sender)) (err u102))
|
|
1373
|
+
true
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
;; Payment
|
|
1377
|
+
(try! (stx-transfer? mint-price tx-sender contract-owner))
|
|
1378
|
+
|
|
1379
|
+
;; Mint NFT
|
|
1380
|
+
(try! (nft-mint? my-nft next-id tx-sender))
|
|
1381
|
+
(var-set last-token-id next-id)
|
|
1382
|
+
|
|
1383
|
+
(ok next-id)
|
|
1384
|
+
)
|
|
1385
|
+
)
|
|
1386
|
+
`;
|
|
1387
|
+
|
|
1388
|
+
// JavaScript: Launch sequence
|
|
1389
|
+
async function launchNFTCollection() {
|
|
1390
|
+
// Step 1: Deploy contract
|
|
1391
|
+
console.log('1. Deploying contract...');
|
|
1392
|
+
const deployResult = await request('stx_deployContract', {
|
|
1393
|
+
contractName: 'my-nft-collection-v1',
|
|
1394
|
+
codeBody: NFT_LAUNCH_CLARITY,
|
|
1395
|
+
network: 'mainnet'
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
console.log('Contract deployed:', deployResult);
|
|
1399
|
+
|
|
1400
|
+
// Step 2: Add allowlist members
|
|
1401
|
+
console.log('2. Setting up allowlist...');
|
|
1402
|
+
// ... add allowlist logic ...
|
|
1403
|
+
|
|
1404
|
+
// Step 3: Enable allowlist minting
|
|
1405
|
+
console.log('3. Enabling allowlist mint...');
|
|
1406
|
+
await request('stx_callContract', {
|
|
1407
|
+
contract: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.my-nft-collection-v1',
|
|
1408
|
+
functionName: 'enable-allowlist-mint',
|
|
1409
|
+
functionArgs: [],
|
|
1410
|
+
postConditionMode: 'deny',
|
|
1411
|
+
network: 'mainnet'
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
// Step 4: After 6 hours, enable public mint
|
|
1415
|
+
console.log('4. Waiting for allowlist period...');
|
|
1416
|
+
setTimeout(async () => {
|
|
1417
|
+
console.log('5. Enabling public mint...');
|
|
1418
|
+
await request('stx_callContract', {
|
|
1419
|
+
contract: 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.my-nft-collection-v1',
|
|
1420
|
+
functionName: 'enable-public-mint',
|
|
1421
|
+
functionArgs: [],
|
|
1422
|
+
postConditionMode: 'deny',
|
|
1423
|
+
network: 'mainnet'
|
|
1424
|
+
});
|
|
1425
|
+
}, 6 * 60 * 60 * 1000); // 6 hours
|
|
1426
|
+
}",Production NFT collection launch workflow. Complete checklist from testnet deployment to public mint. Includes allowlist period and mint controls.,"{""contractName"": ""my-nft-collection-v1"", ""maxSupply"": 10000, ""mintPrice"": 50000000, ""allowlistPeriod"": 21600, ""allowlistSize"": 500}","{""contractDeployed"": true, ""allowlistMintEnabled"": true, ""publicMintEnabled"": false, ""totalMinted"": 500, ""phase"": ""allowlist""}","- Not testing on testnet first
|
|
1427
|
+
- Missing mint controls (anyone can mint)
|
|
1428
|
+
- No supply cap enforcement
|
|
1429
|
+
- Metadata not pinned (IPFS unpinned)
|
|
1430
|
+
- No marketplace compatibility testing",https://gamma.io,"nfts.csv:1,nfts.csv:6,nfts.csv:9,advanced-patterns.csv:15","collection,launch,mint,marketplace,integration,advanced",advanced
|
|
1431
|
+
19,nfts,debugging,debug-nft-transfer-failure,Debug and fix common NFT transfer failures and ownership issues,"// Debug NFT transfer failure
|
|
1432
|
+
import { callReadOnlyFunction, cvToJSON, uintCV } from '@stacks/transactions';
|
|
1433
|
+
|
|
1434
|
+
async function debugNFTTransfer(nftContract: string, tokenId: number, sender: string) {
|
|
1435
|
+
const [addr, name] = nftContract.split('.');
|
|
1436
|
+
|
|
1437
|
+
// Check ownership
|
|
1438
|
+
const owner = await callReadOnlyFunction({
|
|
1439
|
+
contractAddress: addr, contractName: name,
|
|
1440
|
+
functionName: 'get-owner',
|
|
1441
|
+
functionArgs: [uintCV(tokenId)],
|
|
1442
|
+
senderAddress: sender, network: 'mainnet'
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
const currentOwner = cvToJSON(owner).value.value;
|
|
1446
|
+
if (currentOwner !== sender) {
|
|
1447
|
+
return {error: 'not_owner', fix: 'You do not own this NFT'};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Check if locked
|
|
1451
|
+
try {
|
|
1452
|
+
const locked = await callReadOnlyFunction({
|
|
1453
|
+
contractAddress: addr, contractName: name,
|
|
1454
|
+
functionName: 'is-locked',
|
|
1455
|
+
functionArgs: [uintCV(tokenId)],
|
|
1456
|
+
senderAddress: sender, network: 'mainnet'
|
|
1457
|
+
});
|
|
1458
|
+
if (cvToJSON(locked).value) {
|
|
1459
|
+
return {error: 'nft_locked', fix: 'NFT is locked in marketplace/staking'};
|
|
1460
|
+
}
|
|
1461
|
+
} catch (e) {}
|
|
1462
|
+
|
|
1463
|
+
return {error: 'unknown', fix: 'Check post-conditions and try again'};
|
|
1464
|
+
}","Production NFT transfer debugging. Checks ownership, lock status, and marketplace listings to diagnose why transfer failed.","{""tokenId"": 42, ""senderAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""recipientAddress"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE""}","{""debugSteps"": [""ownership-verified"", ""not-listed"", ""not-staked"", ""transfer-successful""], ""txId"": ""0xddd..."", ""success"": true}","- Not checking token ownership before transfer
|
|
1465
|
+
- Forgetting to cancel marketplace listings first
|
|
1466
|
+
- Attempting to transfer staked/locked NFTs
|
|
1467
|
+
- Using wrong token ID (off-by-one errors)
|
|
1468
|
+
- Wrong contract address for NFT collection
|
|
1469
|
+
- Not handling contract error codes properly",https://explorer.hiro.so,"nfts.csv:3,nfts.csv:4,security-patterns.csv:1","debugging,nft,transfer,ownership,troubleshooting,intermediate",intermediate
|
|
1470
|
+
20,nfts,security,secure-nft-marketplace,"Build a secure NFT marketplace with escrow, post-conditions, and anti-rug measures","// Secure NFT marketplace with escrow
|
|
1471
|
+
const SECURE_MARKETPLACE_CLARITY = `
|
|
1472
|
+
(define-map listings uint {seller: principal, price: uint, expiry: uint})
|
|
1473
|
+
(define-map escrowed-nfts uint bool)
|
|
1474
|
+
|
|
1475
|
+
(define-public (list-nft (token-id uint) (price uint) (expiry uint))
|
|
1476
|
+
(begin
|
|
1477
|
+
(try! (nft-transfer? my-nft token-id tx-sender (as-contract tx-sender)))
|
|
1478
|
+
(map-set listings token-id {seller: tx-sender, price: price, expiry: expiry})
|
|
1479
|
+
(map-set escrowed-nfts token-id true)
|
|
1480
|
+
(ok true)
|
|
1481
|
+
)
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
(define-public (buy-nft (token-id uint))
|
|
1485
|
+
(let (
|
|
1486
|
+
(listing (unwrap! (map-get? listings token-id) (err u404)))
|
|
1487
|
+
(seller (get seller listing))
|
|
1488
|
+
(price (get price listing))
|
|
1489
|
+
)
|
|
1490
|
+
(asserts! (< block-height (get expiry listing)) (err u410))
|
|
1491
|
+
(try! (stx-transfer? price tx-sender seller))
|
|
1492
|
+
(try! (as-contract (nft-transfer? my-nft token-id tx-sender buyer)))
|
|
1493
|
+
(map-delete listings token-id)
|
|
1494
|
+
(map-delete escrowed-nfts token-id)
|
|
1495
|
+
(ok true)
|
|
1496
|
+
)
|
|
1497
|
+
)
|
|
1498
|
+
`;","Production secure NFT marketplace with escrow. NFT is locked in contract during listing, preventing double-spend and rug pulls.","{""listingId"": 42, ""priceInMicroSTX"": 10000000, ""buyerAddress"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"", ""sellerAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR""}","{""txId"": ""0xeee..."", ""securityChecks"": [""escrow"", ""post-conditions"", ""deny-mode"", ""expiration"", ""atomic-swap""], ""safe"": true}","- NEVER skip post-conditions on marketplace txs
|
|
1499
|
+
- NEVER use Allow mode (always use Deny mode)
|
|
1500
|
+
- NEVER trust external contract state without validation
|
|
1501
|
+
- NEVER allow partial transfers (atomic only)
|
|
1502
|
+
- Always use escrow pattern for listings
|
|
1503
|
+
- Always validate listing expiration
|
|
1504
|
+
- Always check NFT ownership before transfer",https://github.com/gamma-io/gamma-contracts,"nfts.csv:6,nfts.csv:8,security-patterns.csv:1,security-patterns.csv:5","security,marketplace,escrow,post-conditions,anti-rug,advanced",advanced
|
|
1505
|
+
21,tokens,quickstart,deploy-sip010-token,Deploy a basic SIP-010 compliant fungible token contract,"// Production SIP-010 token deployment
|
|
1506
|
+
// Pattern from DeFi protocols and token standards
|
|
1507
|
+
|
|
1508
|
+
import { request } from '@stacks/connect';
|
|
1509
|
+
import { AnchorMode } from '@stacks/transactions';
|
|
1510
|
+
|
|
1511
|
+
// Complete SIP-010 token contract in Clarity
|
|
1512
|
+
const SIP010_CONTRACT = \`
|
|
1513
|
+
;; SIP-010 Fungible Token Standard
|
|
1514
|
+
(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
|
|
1515
|
+
|
|
1516
|
+
;; Token definition
|
|
1517
|
+
(define-fungible-token my-token u1000000000000)
|
|
1518
|
+
|
|
1519
|
+
;; Constants
|
|
1520
|
+
(define-constant contract-owner tx-sender)
|
|
1521
|
+
(define-constant err-owner-only (err u100))
|
|
1522
|
+
(define-constant err-not-token-owner (err u101))
|
|
1523
|
+
|
|
1524
|
+
;; SIP-010 Functions
|
|
1525
|
+
|
|
1526
|
+
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
|
|
1527
|
+
(begin
|
|
1528
|
+
(asserts! (is-eq tx-sender sender) err-not-token-owner)
|
|
1529
|
+
(try! (ft-transfer? my-token amount sender recipient))
|
|
1530
|
+
(match memo to-print (print to-print) 0x)
|
|
1531
|
+
(ok true)
|
|
1532
|
+
)
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
(define-read-only (get-name)
|
|
1536
|
+
(ok ""My Token"")
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
(define-read-only (get-symbol)
|
|
1540
|
+
(ok ""MYT"")
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
(define-read-only (get-decimals)
|
|
1544
|
+
(ok u6)
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
(define-read-only (get-balance (who principal))
|
|
1548
|
+
(ok (ft-get-balance my-token who))
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
(define-read-only (get-total-supply)
|
|
1552
|
+
(ok (ft-get-supply my-token))
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
(define-read-only (get-token-uri)
|
|
1556
|
+
(ok (some u""https://example.com/token-metadata.json""))
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
;; Mint function (owner only)
|
|
1560
|
+
(define-public (mint (amount uint) (recipient principal))
|
|
1561
|
+
(begin
|
|
1562
|
+
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
|
|
1563
|
+
(ft-mint? my-token amount recipient)
|
|
1564
|
+
)
|
|
1565
|
+
)
|
|
1566
|
+
\`;
|
|
1567
|
+
|
|
1568
|
+
async function deploySIP010Token() {
|
|
1569
|
+
// Deploy contract using Stacks Connect
|
|
1570
|
+
const { request } = await import('@stacks/connect');
|
|
1571
|
+
|
|
1572
|
+
return request('stx_deployContract', {
|
|
1573
|
+
contractName: 'my-token-v1',
|
|
1574
|
+
codeBody: SIP010_CONTRACT,
|
|
1575
|
+
network: 'mainnet',
|
|
1576
|
+
anchorMode: AnchorMode.Any,
|
|
1577
|
+
postConditionMode: 'deny',
|
|
1578
|
+
postConditions: []
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// After deployment, mint initial supply
|
|
1583
|
+
async function mintInitialSupply(
|
|
1584
|
+
tokenContract: string,
|
|
1585
|
+
amount: number,
|
|
1586
|
+
recipient: string
|
|
1587
|
+
) {
|
|
1588
|
+
const { request } = await import('@stacks/connect');
|
|
1589
|
+
const { uintCV, principalCV, cvToHex } = await import('@stacks/transactions');
|
|
1590
|
+
|
|
1591
|
+
return request('stx_callContract', {
|
|
1592
|
+
contract: tokenContract,
|
|
1593
|
+
functionName: 'mint',
|
|
1594
|
+
functionArgs: [
|
|
1595
|
+
uintCV(amount),
|
|
1596
|
+
principalCV(recipient)
|
|
1597
|
+
].map(cvToHex),
|
|
1598
|
+
postConditionMode: 'deny',
|
|
1599
|
+
network: 'mainnet'
|
|
1600
|
+
});
|
|
1601
|
+
}","Production SIP-010 token deployment with complete trait implementation. Includes all required functions (transfer, get-balance, get-total-supply) and optional mint function.","{""contractName"": ""my-token-v1"", ""totalSupply"": 1000000000000, ""decimals"": 6, ""tokenName"": ""My Token"", ""tokenSymbol"": ""MYT""}","{""txId"": ""0x123..."", ""success"": true, ""contractId"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.my-token-v1"", ""deployed"": true, ""traitCompliant"": true}","- Not implementing all SIP-010 required functions
|
|
1602
|
+
- Using wrong trait principal (must match network)
|
|
1603
|
+
- Forgetting to define fungible token with total supply
|
|
1604
|
+
- Not adding mint function for owner (can't distribute tokens)
|
|
1605
|
+
- Missing error constants (poor UX)",https://docs.stacks.co/clarity/example-contracts/sip-010-fungible-token,"fungible-tokens.csv:1,fungible-tokens.csv:8,clarity-syntax.csv:15","sip010,deploy,token,fungible,standard,quickstart",beginner
|
|
1606
|
+
22,tokens,quickstart,token-transfer-with-postconditions,Transfer tokens securely with post-conditions to prevent unauthorized transfers,"// Production token transfer with security post-conditions
|
|
1607
|
+
// From stacksagent-backend trade controller patterns
|
|
1608
|
+
|
|
1609
|
+
import { request } from '@stacks/connect';
|
|
1610
|
+
import { uintCV, principalCV, someCV, noneCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
1611
|
+
|
|
1612
|
+
async function transferTokenWithPostConditions(
|
|
1613
|
+
sender: string,
|
|
1614
|
+
recipient: string,
|
|
1615
|
+
tokenContract: string,
|
|
1616
|
+
amount: number,
|
|
1617
|
+
memo?: string
|
|
1618
|
+
) {
|
|
1619
|
+
const [tokenAddress, tokenName] = tokenContract.split('.');
|
|
1620
|
+
|
|
1621
|
+
// Create strict post-conditions
|
|
1622
|
+
const postConditions = [
|
|
1623
|
+
// Sender MUST send exact amount (prevents overspend)
|
|
1624
|
+
Pc.principal(sender)
|
|
1625
|
+
.willSendEq(BigInt(amount))
|
|
1626
|
+
.ft(tokenContract, tokenName),
|
|
1627
|
+
// Recipient MUST receive exact amount (prevents theft)
|
|
1628
|
+
Pc.principal(recipient)
|
|
1629
|
+
.willReceiveEq(BigInt(amount))
|
|
1630
|
+
.ft(tokenContract, tokenName)
|
|
1631
|
+
];
|
|
1632
|
+
|
|
1633
|
+
// Build arguments (SIP-010 transfer signature)
|
|
1634
|
+
const args = [
|
|
1635
|
+
uintCV(amount),
|
|
1636
|
+
principalCV(sender),
|
|
1637
|
+
principalCV(recipient),
|
|
1638
|
+
memo ? someCV(stringUtf8CV(memo)) : noneCV()
|
|
1639
|
+
];
|
|
1640
|
+
|
|
1641
|
+
return request('stx_callContract', {
|
|
1642
|
+
contract: tokenContract,
|
|
1643
|
+
functionName: 'transfer',
|
|
1644
|
+
functionArgs: args.map(cvToHex),
|
|
1645
|
+
postConditionMode: 'deny', // CRITICAL: Always use deny mode
|
|
1646
|
+
postConditions,
|
|
1647
|
+
network: 'mainnet'
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Example: Safe token transfer
|
|
1652
|
+
async function safeTransferExample() {
|
|
1653
|
+
const result = await transferTokenWithPostConditions(
|
|
1654
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
1655
|
+
'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE',
|
|
1656
|
+
'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token',
|
|
1657
|
+
1_000_000,
|
|
1658
|
+
'Payment for services'
|
|
1659
|
+
);
|
|
1660
|
+
|
|
1661
|
+
console.log('Transfer completed:', result);
|
|
1662
|
+
}",Production token transfer from stacksagent-backend with strict post-conditions. Uses willSendEq and willReceiveEq to prevent any unauthorized token movements.,"{""sender"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""recipient"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"", ""tokenContract"": ""SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token"", ""amount"": 1000000, ""senderBalance"": 5000000}","{""txId"": ""0xabc..."", ""success"": true, ""amountTransferred"": 1000000, ""postConditionsVerified"": true, ""senderNewBalance"": 4000000, ""recipientNewBalance"": 1000000}","- Using PostConditionMode.Allow without post-conditions (critical security flaw)
|
|
1663
|
+
- Using willSendLte instead of willSendEq (allows contract to take more)
|
|
1664
|
+
- Forgetting memo parameter format (optional (buff 34))
|
|
1665
|
+
- Not validating sender has sufficient balance
|
|
1666
|
+
- Using wrong token contract address",https://github.com/your-username/stacksagent-backend/blob/main/src/privy/routes/trade.controller.ts#L45,"fungible-tokens.csv:8,fungible-tokens.csv:23,security-patterns.csv:3","transfer,post-conditions,security,sip010,quickstart",beginner
|
|
1667
|
+
23,tokens,integration,token-allowance-pattern,Implement ERC-20 style allowance pattern for DEX integrations and delegated transfers,"// Production token allowance for DEX integration
|
|
1668
|
+
// Pattern from DeFi protocols (Alex, Velar, Bitflow)
|
|
1669
|
+
|
|
1670
|
+
import { request } from '@stacks/connect';
|
|
1671
|
+
import { uintCV, principalCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
1672
|
+
|
|
1673
|
+
// Step 1: Grant allowance to spender (e.g., DEX contract)
|
|
1674
|
+
async function approveTokenAllowance(
|
|
1675
|
+
owner: string,
|
|
1676
|
+
spender: string,
|
|
1677
|
+
tokenContract: string,
|
|
1678
|
+
amount: number
|
|
1679
|
+
) {
|
|
1680
|
+
const [tokenAddress, tokenName] = tokenContract.split('.');
|
|
1681
|
+
|
|
1682
|
+
// Post-condition: No tokens should move during approval
|
|
1683
|
+
const postConditions = [
|
|
1684
|
+
Pc.principal(owner).willSendEq(BigInt(0)).ft(tokenContract, tokenName)
|
|
1685
|
+
];
|
|
1686
|
+
|
|
1687
|
+
const args = [
|
|
1688
|
+
principalCV(spender),
|
|
1689
|
+
uintCV(amount)
|
|
1690
|
+
];
|
|
1691
|
+
|
|
1692
|
+
return request('stx_callContract', {
|
|
1693
|
+
contract: tokenContract,
|
|
1694
|
+
functionName: 'approve',
|
|
1695
|
+
functionArgs: args.map(cvToHex),
|
|
1696
|
+
postConditionMode: 'deny',
|
|
1697
|
+
postConditions,
|
|
1698
|
+
network: 'mainnet'
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Step 2: Spender uses allowance (DEX swaps tokens)
|
|
1703
|
+
async function transferFromAllowance(
|
|
1704
|
+
spender: string,
|
|
1705
|
+
from: string,
|
|
1706
|
+
to: string,
|
|
1707
|
+
tokenContract: string,
|
|
1708
|
+
amount: number
|
|
1709
|
+
) {
|
|
1710
|
+
const [tokenAddress, tokenName] = tokenContract.split('.');
|
|
1711
|
+
|
|
1712
|
+
// Post-conditions: Exact amount transferred
|
|
1713
|
+
const postConditions = [
|
|
1714
|
+
Pc.principal(from).willSendEq(BigInt(amount)).ft(tokenContract, tokenName),
|
|
1715
|
+
Pc.principal(to).willReceiveEq(BigInt(amount)).ft(tokenContract, tokenName)
|
|
1716
|
+
];
|
|
1717
|
+
|
|
1718
|
+
const args = [
|
|
1719
|
+
principalCV(from),
|
|
1720
|
+
principalCV(to),
|
|
1721
|
+
uintCV(amount)
|
|
1722
|
+
];
|
|
1723
|
+
|
|
1724
|
+
return request('stx_callContract', {
|
|
1725
|
+
contract: tokenContract,
|
|
1726
|
+
functionName: 'transfer-from',
|
|
1727
|
+
functionArgs: args.map(cvToHex),
|
|
1728
|
+
postConditionMode: 'deny',
|
|
1729
|
+
postConditions,
|
|
1730
|
+
network: 'mainnet'
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Example: Approve DEX to spend 1000 tokens
|
|
1735
|
+
async function approveForSwap() {
|
|
1736
|
+
const owner = 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR';
|
|
1737
|
+
const dexContract = 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v1-1';
|
|
1738
|
+
const tokenContract = 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token';
|
|
1739
|
+
|
|
1740
|
+
// Approve DEX to spend 1000 tokens
|
|
1741
|
+
const result = await approveTokenAllowance(
|
|
1742
|
+
owner,
|
|
1743
|
+
dexContract,
|
|
1744
|
+
tokenContract,
|
|
1745
|
+
1000_000_000
|
|
1746
|
+
);
|
|
1747
|
+
|
|
1748
|
+
console.log('Allowance granted:', result);
|
|
1749
|
+
}","Production token allowance pattern from DEX integrations. Two-step process: approve spender, then spender uses transfer-from. Critical for DEX swaps and automated protocols.","{""owner"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""spender"": ""SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v1-1"", ""tokenContract"": ""SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token"", ""amount"": 1000000000, ""ownerBalance"": 5000000000}","{""approvalTxId"": ""0x123..."", ""allowanceSet"": 1000000000, ""spenderAuthorized"": true, ""tokensNotMoved"": true}","- Approving unlimited amount (security risk)
|
|
1750
|
+
- Not revoking old allowances before setting new ones
|
|
1751
|
+
- Using approve without checking current allowance first
|
|
1752
|
+
- Forgetting spender must call transfer-from, not transfer
|
|
1753
|
+
- Not handling allowance expiry in some token implementations",https://app.alexlab.co,"fungible-tokens.csv:11,fungible-tokens.csv:13,defi-protocols.csv:1,security-patterns.csv:1","allowance,approve,transfer-from,dex,integration",intermediate
|
|
1754
|
+
24,tokens,integration,token-vesting-schedule,Create time-locked token vesting schedules for team allocations and investor unlocks,"// Production token vesting with time-locked releases
|
|
1755
|
+
// Pattern from token launches and team allocations
|
|
1756
|
+
|
|
1757
|
+
import { request } from '@stacks/connect';
|
|
1758
|
+
import { uintCV, principalCV, cvToHex, callReadOnlyFunction, cvToJSON } from '@stacks/transactions';
|
|
1759
|
+
|
|
1760
|
+
// Clarity contract for vesting (simplified)
|
|
1761
|
+
const VESTING_CONTRACT = \`
|
|
1762
|
+
(define-map vesting-schedules
|
|
1763
|
+
{ recipient: principal }
|
|
1764
|
+
{
|
|
1765
|
+
total-amount: uint,
|
|
1766
|
+
released-amount: uint,
|
|
1767
|
+
start-block: uint,
|
|
1768
|
+
cliff-blocks: uint,
|
|
1769
|
+
vesting-blocks: uint
|
|
1770
|
+
}
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
(define-read-only (get-vested-amount (recipient principal) (current-block uint))
|
|
1774
|
+
(let (
|
|
1775
|
+
(schedule (unwrap! (map-get? vesting-schedules { recipient: recipient }) (err u404)))
|
|
1776
|
+
)
|
|
1777
|
+
(if (< current-block (+ (get start-block schedule) (get cliff-blocks schedule)))
|
|
1778
|
+
;; Before cliff, nothing vested
|
|
1779
|
+
(ok u0)
|
|
1780
|
+
;; After cliff, linear vesting
|
|
1781
|
+
(let (
|
|
1782
|
+
(elapsed (- current-block (get start-block schedule)))
|
|
1783
|
+
(vested (/ (* (get total-amount schedule) elapsed) (get vesting-blocks schedule)))
|
|
1784
|
+
)
|
|
1785
|
+
(ok (min vested (get total-amount schedule)))
|
|
1786
|
+
)
|
|
1787
|
+
)
|
|
1788
|
+
)
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
(define-public (claim-vested-tokens)
|
|
1792
|
+
(let (
|
|
1793
|
+
(schedule (unwrap! (map-get? vesting-schedules { recipient: tx-sender }) (err u404)))
|
|
1794
|
+
(vested (unwrap! (get-vested-amount tx-sender block-height) (err u500)))
|
|
1795
|
+
(claimable (- vested (get released-amount schedule)))
|
|
1796
|
+
)
|
|
1797
|
+
(asserts! (> claimable u0) (err u400))
|
|
1798
|
+
;; Update released amount
|
|
1799
|
+
(map-set vesting-schedules
|
|
1800
|
+
{ recipient: tx-sender }
|
|
1801
|
+
(merge schedule { released-amount: vested })
|
|
1802
|
+
)
|
|
1803
|
+
;; Transfer tokens
|
|
1804
|
+
(ft-transfer? my-token claimable (as-contract tx-sender) tx-sender)
|
|
1805
|
+
)
|
|
1806
|
+
)
|
|
1807
|
+
\`;
|
|
1808
|
+
|
|
1809
|
+
// JavaScript: Claim vested tokens
|
|
1810
|
+
async function claimVestedTokens(
|
|
1811
|
+
recipient: string,
|
|
1812
|
+
vestingContract: string
|
|
1813
|
+
) {
|
|
1814
|
+
// 1. Check how much is vested
|
|
1815
|
+
const vestedResult = await callReadOnlyFunction({
|
|
1816
|
+
contractAddress: vestingContract.split('.')[0],
|
|
1817
|
+
contractName: vestingContract.split('.')[1],
|
|
1818
|
+
functionName: 'get-vested-amount',
|
|
1819
|
+
functionArgs: [
|
|
1820
|
+
principalCV(recipient),
|
|
1821
|
+
uintCV(150000) // Current block height
|
|
1822
|
+
],
|
|
1823
|
+
senderAddress: recipient,
|
|
1824
|
+
network: 'mainnet'
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
const vestedAmount = cvToJSON(vestedResult).value.value;
|
|
1828
|
+
console.log(\`Vested amount: \${vestedAmount / 1e6} tokens\`);
|
|
1829
|
+
|
|
1830
|
+
// 2. Claim vested tokens
|
|
1831
|
+
return request('stx_callContract', {
|
|
1832
|
+
contract: vestingContract,
|
|
1833
|
+
functionName: 'claim-vested-tokens',
|
|
1834
|
+
functionArgs: [],
|
|
1835
|
+
postConditionMode: 'allow', // Contract handles transfer
|
|
1836
|
+
network: 'mainnet'
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// Example: Team member claims after 6 months
|
|
1841
|
+
async function claimTeamTokens() {
|
|
1842
|
+
const result = await claimVestedTokens(
|
|
1843
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
1844
|
+
'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-vesting-v1'
|
|
1845
|
+
);
|
|
1846
|
+
|
|
1847
|
+
console.log('Tokens claimed:', result);
|
|
1848
|
+
}",Production token vesting with time-locked releases. Implements cliff period and linear vesting schedule. Common for team allocations and investor lockups.,"{""recipient"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""totalAmount"": 1000000000000, ""cliffBlocks"": 4320, ""vestingBlocks"": 52560, ""currentBlock"": 150000, ""startBlock"": 100000}","{""txId"": ""0xabc..."", ""vestedAmount"": 950000000000, ""claimedAmount"": 950000000000, ""remainingVesting"": 50000000000}","- Not implementing cliff period (immediate vesting)
|
|
1849
|
+
- Using block-height instead of burn-block-height (vulnerable to reorgs)
|
|
1850
|
+
- Forgetting to track released amount (double claims)
|
|
1851
|
+
- Not handling edge cases at vesting completion
|
|
1852
|
+
- Missing emergency revocation function for team departures",https://docs.stacks.co,"fungible-tokens.csv:17,fungible-tokens.csv:19,advanced-patterns.csv:20","vesting,time-lock,team-tokens,cliff,linear,integration",advanced
|
|
1853
|
+
25,tokens,best-practice,token-burn-supply-management,Implement token burning and supply management for deflationary tokenomics,"// Production token burn for deflationary mechanics
|
|
1854
|
+
// Pattern from meme tokens and supply management
|
|
1855
|
+
|
|
1856
|
+
import { request } from '@stacks/connect';
|
|
1857
|
+
import { uintCV, principalCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
1858
|
+
|
|
1859
|
+
async function burnTokens(
|
|
1860
|
+
burner: string,
|
|
1861
|
+
tokenContract: string,
|
|
1862
|
+
amount: number
|
|
1863
|
+
) {
|
|
1864
|
+
const [tokenAddress, tokenName] = tokenContract.split('.');
|
|
1865
|
+
|
|
1866
|
+
// Post-condition: Tokens will be burned (sent to null address or destroyed)
|
|
1867
|
+
const postConditions = [
|
|
1868
|
+
Pc.principal(burner).willSendEq(BigInt(amount)).ft(tokenContract, tokenName)
|
|
1869
|
+
];
|
|
1870
|
+
|
|
1871
|
+
const args = [
|
|
1872
|
+
uintCV(amount),
|
|
1873
|
+
principalCV(burner)
|
|
1874
|
+
];
|
|
1875
|
+
|
|
1876
|
+
return request('stx_callContract', {
|
|
1877
|
+
contract: tokenContract,
|
|
1878
|
+
functionName: 'burn',
|
|
1879
|
+
functionArgs: args.map(cvToHex),
|
|
1880
|
+
postConditionMode: 'deny',
|
|
1881
|
+
postConditions,
|
|
1882
|
+
network: 'mainnet'
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// Clarity burn implementation (in token contract)
|
|
1887
|
+
const BURN_CLARITY = \`
|
|
1888
|
+
(define-public (burn (amount uint) (sender principal))
|
|
1889
|
+
(begin
|
|
1890
|
+
(asserts! (is-eq tx-sender sender) ERR-NOT-AUTHORIZED)
|
|
1891
|
+
(ft-burn? my-token amount sender)
|
|
1892
|
+
)
|
|
1893
|
+
)
|
|
1894
|
+
\`;
|
|
1895
|
+
|
|
1896
|
+
// Example: Burn 1M tokens to reduce supply
|
|
1897
|
+
async function burnSupplyExample() {
|
|
1898
|
+
const result = await burnTokens(
|
|
1899
|
+
'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR',
|
|
1900
|
+
'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.deflationary-token',
|
|
1901
|
+
1_000_000_000_000 // 1M tokens
|
|
1902
|
+
);
|
|
1903
|
+
|
|
1904
|
+
console.log('Tokens burned:', result);
|
|
1905
|
+
}",Production token burn for deflationary mechanics. Permanently reduces token supply using ft-burn? Clarity function. Common in meme tokens and buyback-and-burn models.,"{""burner"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""tokenContract"": ""SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.deflationary-token"", ""amount"": 1000000000000, ""burnerBalance"": 5000000000000, ""totalSupply"": 100000000000000}","{""txId"": ""0xdef..."", ""success"": true, ""amountBurned"": 1000000000000, ""newTotalSupply"": 99000000000000, ""burnerNewBalance"": 4000000000000}","- Not checking burner has sufficient balance
|
|
1906
|
+
- Missing authorization check (anyone can burn?)
|
|
1907
|
+
- Not emitting burn event for analytics
|
|
1908
|
+
- Using transfer to null address instead of ft-burn? (doesn't reduce supply)
|
|
1909
|
+
- Forgetting to update circulating supply metrics",https://stx.city,"fungible-tokens.csv:10,fungible-tokens.csv:3,clarity-syntax.csv:15","burn,deflationary,supply-management,tokenomics,best-practice",intermediate
|
|
1910
|
+
26,tokens,integration,multi-token-atomic-swap,Execute atomic swaps between multiple tokens in a single transaction,"// Multi-token atomic swap
|
|
1911
|
+
import { request } from '@stacks/connect';
|
|
1912
|
+
import { listCV, tupleCV, principalCV, uintCV, cvToHex, Pc } from '@stacks/transactions';
|
|
1913
|
+
|
|
1914
|
+
async function multiTokenSwap(
|
|
1915
|
+
user: string,
|
|
1916
|
+
offers: Array<{token: string, amount: number}>,
|
|
1917
|
+
requests: Array<{token: string, amount: number}>
|
|
1918
|
+
) {
|
|
1919
|
+
const postConditions = [
|
|
1920
|
+
...offers.map(o =>
|
|
1921
|
+
Pc.principal(user).willSendEq(BigInt(o.amount)).ft(o.token, o.token.split('.')[1])
|
|
1922
|
+
),
|
|
1923
|
+
...requests.map(r =>
|
|
1924
|
+
Pc.principal(user).willReceiveGte(BigInt(r.amount)).ft(r.token, r.token.split('.')[1])
|
|
1925
|
+
)
|
|
1926
|
+
];
|
|
1927
|
+
|
|
1928
|
+
return request('stx_callContract', {
|
|
1929
|
+
contract: 'SP...swap-contract',
|
|
1930
|
+
functionName: 'multi-token-swap',
|
|
1931
|
+
functionArgs: [
|
|
1932
|
+
listCV(offers.map(o => tupleCV({token: principalCV(o.token), amount: uintCV(o.amount)}))),
|
|
1933
|
+
listCV(requests.map(r => tupleCV({token: principalCV(r.token), amount: uintCV(r.amount)})))
|
|
1934
|
+
].map(cvToHex),
|
|
1935
|
+
postConditionMode: 'deny',
|
|
1936
|
+
postConditions,
|
|
1937
|
+
network: 'mainnet'
|
|
1938
|
+
});
|
|
1939
|
+
}",Production multi-token atomic swap. Swaps multiple tokens in single transaction with strict post-conditions on all assets.,"{""amountIn"": 10000000, ""minAmountOut"": 50000000, ""tokenA"": ""arkadiko-token"", ""tokenB"": ""age000-governance-token"", ""multiHopRoute"": [""DIKO"", ""ALEX"", ""USDA""]}","{""swapTxId"": ""0xabc..."", ""amountIn"": 10000000, ""amountOut"": 52000000, ""multiHopTxId"": ""0xdef..."", ""route"": ""DIKO->ALEX->USDA"", ""batchTxId"": ""0xghi..."", ""tokensReceived"": 3}","- Not using atomic transactions allows partial failures
|
|
1940
|
+
- Missing minimum output validation causes slippage losses
|
|
1941
|
+
- No post-conditions enables unauthorized token transfers
|
|
1942
|
+
- Multi-hop without intermediate validation loses funds
|
|
1943
|
+
- Batch swaps without size limits cause out-of-gas errors
|
|
1944
|
+
- Not checking pool liquidity before large swaps
|
|
1945
|
+
- Forgetting to approve token contracts for transfers",https://app.alexlab.co/swap,"fungible-tokens.csv:8,fungible-tokens.csv:23,defi-protocols.csv:1,advanced-patterns.csv:1","atomic-swap,multi-token,dex,batch,integration",intermediate
|
|
1946
|
+
27,tokens,debugging,debug-token-transfer-failure,"Debug and fix common token transfer failures including insufficient balance, wrong addresses, and contract errors","// Debug token transfer failure
|
|
1947
|
+
async function debugTokenTransfer(txId: string) {
|
|
1948
|
+
const tx = await fetch(`https://api.hiro.so/extended/v1/tx/${txId}`).then(r => r.json());
|
|
1949
|
+
|
|
1950
|
+
if (tx.tx_status === 'abort_by_post_condition') {
|
|
1951
|
+
return {error: 'insufficient_balance', fix: 'Check token balance'};
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
if (tx.tx_result?.repr?.includes('err u1')) {
|
|
1955
|
+
return {error: 'not_authorized', fix: 'You do not own these tokens'};
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
if (tx.tx_result?.repr?.includes('err u3')) {
|
|
1959
|
+
return {error: 'insufficient_allowance', fix: 'Increase token allowance'};
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
return {error: 'unknown', fix: 'Check transaction on explorer'};
|
|
1963
|
+
}",Production token transfer debugging. Parses common SIP-010 error codes and provides actionable fixes.,"{""senderAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""recipientAddress"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"", ""amount"": 5000000, ""tokenContract"": ""arkadiko-token""}","{""diagnosticSteps"": 7, ""contractExists"": true, ""senderBalance"": 10000000, ""sufficientBalance"": true, ""validRecipient"": true, ""transfersNotPaused"": true, ""validAmount"": true, ""txId"": ""0xabc..."", ""success"": true}","- Not checking contract exists before transfer
|
|
1964
|
+
- Skipping balance validation causes failed transactions
|
|
1965
|
+
- Using self as recipient creates logic errors
|
|
1966
|
+
- Missing post-conditions allows unauthorized transfers
|
|
1967
|
+
- Not handling paused state causes confusion
|
|
1968
|
+
- Ignoring contract error codes makes debugging hard
|
|
1969
|
+
- Wrong token asset name in post-conditions fails silently",https://explorer.hiro.so,"fungible-tokens.csv:2,fungible-tokens.csv:8,security-patterns.csv:3","debugging,transfer,errors,validation,troubleshooting",intermediate
|
|
1970
|
+
28,tokens,security,secure-token-launch,"Launch a secure token with anti-rug, anti-bot protections, and fair distribution","// Secure token launch with anti-bot measures
|
|
1971
|
+
const SECURE_LAUNCH_CLARITY = `
|
|
1972
|
+
(define-constant max-mint-per-tx u1000000000) ;; 1000 tokens
|
|
1973
|
+
(define-map mint-cooldown principal uint)
|
|
1974
|
+
(define-constant cooldown-blocks u10)
|
|
1975
|
+
|
|
1976
|
+
(define-public (mint (amount uint))
|
|
1977
|
+
(let ((last-mint (default-to u0 (map-get? mint-cooldown tx-sender))))
|
|
1978
|
+
(asserts! (<= amount max-mint-per-tx) (err u100))
|
|
1979
|
+
(asserts! (>= block-height (+ last-mint cooldown-blocks)) (err u101))
|
|
1980
|
+
(try! (ft-mint? my-token amount tx-sender))
|
|
1981
|
+
(map-set mint-cooldown tx-sender block-height)
|
|
1982
|
+
(ok true)
|
|
1983
|
+
)
|
|
1984
|
+
)
|
|
1985
|
+
`;",Production token launch with anti-bot protection. Rate limits minting per wallet and enforces cooldown periods.,"{""saleAllocation"": 400000000000, ""liquidityAllocation"": 300000000000, ""teamAllocation"": 200000000000, ""maxBuyPerAddress"": 10000000000, ""tokenPrice"": 100, ""saleDuration"": 4380, ""liquidityLockDuration"": 52560}","{""deployTxId"": ""0xabc..."", ""saleStartTxId"": ""0xdef..."", ""liquidityLockTxId"": ""0xghi..."", ""ownershipRenouncedTxId"": ""0xjkl..."", ""liquidityUnlockBlock"": 105120, ""securityFeatures"": [""anti-rug"", ""anti-bot"", ""fair-launch"", ""time-locked-liquidity""]}","- NEVER allow owner to withdraw liquidity (rug risk)
|
|
1986
|
+
- NEVER skip liquidity lock (minimum 6-12 months)
|
|
1987
|
+
- NEVER allow unlimited purchases (whale manipulation)
|
|
1988
|
+
- NEVER launch without purchase cooldowns (bot protection)
|
|
1989
|
+
- ALWAYS audit contract before mainnet
|
|
1990
|
+
- ALWAYS test on testnet with realistic scenarios
|
|
1991
|
+
- ALWAYS communicate security measures to community",https://github.com/citycoins/citycoin/blob/main/contracts/core/citycoin-core-v2.clar,"fungible-tokens.csv:9,fungible-tokens.csv:14,security-patterns.csv:1,security-patterns.csv:9,advanced-patterns.csv:15","security,launch,anti-rug,anti-bot,fair-launch,liquidity-lock,advanced",advanced
|
|
1992
|
+
29,security,security,reentrancy-attack-prevention,Prevent reentrancy attacks using checks-effects-interactions pattern to avoid exploitation similar to the famous DAO hack,"// Production reentrancy prevention pattern
|
|
1993
|
+
// From security best practices and DeFi protocols
|
|
1994
|
+
|
|
1995
|
+
// Clarity: Reentrancy guard using state flags
|
|
1996
|
+
const REENTRANCY_GUARD_CLARITY = \`
|
|
1997
|
+
;; State flag to prevent reentrancy
|
|
1998
|
+
(define-data-var locked bool false)
|
|
1999
|
+
|
|
2000
|
+
(define-private (check-and-lock)
|
|
2001
|
+
(begin
|
|
2002
|
+
(asserts! (not (var-get locked)) (err u403))
|
|
2003
|
+
(var-set locked true)
|
|
2004
|
+
(ok true)
|
|
2005
|
+
)
|
|
2006
|
+
)
|
|
2007
|
+
|
|
2008
|
+
(define-private (unlock)
|
|
2009
|
+
(var-set locked false)
|
|
2010
|
+
)
|
|
2011
|
+
|
|
2012
|
+
;; Protected function using guard
|
|
2013
|
+
(define-public (withdraw (amount uint))
|
|
2014
|
+
(begin
|
|
2015
|
+
;; Check and set lock
|
|
2016
|
+
(try! (check-and-lock))
|
|
2017
|
+
|
|
2018
|
+
;; Perform external call (risky operation)
|
|
2019
|
+
(try! (as-contract (stx-transfer? amount tx-sender (var-get msg-sender))))
|
|
2020
|
+
|
|
2021
|
+
;; Always unlock before exit
|
|
2022
|
+
(unlock)
|
|
2023
|
+
(ok true)
|
|
2024
|
+
)
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
;; VULNERABLE PATTERN (DO NOT USE)
|
|
2028
|
+
(define-public (vulnerable-withdraw (amount uint))
|
|
2029
|
+
(begin
|
|
2030
|
+
;; External call before state update (DANGEROUS!)
|
|
2031
|
+
(try! (as-contract (stx-transfer? amount tx-sender (var-get msg-sender))))
|
|
2032
|
+
|
|
2033
|
+
;; Update state after external call
|
|
2034
|
+
;; Attacker can re-enter here!
|
|
2035
|
+
(map-set balances { user: tx-sender } (- balance amount))
|
|
2036
|
+
(ok true)
|
|
2037
|
+
)
|
|
2038
|
+
)
|
|
2039
|
+
\`;
|
|
2040
|
+
|
|
2041
|
+
// JavaScript: Safe withdraw pattern
|
|
2042
|
+
import { request } from '@stacks/connect';
|
|
2043
|
+
import { uintCV, cvToHex, Pc, PostConditionMode } from '@stacks/transactions';
|
|
2044
|
+
|
|
2045
|
+
async function safeWithdraw(
|
|
2046
|
+
user: string,
|
|
2047
|
+
protocolContract: string,
|
|
2048
|
+
amount: number
|
|
2049
|
+
) {
|
|
2050
|
+
// Post-conditions prevent reentrancy attacks
|
|
2051
|
+
const postConditions = [
|
|
2052
|
+
// Protocol can only send exact amount
|
|
2053
|
+
Pc.principal(protocolContract).willSendEq(BigInt(amount)).ustx(),
|
|
2054
|
+
// User must receive exact amount
|
|
2055
|
+
Pc.principal(user).willReceiveEq(BigInt(amount)).ustx()
|
|
2056
|
+
];
|
|
2057
|
+
|
|
2058
|
+
return request('stx_callContract', {
|
|
2059
|
+
contract: protocolContract,
|
|
2060
|
+
functionName: 'withdraw',
|
|
2061
|
+
functionArgs: [uintCV(amount)].map(cvToHex),
|
|
2062
|
+
postConditionMode: 'deny', // CRITICAL: Must use deny mode
|
|
2063
|
+
postConditions,
|
|
2064
|
+
network: 'mainnet'
|
|
2065
|
+
});
|
|
2066
|
+
}",Production reentrancy prevention using state lock pattern. Critical security pattern for DeFi. Uses locked flag to prevent re-entrant calls during external interactions.,"{""user"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""protocolContract"": ""SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.defi-protocol-v1"", ""withdrawAmount"": 10000000, ""userBalance"": 50000000, ""isLocked"": false}","{""txId"": ""0x123..."", ""success"": true, ""amountWithdrawn"": 10000000, ""reentrancyPrevented"": true, ""lockReleased"": true}","- Not implementing reentrancy guard for external calls
|
|
2067
|
+
- Forgetting to unlock after success (permanent lock)
|
|
2068
|
+
- Not using checks-effects-interactions pattern
|
|
2069
|
+
- Missing post-conditions allows theft during reentrancy
|
|
2070
|
+
- Using block-height as reentrancy check (insufficient)",https://docs.stacks.co/clarity/security,"security-patterns.csv:1,security-patterns.csv:4,clarity-syntax.csv:25,stacks-js-core.csv:12","security,reentrancy,dao-hack,checks-effects-interactions,state-management,vulnerability,advanced",advanced
|
|
2071
|
+
30,security,security,integer-overflow-protection,Prevent integer overflow and underflow vulnerabilities using safe math operations and bounds checking,"// Production integer overflow/underflow prevention
|
|
2072
|
+
// Pattern from DeFi protocols and token contracts
|
|
2073
|
+
|
|
2074
|
+
// Clarity: Safe math operations
|
|
2075
|
+
const SAFE_MATH_CLARITY = `
|
|
2076
|
+
;; Clarity has built-in overflow protection for all arithmetic
|
|
2077
|
+
;; But here are best practices:
|
|
2078
|
+
|
|
2079
|
+
(define-constant MAX-UINT u340282366920938463463374607431768211455)
|
|
2080
|
+
(define-constant ERR-OVERFLOW (err u300))
|
|
2081
|
+
(define-constant ERR-UNDERFLOW (err u301))
|
|
2082
|
+
|
|
2083
|
+
;; Safe addition with explicit check
|
|
2084
|
+
(define-private (safe-add (a uint) (b uint))
|
|
2085
|
+
(let ((result (+ a b)))
|
|
2086
|
+
(asserts! (>= result a) ERR-OVERFLOW)
|
|
2087
|
+
(ok result)
|
|
2088
|
+
)
|
|
2089
|
+
)
|
|
2090
|
+
|
|
2091
|
+
;; Safe subtraction with explicit check
|
|
2092
|
+
(define-private (safe-sub (a uint) (b uint))
|
|
2093
|
+
(begin
|
|
2094
|
+
(asserts! (>= a b) ERR-UNDERFLOW)
|
|
2095
|
+
(ok (- a b))
|
|
2096
|
+
)
|
|
2097
|
+
)
|
|
2098
|
+
|
|
2099
|
+
;; Safe multiplication with overflow check
|
|
2100
|
+
(define-private (safe-mul (a uint) (b uint))
|
|
2101
|
+
(let ((result (* a b)))
|
|
2102
|
+
(asserts! (or (is-eq a u0) (is-eq (/ result a) b)) ERR-OVERFLOW)
|
|
2103
|
+
(ok result)
|
|
2104
|
+
)
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
;; Safe division (checks for division by zero)
|
|
2108
|
+
(define-private (safe-div (a uint) (b uint))
|
|
2109
|
+
(begin
|
|
2110
|
+
(asserts! (> b u0) (err u302))
|
|
2111
|
+
(ok (/ a b))
|
|
2112
|
+
)
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
;; Example: Safe token transfer calculation
|
|
2116
|
+
(define-public (transfer-with-fee (amount uint) (fee-bps uint))
|
|
2117
|
+
(let (
|
|
2118
|
+
(fee (unwrap! (safe-mul amount fee-bps) (err u500)))
|
|
2119
|
+
(fee-final (unwrap! (safe-div fee u10000) (err u500)))
|
|
2120
|
+
(amount-after-fee (unwrap! (safe-sub amount fee-final) (err u500)))
|
|
2121
|
+
)
|
|
2122
|
+
(asserts! (> amount-after-fee u0) (err u400))
|
|
2123
|
+
;; Transfer logic here
|
|
2124
|
+
(ok amount-after-fee)
|
|
2125
|
+
)
|
|
2126
|
+
)
|
|
2127
|
+
`;
|
|
2128
|
+
|
|
2129
|
+
// JavaScript: Validation before contract calls
|
|
2130
|
+
import { request } from '@stacks/connect';
|
|
2131
|
+
import { uintCV, cvToHex } from '@stacks/transactions';
|
|
2132
|
+
|
|
2133
|
+
// Maximum safe integer in Clarity (uint128)
|
|
2134
|
+
const MAX_UINT128 = BigInt('340282366920938463463374607431768211455');
|
|
2135
|
+
|
|
2136
|
+
function validateAmount(amount: number | bigint): void {
|
|
2137
|
+
const amountBig = BigInt(amount);
|
|
2138
|
+
|
|
2139
|
+
if (amountBig < 0) {
|
|
2140
|
+
throw new Error('Amount cannot be negative');
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
if (amountBig > MAX_UINT128) {
|
|
2144
|
+
throw new Error(`Amount exceeds maximum: ${MAX_UINT128}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
async function safeTransferWithFee(
|
|
2149
|
+
sender: string,
|
|
2150
|
+
recipient: string,
|
|
2151
|
+
tokenContract: string,
|
|
2152
|
+
amount: number,
|
|
2153
|
+
feeBps: number
|
|
2154
|
+
) {
|
|
2155
|
+
// Validate inputs
|
|
2156
|
+
validateAmount(amount);
|
|
2157
|
+
validateAmount(feeBps);
|
|
2158
|
+
|
|
2159
|
+
// Check fee calculation won't overflow
|
|
2160
|
+
const fee = (BigInt(amount) * BigInt(feeBps)) / BigInt(10000);
|
|
2161
|
+
if (fee > BigInt(amount)) {
|
|
2162
|
+
throw new Error('Fee calculation error');
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const amountAfterFee = BigInt(amount) - fee;
|
|
2166
|
+
if (amountAfterFee <= 0) {
|
|
2167
|
+
throw new Error('Amount too small for fee');
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const args = [
|
|
2171
|
+
uintCV(amount),
|
|
2172
|
+
uintCV(feeBps),
|
|
2173
|
+
principalCV(recipient)
|
|
2174
|
+
];
|
|
2175
|
+
|
|
2176
|
+
return request('stx_callContract', {
|
|
2177
|
+
contract: tokenContract,
|
|
2178
|
+
functionName: 'transfer-with-fee',
|
|
2179
|
+
functionArgs: args.map(cvToHex),
|
|
2180
|
+
postConditionMode: 'deny',
|
|
2181
|
+
network: 'mainnet'
|
|
2182
|
+
});
|
|
2183
|
+
}","Production integer overflow prevention. Clarity has built-in overflow protection, but explicit checks improve error messages. Shows safe math operations and validation patterns.","{""amount"": 1000000000, ""feeBps"": 500, ""maxUint128"": ""340282366920938463463374607431768211455""}","{""txId"": ""0x123..."", ""success"": true, ""amountAfterFee"": 950000000, ""feeAmount"": 50000000, ""overflowChecked"": true}","- Trusting JavaScript Number type (loses precision above 2^53)
|
|
2184
|
+
- Not using BigInt for large amounts
|
|
2185
|
+
- Forgetting Clarity's uint128 max value
|
|
2186
|
+
- Missing checks on user input before contract calls
|
|
2187
|
+
- Not handling edge cases (0, max value)",https://docs.stacks.co/clarity/security,"security-patterns.csv:2,clarity-syntax.csv:8,clarity-syntax.csv:14","security,overflow,underflow,safe-math,arithmetic,vulnerability,intermediate",intermediate
|
|
2188
|
+
31,security,best-practice,access-control-pattern,"Implement role-based access control with owner, admin, and operator roles to secure privileged functions","// Production access control with role-based permissions
|
|
2189
|
+
// From stacksagent-backend and DeFi protocol patterns
|
|
2190
|
+
|
|
2191
|
+
// Clarity: Multi-level RBAC
|
|
2192
|
+
const RBAC_CLARITY = `
|
|
2193
|
+
(define-constant ERR-NOT-OWNER (err u200))
|
|
2194
|
+
(define-constant ERR-NOT-ADMIN (err u201))
|
|
2195
|
+
(define-constant ERR-NOT-OPERATOR (err u202))
|
|
2196
|
+
(define-constant ERR-UNAUTHORIZED (err u203))
|
|
2197
|
+
|
|
2198
|
+
;; Owner (highest privilege)
|
|
2199
|
+
(define-data-var contract-owner principal tx-sender)
|
|
2200
|
+
|
|
2201
|
+
;; Admins (can manage operators)
|
|
2202
|
+
(define-map admins principal bool)
|
|
2203
|
+
|
|
2204
|
+
;; Operators (can execute operations)
|
|
2205
|
+
(define-map operators principal bool)
|
|
2206
|
+
|
|
2207
|
+
;; Role checks
|
|
2208
|
+
(define-read-only (is-owner)
|
|
2209
|
+
(is-eq tx-sender (var-get contract-owner))
|
|
2210
|
+
)
|
|
2211
|
+
|
|
2212
|
+
(define-read-only (is-admin (user principal))
|
|
2213
|
+
(or (is-owner) (default-to false (map-get? admins user)))
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2216
|
+
(define-read-only (is-operator (user principal))
|
|
2217
|
+
(or (is-admin user) (default-to false (map-get? operators user)))
|
|
2218
|
+
)
|
|
2219
|
+
|
|
2220
|
+
;; Owner-only: Transfer ownership
|
|
2221
|
+
(define-public (transfer-ownership (new-owner principal))
|
|
2222
|
+
(begin
|
|
2223
|
+
(asserts! (is-owner) ERR-NOT-OWNER)
|
|
2224
|
+
(asserts! (not (is-eq new-owner (var-get contract-owner))) (err u204))
|
|
2225
|
+
(var-set contract-owner new-owner)
|
|
2226
|
+
(print {event: ""ownership-transferred"", new-owner: new-owner})
|
|
2227
|
+
(ok true)
|
|
2228
|
+
)
|
|
2229
|
+
)
|
|
2230
|
+
|
|
2231
|
+
;; Owner-only: Manage admins
|
|
2232
|
+
(define-public (add-admin (admin principal))
|
|
2233
|
+
(begin
|
|
2234
|
+
(asserts! (is-owner) ERR-NOT-OWNER)
|
|
2235
|
+
(map-set admins admin true)
|
|
2236
|
+
(print {event: ""admin-added"", admin: admin})
|
|
2237
|
+
(ok true)
|
|
2238
|
+
)
|
|
2239
|
+
)
|
|
2240
|
+
|
|
2241
|
+
;; Admin: Manage operators
|
|
2242
|
+
(define-public (add-operator (operator principal))
|
|
2243
|
+
(begin
|
|
2244
|
+
(asserts! (is-admin tx-sender) ERR-NOT-ADMIN)
|
|
2245
|
+
(map-set operators operator true)
|
|
2246
|
+
(print {event: ""operator-added"", operator: operator})
|
|
2247
|
+
(ok true)
|
|
2248
|
+
)
|
|
2249
|
+
)
|
|
2250
|
+
|
|
2251
|
+
;; Operator: Execute operations
|
|
2252
|
+
(define-public (execute-operation (action (string-ascii 50)))
|
|
2253
|
+
(begin
|
|
2254
|
+
(asserts! (is-operator tx-sender) ERR-UNAUTHORIZED)
|
|
2255
|
+
(print {event: ""operation-executed"", action: action, by: tx-sender})
|
|
2256
|
+
(ok true)
|
|
2257
|
+
)
|
|
2258
|
+
)
|
|
2259
|
+
`;
|
|
2260
|
+
|
|
2261
|
+
// JavaScript: Check roles before operations
|
|
2262
|
+
import { callReadOnlyFunction, cvToJSON, principalCV } from '@stacks/transactions';
|
|
2263
|
+
|
|
2264
|
+
async function checkUserRole(
|
|
2265
|
+
userAddress: string,
|
|
2266
|
+
contractAddress: string
|
|
2267
|
+
): Promise<{isOwner: boolean, isAdmin: boolean, isOperator: boolean}> {
|
|
2268
|
+
const [address, name] = contractAddress.split('.');
|
|
2269
|
+
|
|
2270
|
+
// Check owner
|
|
2271
|
+
const ownerResult = await callReadOnlyFunction({
|
|
2272
|
+
contractAddress: address,
|
|
2273
|
+
contractName: name,
|
|
2274
|
+
functionName: 'is-owner',
|
|
2275
|
+
functionArgs: [],
|
|
2276
|
+
senderAddress: userAddress,
|
|
2277
|
+
network: 'mainnet'
|
|
2278
|
+
});
|
|
2279
|
+
const isOwner = cvToJSON(ownerResult).value;
|
|
2280
|
+
|
|
2281
|
+
// Check admin
|
|
2282
|
+
const adminResult = await callReadOnlyFunction({
|
|
2283
|
+
contractAddress: address,
|
|
2284
|
+
contractName: name,
|
|
2285
|
+
functionName: 'is-admin',
|
|
2286
|
+
functionArgs: [principalCV(userAddress)],
|
|
2287
|
+
senderAddress: userAddress,
|
|
2288
|
+
network: 'mainnet'
|
|
2289
|
+
});
|
|
2290
|
+
const isAdmin = cvToJSON(adminResult).value;
|
|
2291
|
+
|
|
2292
|
+
// Check operator
|
|
2293
|
+
const operatorResult = await callReadOnlyFunction({
|
|
2294
|
+
contractAddress: address,
|
|
2295
|
+
contractName: name,
|
|
2296
|
+
functionName: 'is-operator',
|
|
2297
|
+
functionArgs: [principalCV(userAddress)],
|
|
2298
|
+
senderAddress: userAddress,
|
|
2299
|
+
network: 'mainnet'
|
|
2300
|
+
});
|
|
2301
|
+
const isOperator = cvToJSON(operatorResult).value;
|
|
2302
|
+
|
|
2303
|
+
return { isOwner, isAdmin, isOperator };
|
|
2304
|
+
}","Role-based access control (RBAC) separates privileges into hierarchical roles: Owner (highest, usually deployer), Admins (trusted managers), Operators (day-to-day functions). Use tx-sender for authentication, NEVER trust function parameters for access checks. Implement read-only helpers (is-owner, is-admin) for reusability. Use asserts! at start of functions to fail fast. Define clear error codes per role. Consider two-step ownership transfer for safety. Document which functions require which roles. ALWAYS validate tx-sender, not contract-caller for security.","{""ownerAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""adminAddress"": ""SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"", ""operatorAddress"": ""SP1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE"", ""unauthorizedAddress"": ""SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7""}","{""ownerCanCallAny"": true, ""adminsCanCallAdminFunctions"": true, ""operatorsCanCallOperatorFunctions"": true, ""roleChecksBeforeLogic"": true, ""clearErrorMessages"": true}","- ALWAYS check tx-sender, NOT contract-caller
|
|
2305
|
+
- DEFINE clear role hierarchy (owner > admin > operator)
|
|
2306
|
+
- USE read-only helpers for role checks
|
|
2307
|
+
- FAIL FAST with asserts! at function start
|
|
2308
|
+
- NEVER trust user-provided principal for auth
|
|
2309
|
+
- IMPLEMENT two-step ownership transfer for safety
|
|
2310
|
+
- LOG all privilege changes
|
|
2311
|
+
- DOCUMENT role requirements clearly",https://github.com/OpenZeppelin/cairo-contracts/blob/main/src/openzeppelin/access/accesscontrol/library.cairo,"security-patterns.csv:3,clarity-syntax.csv:31,stacks-js-core.csv:8","security,access-control,rbac,authorization,permissions,best-practice,intermediate",intermediate
|
|
2312
|
+
32,security,best-practice,input-validation,"Validate and sanitize all user inputs with bounds checking, type validation, and business logic constraints","// Input validation comprehensive
|
|
2313
|
+
const INPUT_VALIDATION_CLARITY = `
|
|
2314
|
+
(define-constant ERR-INVALID-AMOUNT (err u400))
|
|
2315
|
+
(define-constant ERR-INVALID-ADDRESS (err u401))
|
|
2316
|
+
(define-constant ERR-INVALID-STRING (err u402))
|
|
2317
|
+
|
|
2318
|
+
(define-private (validate-amount (amount uint))
|
|
2319
|
+
(begin
|
|
2320
|
+
(asserts! (> amount u0) ERR-INVALID-AMOUNT)
|
|
2321
|
+
(asserts! (< amount u1000000000000) ERR-INVALID-AMOUNT)
|
|
2322
|
+
(ok amount)
|
|
2323
|
+
)
|
|
2324
|
+
)
|
|
2325
|
+
|
|
2326
|
+
(define-private (validate-address (addr principal))
|
|
2327
|
+
(begin
|
|
2328
|
+
(asserts! (not (is-eq addr 'SP000000000000000000002Q6VF78)) ERR-INVALID-ADDRESS)
|
|
2329
|
+
(asserts! (not (is-eq addr tx-sender)) ERR-INVALID-ADDRESS)
|
|
2330
|
+
(ok addr)
|
|
2331
|
+
)
|
|
2332
|
+
)
|
|
2333
|
+
|
|
2334
|
+
(define-public (transfer (amount uint) (recipient principal))
|
|
2335
|
+
(begin
|
|
2336
|
+
(try! (validate-amount amount))
|
|
2337
|
+
(try! (validate-address recipient))
|
|
2338
|
+
(ft-transfer? my-token amount tx-sender recipient)
|
|
2339
|
+
)
|
|
2340
|
+
)
|
|
2341
|
+
`;","Production input validation patterns. Validates amounts, addresses, strings, and business logic constraints before execution.","{""invalidAmounts"": [0, 500, 999999999999999999], ""validAmount"": 1000000, ""invalidAddress"": ""SP000000000000000000002Q6VF78"", ""selfTransferAddress"": ""tx-sender"", ""emptyString"": """", ""longString"": ""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"", ""validString"": ""valid memo"", ""emptyList"": [], ""largeList"": [""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item"", ""item""], ""invalidPercentage"": 10001}","{""zeroAmountError"": ""ERR-INVALID-AMOUNT"", ""lowAmountError"": ""ERR-INVALID-AMOUNT"", ""highAmountError"": ""ERR-OVERFLOW"", ""invalidAddressError"": ""ERR-INVALID-ADDRESS"", ""selfTransferError"": ""ERR-INVALID-ADDRESS"", ""emptyStringError"": ""ERR-INVALID-STRING"", ""longStringError"": ""ERR-INVALID-STRING"", ""emptyListError"": ""ERR-OUT-OF-BOUNDS"", ""largeListError"": ""ERR-OUT-OF-BOUNDS"", ""invalidPercentageError"": ""ERR-INVALID-PERCENTAGE"", ""validInputsSuccess"": true}","- VALIDATE all inputs before state changes
|
|
2342
|
+
- CHECK bounds (min/max) for numeric values
|
|
2343
|
+
- VERIFY principal addresses (not zero, not self)
|
|
2344
|
+
- LIMIT string lengths to prevent DoS
|
|
2345
|
+
- VALIDATE list lengths before iteration
|
|
2346
|
+
- USE helper functions for reusability
|
|
2347
|
+
- DEFINE clear validation constants
|
|
2348
|
+
- CLIENT validation is UX, not security",https://github.com/crytic/building-secure-contracts/blob/master/development-guidelines/token_integration.md,"security-patterns.csv:5,clarity-syntax.csv:17,clarity-syntax.csv:33","security,validation,input-sanitization,bounds-checking,best-practice,intermediate",intermediate
|
|
2349
|
+
33,security,security,rate-limiting-dos-protection,"Prevent Denial of Service attacks using rate limiting, gas accounting, and complexity bounds","// Rate limiting for DoS protection
|
|
2350
|
+
const RATE_LIMIT_CLARITY = `
|
|
2351
|
+
(define-map rate-limits principal {count: uint, window-start: uint})
|
|
2352
|
+
(define-constant max-calls-per-window u10)
|
|
2353
|
+
(define-constant window-blocks u144) ;; ~24 hours
|
|
2354
|
+
|
|
2355
|
+
(define-private (check-rate-limit)
|
|
2356
|
+
(let (
|
|
2357
|
+
(limit (default-to {count: u0, window-start: block-height}
|
|
2358
|
+
(map-get? rate-limits tx-sender)))
|
|
2359
|
+
(blocks-passed (- block-height (get window-start limit)))
|
|
2360
|
+
)
|
|
2361
|
+
(if (>= blocks-passed window-blocks)
|
|
2362
|
+
(begin
|
|
2363
|
+
(map-set rate-limits tx-sender {count: u1, window-start: block-height})
|
|
2364
|
+
(ok true)
|
|
2365
|
+
)
|
|
2366
|
+
(begin
|
|
2367
|
+
(asserts! (< (get count limit) max-calls-per-window) (err u429))
|
|
2368
|
+
(map-set rate-limits tx-sender
|
|
2369
|
+
(merge limit {count: (+ (get count limit) u1)}))
|
|
2370
|
+
(ok true)
|
|
2371
|
+
)
|
|
2372
|
+
)
|
|
2373
|
+
)
|
|
2374
|
+
)
|
|
2375
|
+
`;",Production rate limiting to prevent DoS attacks. Tracks calls per wallet in rolling time window and enforces limits.,"{""action_11_same_block"": ""11th action"", ""expensive_op_cooldown"": ""within 6 blocks"", ""batch_size_51"": ""51 items"", ""global_actions_1001"": ""1001st action"", ""normal_action"": ""1st action""}","{""rate_limit_exceeded"": ""err-u301 rate-limit"", ""cooldown_active"": ""err-u302 cooldown-active"", ""batch_too_large"": ""err-u303 too-complex"", ""global_limit"": ""err-u301 rate-limit"", ""normal_success"": ""ok true""}","- IMPLEMENT per-user AND global rate limits
|
|
2376
|
+
- USE block-height for automatic counter resets
|
|
2377
|
+
- ENFORCE maximum batch sizes (e.g., 50)
|
|
2378
|
+
- ADD cooldowns for expensive operations
|
|
2379
|
+
- NEVER allow unbounded loops
|
|
2380
|
+
- TRACK gas consumption awareness
|
|
2381
|
+
- CONSIDER economic costs as deterrent
|
|
2382
|
+
- MONITOR for abuse patterns off-chain",https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/,"security-patterns.csv:7,clarity-syntax.csv:42,advanced-patterns.csv:8","security,dos-protection,rate-limiting,gas-optimization,complexity-bounds,vulnerability,advanced",advanced
|
|
2383
|
+
34,security,security,secure-randomness,Generate secure randomness using VRF (Verifiable Random Function) instead of predictable block properties,"// Secure randomness using VRF
|
|
2384
|
+
const SECURE_RANDOM_CLARITY = `
|
|
2385
|
+
(define-data-var nonce uint u0)
|
|
2386
|
+
|
|
2387
|
+
(define-read-only (get-pseudo-random (max uint))
|
|
2388
|
+
(let (
|
|
2389
|
+
(seed (buff-to-uint-be (hash-sha256 (concat
|
|
2390
|
+
(unwrap-panic (to-consensus-buff? block-height))
|
|
2391
|
+
(unwrap-panic (to-consensus-buff? (var-get nonce)))
|
|
2392
|
+
))))
|
|
2393
|
+
)
|
|
2394
|
+
(ok (mod seed max))
|
|
2395
|
+
)
|
|
2396
|
+
)
|
|
2397
|
+
|
|
2398
|
+
(define-public (use-randomness)
|
|
2399
|
+
(let ((random (unwrap! (get-pseudo-random u100) (err u500))))
|
|
2400
|
+
(var-set nonce (+ (var-get nonce) u1))
|
|
2401
|
+
(ok random)
|
|
2402
|
+
)
|
|
2403
|
+
)
|
|
2404
|
+
`;","Production secure randomness using block hash + nonce. Not truly random but prevents prediction. For critical randomness, use VRF oracles.","{""vulnerable_block_hash"": ""height 12345"", ""vulnerable_tx_sender"": ""attacker controlled"", ""commit_reveal"": ""10 participants, 6 block delay"", ""vrf_proof"": ""valid VRF proof with public key""}","{""block_hash_result"": ""predictable, miner manipulable"", ""tx_sender_result"": ""attacker selects favorable address"", ""commit_reveal_result"": ""secure if majority honest"", ""vrf_result"": ""cryptographically secure, verifiable"", ""random_range"": ""0-99 uniform distribution""}","- NEVER use block-hash for randomness
|
|
2405
|
+
- NEVER use tx-sender for randomness
|
|
2406
|
+
- USE VRF with verifiable proofs (best)
|
|
2407
|
+
- IMPLEMENT commit-reveal with delay
|
|
2408
|
+
- COMBINE multiple randomness sources
|
|
2409
|
+
- REQUIRE reveal delay (6+ blocks)
|
|
2410
|
+
- VERIFY VRF proofs on-chain
|
|
2411
|
+
- CONSIDER oracle integration for VRF",https://blog.chain.link/chainlink-vrf-on-chain-verifiable-randomness/,"security-patterns.csv:10,clarity-syntax.csv:47,oracles.csv:15","security,randomness,vrf,commit-reveal,lottery,vulnerability,advanced",advanced
|
|
2412
|
+
35,security,security,privilege-escalation-prevention,"Prevent privilege escalation attacks through proper role validation, state management, and ownership transfer patterns","// Privilege escalation prevention
|
|
2413
|
+
const PRIVILEGE_ESCALATION_CLARITY = `
|
|
2414
|
+
(define-constant ERR-PRIVILEGE-ESCALATION (err u403))
|
|
2415
|
+
|
|
2416
|
+
(define-map user-roles principal (string-ascii 20))
|
|
2417
|
+
|
|
2418
|
+
(define-public (promote-user (user principal) (new-role (string-ascii 20)))
|
|
2419
|
+
(let (
|
|
2420
|
+
(caller-role (default-to ""none"" (map-get? user-roles tx-sender)))
|
|
2421
|
+
(target-role (default-to ""none"" (map-get? user-roles user)))
|
|
2422
|
+
)
|
|
2423
|
+
;; Prevent self-promotion
|
|
2424
|
+
(asserts! (not (is-eq tx-sender user)) ERR-PRIVILEGE-ESCALATION)
|
|
2425
|
+
|
|
2426
|
+
;; Prevent promoting to same or higher role
|
|
2427
|
+
(asserts! (not (is-eq caller-role target-role)) ERR-PRIVILEGE-ESCALATION)
|
|
2428
|
+
|
|
2429
|
+
;; Only admin can promote to admin
|
|
2430
|
+
(if (is-eq new-role ""admin"")
|
|
2431
|
+
(asserts! (is-eq caller-role ""owner"") ERR-PRIVILEGE-ESCALATION)
|
|
2432
|
+
true
|
|
2433
|
+
)
|
|
2434
|
+
|
|
2435
|
+
(map-set user-roles user new-role)
|
|
2436
|
+
(ok true)
|
|
2437
|
+
)
|
|
2438
|
+
)
|
|
2439
|
+
`;",Production privilege escalation prevention. Users cannot promote themselves or promote others to equal/higher roles.,"{""vulnerable_transfer"": ""SP_TYPO_ADDRESS"", ""vulnerable_escalation"": ""admin adds self as owner"", ""secure_transfer"": ""two-step with acceptance"", ""timelock_execution"": ""before delay period"", ""role_escalation"": ""operator tries admin role""}","{""vulnerable_transfer"": ""ownership lost permanently"", ""vulnerable_escalation"": ""admin becomes owner"", ""secure_transfer"": ""pending until acceptance"", ""timelock_fail"": ""err-u609 before delay"", ""role_fail"": ""err-u605 insufficient level"", ""events"": ""all privilege changes logged""}","- ALWAYS use two-step ownership transfer
|
|
2440
|
+
- REQUIRE explicit acceptance from new owner
|
|
2441
|
+
- IMPLEMENT role hierarchy (level-based)
|
|
2442
|
+
- USE timelocks for critical upgrades (24+ hours)
|
|
2443
|
+
- PREVENT admins from self-elevation
|
|
2444
|
+
- LOG all privilege changes with events
|
|
2445
|
+
- ALLOW ownership transfer cancellation
|
|
2446
|
+
- NEVER skip validation for ""trusted"" users",https://docs.openzeppelin.com/contracts/4.x/api/access,"security-patterns.csv:3,security-patterns.csv:6,clarity-syntax.csv:31","security,privilege-escalation,ownership,timelock,role-hierarchy,vulnerability,advanced",advanced
|
|
2447
|
+
36,auth,quickstart,wallet-connect-flow,Complete wallet connection flow with address retrieval for STX and BTC addresses,"// Production wallet connection pattern from sbtc-market-frontend
|
|
2448
|
+
export async function connectWallet() {
|
|
2449
|
+
const { connect, isConnected, getLocalStorage } = await import(""@stacks/connect"");
|
|
2450
|
+
|
|
2451
|
+
// Check if already connected
|
|
2452
|
+
if (isConnected()) {
|
|
2453
|
+
const userData = getLocalStorage();
|
|
2454
|
+
console.log('Already authenticated');
|
|
2455
|
+
|
|
2456
|
+
// Access STX and BTC addresses
|
|
2457
|
+
if (userData?.addresses) {
|
|
2458
|
+
const stxAddress = userData.addresses.stx?.[0]?.address;
|
|
2459
|
+
const btcAddress = userData.addresses.btc?.[0]?.address;
|
|
2460
|
+
console.log('STX Address:', stxAddress);
|
|
2461
|
+
console.log('BTC Address:', btcAddress);
|
|
2462
|
+
|
|
2463
|
+
return { addresses: userData.addresses };
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// Connect if not connected
|
|
2468
|
+
return connect({
|
|
2469
|
+
onFinish: (payload) => {
|
|
2470
|
+
console.log('Connected:', payload.addresses);
|
|
2471
|
+
},
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// Helper: Get just the STX address
|
|
2476
|
+
export async function resolveStxAddress() {
|
|
2477
|
+
const { isConnected, getLocalStorage } = await import(""@stacks/connect"");
|
|
2478
|
+
if (!isConnected()) return null;
|
|
2479
|
+
|
|
2480
|
+
const data = getLocalStorage();
|
|
2481
|
+
return data?.addresses?.stx?.[0]?.address || null;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// Helper: Check connection status
|
|
2485
|
+
export async function isWalletConnected() {
|
|
2486
|
+
const { isConnected } = await import(""@stacks/connect"");
|
|
2487
|
+
return isConnected();
|
|
2488
|
+
}","Production wallet connection pattern from sbtc-market-frontend. Uses isConnected() to check before connecting, getLocalStorage() to retrieve addresses (both STX and BTC), and proper error handling.","{""walletInstalled"": true, ""userApproved"": true}","{""connected"": true, ""stxAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""btcAddress"": ""bc1q..."", ""addressesObject"": {""stx"": [{""address"": ""SP2C2YFP...""}], ""btc"": [{""address"": ""bc1q...""}]}}","- Not checking isConnected() before calling connect() causes unnecessary wallet popups
|
|
2489
|
+
- Forgetting to handle case when wallet extension is not installed
|
|
2490
|
+
- Not accessing userData.addresses correctly (it's nested: addresses.stx[0].address)
|
|
2491
|
+
- Using deprecated showConnect() instead of connect()",https://github.com/your-username/sbtc-market-frontend/blob/main/src/lib/wallet.ts,"authentication.csv:1,authentication.csv:2,authentication.csv:4,stacks-js-core.csv:1,stacks-js-core.csv:5","authentication,wallet,connect,quickstart,react,beginner",beginner
|
|
2492
|
+
37,auth,integration,jwt-authentication,Generate and verify JWT tokens from wallet signatures for secure server-side authentication,"// Production JWT authentication via wallet signature
|
|
2493
|
+
// From stacksagent-frontend/src/components/auth/WalletAuthenticator.tsx
|
|
2494
|
+
|
|
2495
|
+
import { request } from '@stacks/connect';
|
|
2496
|
+
|
|
2497
|
+
async function authenticateWalletWithJWT() {
|
|
2498
|
+
// 1. Generate timestamp and message
|
|
2499
|
+
const timestamp = Date.now();
|
|
2500
|
+
const message = `Authenticate with StacksAgent: ${timestamp}`;
|
|
2501
|
+
|
|
2502
|
+
try {
|
|
2503
|
+
// 2. Request signature from wallet
|
|
2504
|
+
const response = await request('stx_signMessage', {
|
|
2505
|
+
message,
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
const signature = response.signature;
|
|
2509
|
+
const publicKey = response.publicKey;
|
|
2510
|
+
|
|
2511
|
+
console.log('Signature obtained:', signature.substring(0, 20) + '...');
|
|
2512
|
+
|
|
2513
|
+
// 3. Send to backend for JWT generation
|
|
2514
|
+
const authResponse = await fetch('/api/auth/authenticate', {
|
|
2515
|
+
method: 'POST',
|
|
2516
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2517
|
+
body: JSON.stringify({
|
|
2518
|
+
message,
|
|
2519
|
+
signature,
|
|
2520
|
+
publicKey,
|
|
2521
|
+
timestamp
|
|
2522
|
+
})
|
|
2523
|
+
});
|
|
2524
|
+
|
|
2525
|
+
if (!authResponse.ok) {
|
|
2526
|
+
throw new Error('Server could not verify signature');
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// 4. Get JWT token from response
|
|
2530
|
+
const { token, expiresAt } = await authResponse.json();
|
|
2531
|
+
|
|
2532
|
+
// 5. Store JWT (httpOnly cookie preferred, or localStorage)
|
|
2533
|
+
document.cookie = `auth_token=${token}; path=/; secure; samesite=strict`;
|
|
2534
|
+
|
|
2535
|
+
console.log('✅ Authenticated successfully');
|
|
2536
|
+
console.log('Token expires:', new Date(expiresAt));
|
|
2537
|
+
|
|
2538
|
+
return { success: true, token, expiresAt };
|
|
2539
|
+
|
|
2540
|
+
} catch (error) {
|
|
2541
|
+
if (error.message.includes('User denied')) {
|
|
2542
|
+
console.error('User cancelled signature request');
|
|
2543
|
+
} else {
|
|
2544
|
+
console.error('Authentication failed:', error.message);
|
|
2545
|
+
}
|
|
2546
|
+
return { success: false, error: error.message };
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// Backend verification (Node.js/Express example)
|
|
2551
|
+
/*
|
|
2552
|
+
import { verifyMessageSignatureRsv } from '@stacks/encryption';
|
|
2553
|
+
import jwt from 'jsonwebtoken';
|
|
2554
|
+
|
|
2555
|
+
app.post('/api/auth/authenticate', (req, res) => {
|
|
2556
|
+
const { message, signature, publicKey, timestamp } = req.body;
|
|
2557
|
+
|
|
2558
|
+
// 1. Verify timestamp freshness (within 5 minutes)
|
|
2559
|
+
const now = Date.now();
|
|
2560
|
+
if (Math.abs(now - timestamp) > 5 * 60 * 1000) {
|
|
2561
|
+
return res.status(401).json({ error: 'Timestamp expired' });
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// 2. Verify signature
|
|
2565
|
+
const isValid = verifyMessageSignatureRsv({ message, publicKey, signature });
|
|
2566
|
+
if (!isValid) {
|
|
2567
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// 3. Generate JWT
|
|
2571
|
+
const walletAddress = publicKeyToAddress(publicKey);
|
|
2572
|
+
const token = jwt.sign(
|
|
2573
|
+
{ address: walletAddress, publicKey },
|
|
2574
|
+
process.env.JWT_SECRET,
|
|
2575
|
+
{ expiresIn: '24h' }
|
|
2576
|
+
);
|
|
2577
|
+
|
|
2578
|
+
return res.json({
|
|
2579
|
+
token,
|
|
2580
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1000
|
|
2581
|
+
});
|
|
2582
|
+
});
|
|
2583
|
+
*/","Production JWT authentication pattern from stacksagent-frontend. User signs a timestamped message with their wallet, backend verifies signature and returns JWT token. Includes both frontend and backend code.","{""message"": ""Authenticate with StacksAgent: 1704067200000"", ""walletConnected"": true, ""userApprovesSignature"": true}","{""success"": true, ""token"": ""eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."", ""expiresAt"": 1704153600000, ""signature"": ""0x1234567890abcdef..."", ""publicKey"": ""03abcdef1234567890...""}","- Not validating timestamp freshness on backend (replay attacks)
|
|
2584
|
+
- Storing JWT in localStorage instead of httpOnly cookies (XSS vulnerability)
|
|
2585
|
+
- Not handling user rejection of signature request
|
|
2586
|
+
- Using weak JWT_SECRET or hardcoding it
|
|
2587
|
+
- Not verifying signature correctly on backend",https://github.com/your-username/stacksagent-frontend/blob/main/src/components/auth/WalletAuthenticator.tsx,"authentication.csv:1,authentication.csv:7,authentication.csv:8,stacks-js-core.csv:47,security-patterns.csv:1","authentication,jwt,signature,security,server-side,intermediate",intermediate
|
|
2588
|
+
38,auth,integration,protected-routes,Implement authentication guards for protected routes with automatic redirection and session validation,"// Production protected routes with wallet auth
|
|
2589
|
+
// Pattern from STX City deploy-stx-city
|
|
2590
|
+
|
|
2591
|
+
import { connect, getLocalStorage } from '@stacks/connect';
|
|
2592
|
+
import { create } from 'zustand';
|
|
2593
|
+
import { persist } from 'zustand/middleware';
|
|
2594
|
+
|
|
2595
|
+
// Zustand store with localStorage persistence
|
|
2596
|
+
interface StacksStore {
|
|
2597
|
+
userData: {
|
|
2598
|
+
profile: {
|
|
2599
|
+
stxAddress: { mainnet: string; testnet: string };
|
|
2600
|
+
btcAddress: { mainnet: string; testnet: string };
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
isAuthenticated: boolean;
|
|
2604
|
+
handleLogIn: () => Promise<void>;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
const useStacksStore = create<StacksStore>()(
|
|
2608
|
+
persist(
|
|
2609
|
+
(set) => ({
|
|
2610
|
+
userData: {
|
|
2611
|
+
profile: {
|
|
2612
|
+
stxAddress: { mainnet: '', testnet: '' },
|
|
2613
|
+
btcAddress: { mainnet: '', testnet: '' }
|
|
2614
|
+
}
|
|
2615
|
+
},
|
|
2616
|
+
isAuthenticated: false,
|
|
2617
|
+
|
|
2618
|
+
async handleLogIn() {
|
|
2619
|
+
const connectResponse = await connect();
|
|
2620
|
+
const localData = getLocalStorage();
|
|
2621
|
+
|
|
2622
|
+
set({
|
|
2623
|
+
userData: {
|
|
2624
|
+
profile: {
|
|
2625
|
+
stxAddress: {
|
|
2626
|
+
mainnet: localData.addresses.stx[0].address,
|
|
2627
|
+
testnet: localData.addresses.stx[0].address
|
|
2628
|
+
},
|
|
2629
|
+
btcAddress: {
|
|
2630
|
+
mainnet: localData.addresses.btc[0].address,
|
|
2631
|
+
testnet: localData.addresses.btc[0].address
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
},
|
|
2635
|
+
isAuthenticated: true
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
}),
|
|
2639
|
+
{
|
|
2640
|
+
name: 'stacks-storage',
|
|
2641
|
+
getStorage: () => localStorage
|
|
2642
|
+
}
|
|
2643
|
+
)
|
|
2644
|
+
);
|
|
2645
|
+
|
|
2646
|
+
// React Router protected route component
|
|
2647
|
+
import { Navigate } from 'react-router-dom';
|
|
2648
|
+
|
|
2649
|
+
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
2650
|
+
const { isAuthenticated, userData } = useStacksStore();
|
|
2651
|
+
|
|
2652
|
+
// Check if user is authenticated
|
|
2653
|
+
if (!isAuthenticated || !userData.profile.stxAddress.mainnet) {
|
|
2654
|
+
// Redirect to login page
|
|
2655
|
+
return <Navigate to=""/login"" replace />;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
return <>{children}</>;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// Usage in routes
|
|
2662
|
+
/*
|
|
2663
|
+
<Routes>
|
|
2664
|
+
<Route path=""/login"" element={<LoginPage />} />
|
|
2665
|
+
<Route path=""/dashboard"" element={
|
|
2666
|
+
<ProtectedRoute>
|
|
2667
|
+
<Dashboard />
|
|
2668
|
+
</ProtectedRoute>
|
|
2669
|
+
} />
|
|
2670
|
+
<Route path=""/profile"" element={
|
|
2671
|
+
<ProtectedRoute>
|
|
2672
|
+
<ProfilePage />
|
|
2673
|
+
</ProtectedRoute>
|
|
2674
|
+
} />
|
|
2675
|
+
</Routes>
|
|
2676
|
+
*/",Production protected routes from STX City using Zustand with localStorage persistence. Reconnects wallet state across reloads and guards routes with authentication checks.,"{""userConnected"": true, ""stxAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""requestedRoute"": ""/dashboard"", ""sessionPersisted"": true}","{""accessGranted"": true, ""redirectTo"": null, ""sessionRestored"": true, ""addressesAvailable"": {""stx"": ""SP2C2YFP..."", ""btc"": ""bc1q...""}}","- Not persisting state causes logout on refresh
|
|
2677
|
+
- Forgetting to check isAuthenticated AND address presence
|
|
2678
|
+
- Not using replace in Navigate causes back button issues
|
|
2679
|
+
- Hardcoding redirect paths instead of using location.state
|
|
2680
|
+
- Not handling wallet disconnection during session",https://github.com/stx-city/deploy-stx-city/blob/main/src/store/StacksStore.ts,"authentication.csv:5,authentication.csv:6,authentication.csv:8,stacks-js-core.csv:2,stacks-js-core.csv:4","authentication,routing,middleware,protected,nextjs,react,intermediate",intermediate
|
|
2681
|
+
39,auth,integration,nft-token-gating,Verify NFT ownership on-chain to gate premium content and features,"// Production NFT ownership verification for gating
|
|
2682
|
+
// Pattern from STX City token metadata and portfolio patterns
|
|
2683
|
+
|
|
2684
|
+
import { callReadOnlyFunction, principalCV, uintCV, cvToJSON } from '@stacks/transactions';
|
|
2685
|
+
import axios from 'axios';
|
|
2686
|
+
|
|
2687
|
+
// Check NFT ownership using read-only function
|
|
2688
|
+
async function checkNFTOwnership(
|
|
2689
|
+
walletAddress: string,
|
|
2690
|
+
nftContract: string,
|
|
2691
|
+
tokenId: number
|
|
2692
|
+
): Promise<boolean> {
|
|
2693
|
+
try {
|
|
2694
|
+
const [contractAddress, contractName] = nftContract.split('.');
|
|
2695
|
+
|
|
2696
|
+
// Call get-owner read-only function
|
|
2697
|
+
const result = await callReadOnlyFunction({
|
|
2698
|
+
contractAddress,
|
|
2699
|
+
contractName,
|
|
2700
|
+
functionName: 'get-owner',
|
|
2701
|
+
functionArgs: [uintCV(tokenId)],
|
|
2702
|
+
senderAddress: walletAddress,
|
|
2703
|
+
network: 'mainnet'
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
const owner = cvToJSON(result).value.value;
|
|
2707
|
+
return owner === walletAddress;
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
console.error('NFT ownership check failed:', error);
|
|
2710
|
+
return false;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// Check if wallet owns any NFT from collection
|
|
2715
|
+
async function hasCollectionAccess(
|
|
2716
|
+
walletAddress: string,
|
|
2717
|
+
collectionContract: string
|
|
2718
|
+
): Promise<boolean> {
|
|
2719
|
+
try {
|
|
2720
|
+
// Use Hiro API to get all NFT holdings
|
|
2721
|
+
const headers = { 'x-hiro-api-key': process.env.HIRO_API_KEY };
|
|
2722
|
+
const { data } = await axios.get(
|
|
2723
|
+
`https://api.hiro.so/extended/v1/tokens/nft/holdings?principal=${walletAddress}`,
|
|
2724
|
+
{ headers }
|
|
2725
|
+
);
|
|
2726
|
+
|
|
2727
|
+
// Check if any NFT is from the target collection
|
|
2728
|
+
const hasNFT = data.results.some((nft: any) =>
|
|
2729
|
+
nft.asset_identifier.startsWith(collectionContract)
|
|
2730
|
+
);
|
|
2731
|
+
|
|
2732
|
+
return hasNFT;
|
|
2733
|
+
} catch (error) {
|
|
2734
|
+
console.error('Collection check failed:', error);
|
|
2735
|
+
return false;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// Express.js middleware for NFT-gated routes
|
|
2740
|
+
/*
|
|
2741
|
+
async function nftGateMiddleware(req, res, next) {
|
|
2742
|
+
const walletAddress = req.session.walletAddress;
|
|
2743
|
+
const requiredCollection = 'SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2';
|
|
2744
|
+
|
|
2745
|
+
if (!walletAddress) {
|
|
2746
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
const hasAccess = await hasCollectionAccess(walletAddress, requiredCollection);
|
|
2750
|
+
|
|
2751
|
+
if (!hasAccess) {
|
|
2752
|
+
return res.status(403).json({
|
|
2753
|
+
error: 'NFT required',
|
|
2754
|
+
message: 'You must own a Stacks Punks NFT to access this feature'
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
next();
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// Protected route
|
|
2762
|
+
app.get('/api/premium/data', nftGateMiddleware, (req, res) => {
|
|
2763
|
+
res.json({ data: 'Premium content for NFT holders' });
|
|
2764
|
+
});
|
|
2765
|
+
*/",Production NFT gating using both read-only contract calls and Hiro API. Verifies specific NFT ownership or collection membership for premium feature access.,"{""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""nftContract"": ""SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.stacks-punks-v2"", ""requiredTokenId"": 42, ""userOwnsToken"": true}","{""hasAccess"": true, ""ownershipVerified"": true, ""nftDetails"": {""collection"": ""Stacks Punks"", ""tokenId"": 42}, ""accessGranted"": true}","- Not handling NFT transfers (ownership can change)
|
|
2766
|
+
- Relying only on frontend checks (bypass via devtools)
|
|
2767
|
+
- Not caching results causes rate limiting on Hiro API
|
|
2768
|
+
- Forgetting to validate contract is legitimate NFT collection
|
|
2769
|
+
- Not handling NFT not found errors gracefully",https://github.com/stx-city/deploy-stx-city/blob/main/src/lib/curveDetailUtils.ts,"authentication.csv:12,nfts.csv:5,stacks-js-core.csv:38,security-patterns.csv:1","authentication,nft,token-gating,access-control,sip009,intermediate",intermediate
|
|
2770
|
+
40,auth,best-practice,session-management,Implement secure server-side session storage with wallet address binding and automatic cleanup,"// Production session management with wallet binding
|
|
2771
|
+
// Pattern from STX City wallet auth and server security
|
|
2772
|
+
|
|
2773
|
+
import { randomBytes } from 'crypto';
|
|
2774
|
+
import { verifyMessageSignatureRsv } from '@stacks/encryption';
|
|
2775
|
+
|
|
2776
|
+
interface Session {
|
|
2777
|
+
id: string;
|
|
2778
|
+
walletAddress: string;
|
|
2779
|
+
publicKey: string;
|
|
2780
|
+
createdAt: number;
|
|
2781
|
+
expiresAt: number;
|
|
2782
|
+
lastActivity: number;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// In-memory session store (use Redis in production)
|
|
2786
|
+
const sessions = new Map<string, Session>();
|
|
2787
|
+
|
|
2788
|
+
// Session configuration
|
|
2789
|
+
const SESSION_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
|
2790
|
+
const ACTIVITY_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
|
2791
|
+
const CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
2792
|
+
|
|
2793
|
+
// Create session after wallet signature verification
|
|
2794
|
+
export async function createSession(
|
|
2795
|
+
message: string,
|
|
2796
|
+
signature: string,
|
|
2797
|
+
publicKey: string
|
|
2798
|
+
): Promise<{ sessionId: string; expiresAt: number } | null> {
|
|
2799
|
+
try {
|
|
2800
|
+
// 1. Verify signature
|
|
2801
|
+
const isValid = verifyMessageSignatureRsv({ message, publicKey, signature });
|
|
2802
|
+
if (!isValid) {
|
|
2803
|
+
console.error('Invalid signature');
|
|
2804
|
+
return null;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// 2. Extract wallet address from public key
|
|
2808
|
+
const { publicKeyToAddress } = await import('@stacks/transactions');
|
|
2809
|
+
const walletAddress = publicKeyToAddress(publicKey);
|
|
2810
|
+
|
|
2811
|
+
// 3. Generate secure session ID
|
|
2812
|
+
const sessionId = randomBytes(32).toString('hex');
|
|
2813
|
+
|
|
2814
|
+
// 4. Create session
|
|
2815
|
+
const now = Date.now();
|
|
2816
|
+
const session: Session = {
|
|
2817
|
+
id: sessionId,
|
|
2818
|
+
walletAddress,
|
|
2819
|
+
publicKey,
|
|
2820
|
+
createdAt: now,
|
|
2821
|
+
expiresAt: now + SESSION_DURATION,
|
|
2822
|
+
lastActivity: now
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
sessions.set(sessionId, session);
|
|
2826
|
+
|
|
2827
|
+
console.log(`✅ Session created for ${walletAddress}`);
|
|
2828
|
+
return { sessionId, expiresAt: session.expiresAt };
|
|
2829
|
+
|
|
2830
|
+
} catch (error) {
|
|
2831
|
+
console.error('Session creation failed:', error);
|
|
2832
|
+
return null;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// Validate and refresh session
|
|
2837
|
+
export function validateSession(sessionId: string): Session | null {
|
|
2838
|
+
const session = sessions.get(sessionId);
|
|
2839
|
+
|
|
2840
|
+
if (!session) {
|
|
2841
|
+
return null;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
const now = Date.now();
|
|
2845
|
+
|
|
2846
|
+
// Check if session expired
|
|
2847
|
+
if (now > session.expiresAt) {
|
|
2848
|
+
sessions.delete(sessionId);
|
|
2849
|
+
return null;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// Check activity timeout
|
|
2853
|
+
if (now - session.lastActivity > ACTIVITY_TIMEOUT) {
|
|
2854
|
+
sessions.delete(sessionId);
|
|
2855
|
+
return null;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// Update last activity
|
|
2859
|
+
session.lastActivity = now;
|
|
2860
|
+
sessions.set(sessionId, session);
|
|
2861
|
+
|
|
2862
|
+
return session;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// Cleanup expired sessions (run periodically)
|
|
2866
|
+
export function cleanupExpiredSessions() {
|
|
2867
|
+
const now = Date.now();
|
|
2868
|
+
let cleaned = 0;
|
|
2869
|
+
|
|
2870
|
+
for (const [sessionId, session] of sessions.entries()) {
|
|
2871
|
+
if (now > session.expiresAt || now - session.lastActivity > ACTIVITY_TIMEOUT) {
|
|
2872
|
+
sessions.delete(sessionId);
|
|
2873
|
+
cleaned++;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
if (cleaned > 0) {
|
|
2878
|
+
console.log(`🧹 Cleaned up ${cleaned} expired sessions`);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// Start cleanup interval
|
|
2883
|
+
setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL);
|
|
2884
|
+
|
|
2885
|
+
// Express.js middleware
|
|
2886
|
+
/*
|
|
2887
|
+
function sessionMiddleware(req, res, next) {
|
|
2888
|
+
const sessionId = req.cookies.session_id;
|
|
2889
|
+
|
|
2890
|
+
if (!sessionId) {
|
|
2891
|
+
return res.status(401).json({ error: 'No session' });
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
const session = validateSession(sessionId);
|
|
2895
|
+
|
|
2896
|
+
if (!session) {
|
|
2897
|
+
res.clearCookie('session_id');
|
|
2898
|
+
return res.status(401).json({ error: 'Invalid or expired session' });
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
req.session = session;
|
|
2902
|
+
req.walletAddress = session.walletAddress;
|
|
2903
|
+
next();
|
|
2904
|
+
}
|
|
2905
|
+
*/","Production session management with wallet address binding and automatic cleanup. Uses httpOnly cookies, validates signatures, and implements activity timeouts.","{""message"": ""Authenticate: 1704067200000"", ""signature"": ""0x1234567890abcdef..."", ""publicKey"": ""03abcdef..."", ""signatureValid"": true}","{""sessionId"": ""a1b2c3d4e5f6..."", ""expiresAt"": 1704153600000, ""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""sessionCreated"": true, ""cookieSet"": true}","- Using localStorage for sessions (XSS vulnerability)
|
|
2906
|
+
- Not implementing activity timeout allows stale sessions
|
|
2907
|
+
- Forgetting cleanup causes memory leaks
|
|
2908
|
+
- Not binding session to wallet address (session hijacking)
|
|
2909
|
+
- Using weak session ID generation (predictable IDs)",https://github.com/stx-city/deploy-stx-city/blob/main/src/store/StacksStore.ts,"authentication.csv:2,authentication.csv:9,security-patterns.csv:1,security-patterns.csv:4","authentication,session,redis,security,cookies,best-practice,intermediate",intermediate
|
|
2910
|
+
41,deployment,quickstart,deploy-contract-with-fees,Deploy Clarity contract to mainnet with post-condition protection,"// Production contract deployment from STX City deploy page
|
|
2911
|
+
// Pattern from /Volumes/Projects/STXCITY/deploy-stx-city/src/app/deploy/create/page.tsx
|
|
2912
|
+
|
|
2913
|
+
import { request } from '@stacks/connect';
|
|
2914
|
+
import { Pc, PostConditionMode } from '@stacks/transactions';
|
|
2915
|
+
|
|
2916
|
+
// Option 1: Deploy with STX payment
|
|
2917
|
+
async function deployContractWithSTX(
|
|
2918
|
+
walletAddress: string,
|
|
2919
|
+
contractName: string,
|
|
2920
|
+
clarityCode: string,
|
|
2921
|
+
deployFee: number = 2000000 // 2 STX in micro-STX
|
|
2922
|
+
) {
|
|
2923
|
+
// Create post-condition: Wallet will send at most deployFee STX
|
|
2924
|
+
const sendSTXPostCondition = Pc.principal(walletAddress)
|
|
2925
|
+
.willSendLte(deployFee)
|
|
2926
|
+
.ustx();
|
|
2927
|
+
|
|
2928
|
+
const deployResponse = await request('stx_deployContract', {
|
|
2929
|
+
name: contractName,
|
|
2930
|
+
clarityCode: clarityCode,
|
|
2931
|
+
clarityVersion: 3,
|
|
2932
|
+
network: 'mainnet',
|
|
2933
|
+
postConditions: [sendSTXPostCondition],
|
|
2934
|
+
postConditionMode: 'deny',
|
|
2935
|
+
fee: 100000 // Transaction fee (0.1 STX)
|
|
2936
|
+
});
|
|
2937
|
+
|
|
2938
|
+
if (deployResponse) {
|
|
2939
|
+
console.log('✅ Contract deployed successfully!');
|
|
2940
|
+
console.log('Transaction ID:', deployResponse.txid);
|
|
2941
|
+
console.log('View at:', `https://explorer.hiro.so/txid/${deployResponse.txid}?chain=mainnet`);
|
|
2942
|
+
return deployResponse.txid;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// Option 2: Deploy with token payment (e.g., WELSH, VELAR)
|
|
2947
|
+
async function deployContractWithToken(
|
|
2948
|
+
walletAddress: string,
|
|
2949
|
+
contractName: string,
|
|
2950
|
+
clarityCode: string,
|
|
2951
|
+
tokenContractId: string, // e.g., 'SP3NE50GEXFG9SZGTT51P40X2CKYSZ5CC4ZTZ7A2G.welshcorgicoin-token'
|
|
2952
|
+
assetName: string, // e.g., 'welshcorgicoin'
|
|
2953
|
+
tokenAmount: number, // Token amount with decimals
|
|
2954
|
+
) {
|
|
2955
|
+
// Create post-condition: Wallet will send at most tokenAmount of token
|
|
2956
|
+
const sendFTCondition = Pc.principal(walletAddress)
|
|
2957
|
+
.willSendLte(tokenAmount)
|
|
2958
|
+
.ft(tokenContractId, assetName);
|
|
2959
|
+
|
|
2960
|
+
const deployResponse = await request('stx_deployContract', {
|
|
2961
|
+
name: contractName,
|
|
2962
|
+
clarityCode: clarityCode,
|
|
2963
|
+
clarityVersion: 3,
|
|
2964
|
+
network: 'mainnet',
|
|
2965
|
+
postConditions: [sendFTCondition],
|
|
2966
|
+
postConditionMode: 'deny',
|
|
2967
|
+
fee: 100000
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
if (deployResponse) {
|
|
2971
|
+
console.log('✅ Contract deployed with token payment!');
|
|
2972
|
+
console.log('Transaction ID:', deployResponse.txid);
|
|
2973
|
+
return deployResponse.txid;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
// Example usage: Deploy SIP-010 token contract
|
|
2978
|
+
async function deploySIP10Token() {
|
|
2979
|
+
const walletAddress = 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR';
|
|
2980
|
+
const contractName = 'my-awesome-token';
|
|
2981
|
+
|
|
2982
|
+
// Sample SIP-010 contract code (simplified)
|
|
2983
|
+
const clarityCode = `
|
|
2984
|
+
;; SIP-010 Token Contract
|
|
2985
|
+
(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
|
|
2986
|
+
|
|
2987
|
+
(define-fungible-token my-token u1000000000)
|
|
2988
|
+
(define-constant contract-owner tx-sender)
|
|
2989
|
+
|
|
2990
|
+
(define-read-only (get-name)
|
|
2991
|
+
(ok ""My Awesome Token""))
|
|
2992
|
+
|
|
2993
|
+
(define-read-only (get-symbol)
|
|
2994
|
+
(ok ""MAT""))
|
|
2995
|
+
|
|
2996
|
+
(define-read-only (get-decimals)
|
|
2997
|
+
(ok u6))
|
|
2998
|
+
|
|
2999
|
+
(define-read-only (get-balance (who principal))
|
|
3000
|
+
(ok (ft-get-balance my-token who)))
|
|
3001
|
+
|
|
3002
|
+
(define-read-only (get-total-supply)
|
|
3003
|
+
(ok (ft-get-supply my-token)))
|
|
3004
|
+
|
|
3005
|
+
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
|
|
3006
|
+
(begin
|
|
3007
|
+
(asserts! (is-eq tx-sender sender) (err u4))
|
|
3008
|
+
(try! (ft-transfer? my-token amount sender recipient))
|
|
3009
|
+
(match memo to-print (print to-print) 0x)
|
|
3010
|
+
(ok true)
|
|
3011
|
+
))
|
|
3012
|
+
|
|
3013
|
+
;; Mint initial supply to contract owner
|
|
3014
|
+
(try! (ft-mint? my-token u1000000000 contract-owner))
|
|
3015
|
+
`;
|
|
3016
|
+
|
|
3017
|
+
// Deploy with 2 STX fee
|
|
3018
|
+
const txId = await deployContractWithSTX(
|
|
3019
|
+
walletAddress,
|
|
3020
|
+
contractName,
|
|
3021
|
+
clarityCode,
|
|
3022
|
+
2000000 // 2 STX
|
|
3023
|
+
);
|
|
3024
|
+
|
|
3025
|
+
return txId;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// Production pattern from STX City: Convert name to valid contract slug
|
|
3029
|
+
function convertToSlug(name: string): string {
|
|
3030
|
+
return name
|
|
3031
|
+
.toLowerCase()
|
|
3032
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
3033
|
+
.replace(/^-+|-+$/g, '');
|
|
3034
|
+
}","Production contract deployment pattern from STX City. Uses request('stx_deployContract', ...) with post-conditions to protect against excessive spending. Supports both STX and token payment methods. Always uses PostConditionMode.Deny for security.","{""walletAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"", ""contractName"": ""my-token"", ""clarityCode"": ""(define-fungible-token...)"", ""network"": ""mainnet"", ""deployFee"": 2000000}","{""txid"": ""0xabc123..."", ""success"": true, ""explorerUrl"": ""https://explorer.hiro.so/txid/0xabc123...?chain=mainnet"", ""contractAddress"": ""SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.my-token""}","- Forgetting post-conditions allows unlimited spending
|
|
3035
|
+
- Using PostConditionMode.Allow instead of Deny (security risk)
|
|
3036
|
+
- Not converting contract name to valid slug (deployment fails)
|
|
3037
|
+
- Setting fee too low causes transaction to be rejected
|
|
3038
|
+
- Forgetting clarityVersion parameter defaults to Clarity 2
|
|
3039
|
+
- Not handling deployResponse being null/undefined
|
|
3040
|
+
- Using testnet contract addresses on mainnet
|
|
3041
|
+
- Contract name must be lowercase alphanumeric + hyphens only",https://github.com/stx-city/deploy-stx-city/blob/main/src/app/deploy/create/page.tsx,"deployment.csv:1,deployment.csv:2,fungible-tokens.csv:1,security-patterns.csv:1","deploy,contract,stx_deployContract,post-conditions,mainnet,fees,security,quickstart,beginner",beginner
|