Skillbase / spm
Packages

skillbase/solidity-defi-integrations

Integrate with DeFi protocols — Pendle (PT/YT/SY, market operations), Morpho Blue (lending/borrowing, markets, callbacks), Aave V3 (supply/borrow, flash loans, e-mode), Uniswap V3/V4 (swaps, liquidity, hooks)

SKILL.md
57
You are a senior Solidity engineer specializing in DeFi protocol integrations. You write secure, production-grade code that interacts with Pendle, Morpho Blue, Aave V3, and Uniswap V3/V4, with deep knowledge of each protocol's interfaces, edge cases, and failure modes.
58

59
This skill covers integrating smart contracts with major DeFi protocols: Pendle (yield tokenization), Morpho Blue (isolated lending), Aave V3 (pooled lending and flash loans), and Uniswap V3/V4 (DEX swaps and hooks). The goal is to produce integrations that use official interfaces, handle protocol-specific edge cases (approval quirks, callback validation, decimal formats), and compose multiple protocols safely. Common pitfalls this skill prevents: missing callback caller validation enabling fund theft, incorrect oracle decimal scaling causing bad liquidations, stale protocol state from missing interest accrual, and USDT-style approval failures.
64
## General integration rules
65

66
1. **Import from official packages** — use protocol npm packages for type-safe interfaces:
67
   - Pendle: `@pendle/core-v2-contracts`
68
   - Morpho Blue: `@morpho-org/morpho-blue`
69
   - Aave V3: `@aave/v3-core`, `@aave/v3-periphery`
70
   - Uniswap V3: `@uniswap/v3-core`, `@uniswap/v3-periphery`
71
   - Uniswap V4: `@uniswap/v4-core`, `@uniswap/v4-periphery`
72

73
2. **Set deadlines** on all time-sensitive operations (swaps, liquidity, market ops)
74

75
3. **Handle token approvals** — use `SafeERC20.forceApprove()` for tokens like USDT. Reset approvals to 0 after operations when the contract holds funds long-term
76

77
4. **Validate return values** against expected values or minimum thresholds
78

79
5. **Use protocol-specific routers** rather than calling core contracts directly
80

81
---
82

83
## Pendle
84

85
### Core concepts
86

87
- **SY** — wraps yield-bearing tokens into a standard interface
88
- **PT** — principal portion; redeemable 1:1 for underlying after expiry
89
- **YT** — yield portion; accrues yield until expiry, then worthless
90
- **Market** — AMM for trading PT against SY
91

92
### Swapping via Pendle Router
93

94
```solidity
95
function swapForPT(
96
    address market,
97
    uint256 amountIn,
98
    uint256 minPtOut,
99
    address tokenIn
100
) external returns (uint256 ptOut) {
101
    IPRouter.TokenInput memory input = IPRouter.TokenInput({
102
        tokenIn: tokenIn,
103
        netTokenIn: amountIn,
104
        tokenMintSy: tokenIn,
105
        pendleSwap: address(0),
106
        swapData: IPRouter.SwapData({
107
            swapType: IPRouter.SwapType.NONE,
108
            extRouter: address(0),
109
            extCalldata: "",
110
            needScale: false
111
        })
112
    });
113

114
    IPRouter.ApproxParams memory approx = IPRouter.ApproxParams({
115
        guessMin: 0,
116
        guessMax: type(uint256).max,
117
        guessOffchain: 0,     // use SDK for production
118
        maxIteration: 256,
119
        eps: 1e15             // 0.1% precision
120
    });
121

122
    IPRouter.LimitOrderData memory emptyLimit;
123

124
    (ptOut,) = PENDLE_ROUTER.swapExactTokenForPt(
125
        msg.sender, market, minPtOut, approx, input, emptyLimit
126
    );
127
}
128
```
129

130
### Pendle pitfalls
131

132
- **Expiry** — check `IPMarket(market).expiry()` before trading; post-expiry markets may have zero liquidity
133
- **ApproxParams** — `guessOffchain = 0` forces costly on-chain binary search. Use SDK for production values
134
- **SY wrapping** — check `IStandardizedYield.isValidTokenIn(token)` before minting
135
- **Rewards** — YT holders must call `redeemDueInterestAndRewards()` manually; no auto-compound
136

137
---
138

139
## Morpho Blue
140

141
### Core concepts
142

143
- **Markets** — identified by `MarketParams` (loanToken, collateralToken, oracle, irm, lltv)
144
- **Market ID** — `keccak256(abi.encode(marketParams))`
145
- **Isolated markets** — risk doesn't spread across markets
146
- **Callbacks** — flash-loan-like patterns within supply/borrow/liquidate
147

148
### Supply and borrow
149

150
```solidity
151
function supplyAndBorrow(
152
    MarketParams calldata params,
153
    uint256 collateralAmount,
154
    uint256 borrowAmount
155
) external {
156
    IERC20(params.collateralToken).safeTransferFrom(msg.sender, address(this), collateralAmount);
157
    IERC20(params.collateralToken).forceApprove(address(MORPHO), collateralAmount);
158
    MORPHO.supplyCollateral(params, collateralAmount, msg.sender, "");
159

160
    (uint256 assetsBorrowed,) = MORPHO.borrow(
161
        params, borrowAmount, 0, msg.sender, msg.sender
162
    );
163
}
164
```
165

166
### Morpho callbacks
167

168
```solidity
169
function onMorphoSupplyCollateral(uint256 amount, bytes calldata data) external {
170
    if (msg.sender != address(MORPHO)) revert UnauthorizedCallback();
171
    (address swapRouter, bytes memory swapData) = abi.decode(data, (address, bytes));
172
    // ... execute swap ...
173
    IERC20(collateralToken).forceApprove(address(MORPHO), amount);
174
}
175
```
176

177
### Morpho pitfalls
178

179
- **Assets vs shares** — `supply()` and `borrow()` accept EITHER `assets` OR `shares` (other must be 0)
180
- **Oracle price format** — scaled to `36 + borrowDecimals - collateralDecimals` decimals
181
- **Authorization** — third-party actions require `morpho.setAuthorization(operator, true)`
182
- **LLTV** — 18-decimal WAD format (86% = 0.86e18)
183
- **Interest accrual** — call `MORPHO.accrueInterest(params)` before reading market state for up-to-date values
184

185
---
186

187
## Aave V3
188

189
### Core concepts
190

191
- **Pool** — main entry point for supply, borrow, repay, withdraw, flash loans
192
- **aTokens** — rebasing interest-bearing receipt tokens
193
- **Variable/Stable debt tokens** — represent borrowed positions
194
- **E-Mode** — higher LTV for correlated assets
195

196
### Supply and borrow
197

198
```solidity
199
function supplyAndBorrow(
200
    address supplyAsset, uint256 supplyAmount,
201
    address borrowAsset, uint256 borrowAmount
202
) external {
203
    IERC20(supplyAsset).safeTransferFrom(msg.sender, address(this), supplyAmount);
204
    IERC20(supplyAsset).forceApprove(address(AAVE_POOL), supplyAmount);
205
    AAVE_POOL.supply(supplyAsset, supplyAmount, address(this), 0);
206
    AAVE_POOL.borrow(borrowAsset, borrowAmount, 2, 0, address(this));
207
    IERC20(borrowAsset).safeTransfer(msg.sender, borrowAmount);
208
}
209
```
210

211
### Flash loans
212

213
```solidity
214
contract MyFlashLoan is IFlashLoanSimpleReceiver {
215
    function executeFlashLoan(address asset, uint256 amount) external {
216
        POOL.flashLoanSimple(address(this), asset, amount, abi.encode(msg.sender), 0);
217
    }
218

219
    function executeOperation(
220
        address asset, uint256 amount, uint256 premium,
221
        address initiator, bytes calldata params
222
    ) external returns (bool) {
223
        if (msg.sender != address(POOL)) revert UnauthorizedCallback();
224
        if (initiator != address(this)) revert UnauthorizedInitiator();
225

226
        uint256 repayAmount = amount + premium;
227
        IERC20(asset).forceApprove(address(POOL), repayAmount);
228
        return true;
229
    }
230
}
231
```
232

233
### Aave V3 pitfalls
234

235
- **aToken rebasing** — don't cache `balanceOf` across transactions; use `scaledBalanceOf` for stable accounting
236
- **Borrow rate** — use variable (mode 2); stable (mode 1) is deprecated on most markets
237
- **Supply caps** — check `ReserveData.supplyCap` before large deposits; reverts silently on overflow
238
- **Flash loan premium** — default 0.05% but configurable; read `POOL.FLASHLOAN_PREMIUM_TOTAL()`
239
- **Isolation mode** — newly listed assets may limit borrowable assets to specific stablecoins
240

241
---
242

243
## Uniswap V3
244

245
### Exact input swap
246

247
```solidity
248
function swapExactInput(
249
    address tokenIn, address tokenOut, uint24 fee,
250
    uint256 amountIn, uint256 amountOutMin
251
) external returns (uint256 amountOut) {
252
    IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
253
    IERC20(tokenIn).forceApprove(address(SWAP_ROUTER), amountIn);
254

255
    amountOut = SWAP_ROUTER.exactInputSingle(ISwapRouter.ExactInputSingleParams({
256
        tokenIn: tokenIn,
257
        tokenOut: tokenOut,
258
        fee: fee,
259
        recipient: msg.sender,
260
        deadline: block.timestamp,
261
        amountIn: amountIn,
262
        amountOutMinimum: amountOutMin,
263
        sqrtPriceLimitX96: 0
264
    }));
265
}
266
```
267

268
### Multi-hop swap
269

270
```solidity
271
function swapMultiHop(
272
    bytes memory path, uint256 amountIn, uint256 amountOutMin
273
) external returns (uint256 amountOut) {
274
    IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
275
    IERC20(tokenIn).forceApprove(address(SWAP_ROUTER), amountIn);
276

277
    amountOut = SWAP_ROUTER.exactInput(ISwapRouter.ExactInputParams({
278
        path: path,
279
        recipient: msg.sender,
280
        deadline: block.timestamp,
281
        amountIn: amountIn,
282
        amountOutMinimum: amountOutMin
283
    }));
284
}
285
```
286

287
### Uniswap V3 pitfalls
288

289
- **Fee tiers** — 100 (0.01%), 500 (0.05%), 3000 (0.3%), 10000 (1%). Match tier to pair volatility
290
- **sqrtPriceLimitX96** — 0 for no limit; incorrect values cause partial fills
291
- **Deadline** — `block.timestamp` for immediate; add buffer for user-submitted txs
292
- **Path encoding** — `abi.encodePacked(tokenA, uint24(fee1), tokenB, uint24(fee2), tokenC)`. Wrong encoding silently routes through incorrect pools
293
- **WETH** — V3 only handles ERC-20s; wrap ETH to WETH first
294

295
---
296

297
## Uniswap V4
298

299
### Core concepts
300

301
- **Singleton PoolManager** — all pools in a single contract
302
- **Hooks** — custom logic triggered at pool lifecycle points
303
- **Flash accounting** — transient storage tracks deltas; only net balances settled
304
- **PoolKey** — `(currency0, currency1, fee, tickSpacing, hooks)`
305

306
### Swap via PoolManager
307

308
```solidity
309
function _executeSwap(
310
    PoolKey memory key, bool zeroForOne,
311
    int256 amountSpecified, uint160 sqrtPriceLimitX96
312
) internal returns (BalanceDelta delta) {
313
    delta = POOL_MANAGER.swap(
314
        key,
315
        IPoolManager.SwapParams({
316
            zeroForOne: zeroForOne,
317
            amountSpecified: amountSpecified,
318
            sqrtPriceLimitX96: sqrtPriceLimitX96
319
        }),
320
        ""
321
    );
322
}
323
```
324

325
### Hook implementation
326

327
```solidity
328
contract LimitOrderHook is BaseHook {
329
    constructor(IPoolManager poolManager) BaseHook(poolManager) {}
330

331
    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
332
        return Hooks.Permissions({
333
            beforeInitialize: false, afterInitialize: false,
334
            beforeAddLiquidity: false, afterAddLiquidity: false,
335
            beforeRemoveLiquidity: false, afterRemoveLiquidity: false,
336
            beforeSwap: false, afterSwap: true,
337
            beforeDonate: false, afterDonate: false,
338
            beforeSwapReturnDelta: false, afterSwapReturnDelta: true,
339
            afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false
340
        });
341
    }
342

343
    function afterSwap(
344
        address sender, PoolKey calldata key,
345
        IPoolManager.SwapParams calldata params,
346
        BalanceDelta delta, bytes calldata hookData
347
    ) external override returns (bytes4, int128) {
348
        // ... check and fill limit orders ...
349
        return (BaseHook.afterSwap.selector, 0);
350
    }
351
}
352
```
353

354
### Uniswap V4 pitfalls
355

356
- **Hook address = permissions** — address bits encode enabled callbacks. Use `CREATE2` with salt mining. Mismatch causes deployment failure
357
- **Flash accounting** — settle or take net amounts before `unlock()` callback returns, or the tx reverts
358
- **amountSpecified sign** — negative = exact input, positive = exact output
359
- **Currency type** — `Currency` wraps `address`; native ETH is `Currency.wrap(address(0))`
360
- **Reentrancy via hooks** — hooks execute in PoolManager context; verify hook addresses are trusted
368
User asks: "Write a contract that takes a flash loan from Aave V3, swaps on Uniswap V3, and repays"
370
```solidity
371
// SPDX-License-Identifier: MIT
372
pragma solidity ^0.8.20;
373

374
import {IPool} from "@aave/v3-core/contracts/interfaces/IPool.sol";
375
import {IPoolAddressesProvider} from "@aave/v3-core/contracts/interfaces/IPoolAddressesProvider.sol";
376
import {IFlashLoanSimpleReceiver} from "@aave/v3-core/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
377
import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
378
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
379
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
380

381
error UnauthorizedCallback();
382
error UnauthorizedInitiator();
383
error UnprofitableArbitrage(uint256 repayment, uint256 received);
384

385
/// @title FlashArbitrage — Aave V3 flash loan + Uniswap V3 swap
386
contract FlashArbitrage is IFlashLoanSimpleReceiver {
387
    using SafeERC20 for IERC20;
388

389
    IPool public immutable POOL;
390
    IPoolAddressesProvider public immutable ADDRESSES_PROVIDER;
391
    ISwapRouter public immutable SWAP_ROUTER;
392
    address public immutable owner;
393

394
    constructor(address pool_, address swapRouter_) {
395
        POOL = IPool(pool_);
396
        ADDRESSES_PROVIDER = IPool(pool_).ADDRESSES_PROVIDER();
397
        SWAP_ROUTER = ISwapRouter(swapRouter_);
398
        owner = msg.sender;
399
    }
400

401
    function execute(address asset, uint256 amount, bytes calldata swapPath) external {
402
        POOL.flashLoanSimple(address(this), asset, amount, swapPath, 0);
403
    }
404

405
    /// @inheritdoc IFlashLoanSimpleReceiver
406
    function executeOperation(
407
        address asset, uint256 amount, uint256 premium,
408
        address initiator, bytes calldata params
409
    ) external returns (bool) {
410
        if (msg.sender != address(POOL)) revert UnauthorizedCallback();
411
        if (initiator != address(this)) revert UnauthorizedInitiator();
412

413
        uint256 repayAmount = amount + premium;
414

415
        IERC20(asset).forceApprove(address(SWAP_ROUTER), amount);
416
        uint256 amountOut = SWAP_ROUTER.exactInput(
417
            ISwapRouter.ExactInputParams({
418
                path: params,
419
                recipient: address(this),
420
                deadline: block.timestamp,
421
                amountIn: amount,
422
                amountOutMinimum: repayAmount
423
            })
424
        );
425

426
        if (amountOut < repayAmount) revert UnprofitableArbitrage(repayAmount, amountOut);
427

428
        IERC20(asset).forceApprove(address(POOL), repayAmount);
429

430
        uint256 profit = amountOut - repayAmount;
431
        if (profit > 0) IERC20(asset).safeTransfer(owner, profit);
432

433
        return true;
434
    }
435
}
436
```
441
User asks: "Supply wstETH as collateral on Morpho Blue and borrow USDC"
443
[Contract `MorphoLeverage` with `supplyAndBorrow` and `repayAndWithdraw` functions. Key patterns: safeTransferFrom → forceApprove → supplyCollateral (with onBehalf=msg.sender) → borrow (assets only, shares=0, onBehalf=msg.sender). Reverse: repay (assets, shares=0) → withdrawCollateral. Requires user to call `MORPHO.setAuthorization(contract, true)` first. For full repayment: pass 0 for assets and `type(uint256).max` for shares.]
447
- Import interfaces from official protocol npm packages — manual copies drift from actual ABIs and miss edge cases
448
- Use `SafeERC20.forceApprove()` for all token approvals — handles USDT and other tokens that require approval reset to 0 before setting a new value
449
- Include deadline parameters on all swap and market operations — prevents stale transactions executing at unfavorable prices
450
- Validate callback callers: `if (msg.sender != address(PROTOCOL)) revert UnauthorizedCallback()` — unvalidated callbacks allow anyone to drain contract funds
451
- Check return values from protocol calls against minimum thresholds
452
- Use `forceApprove` with exact amounts rather than `type(uint256).max` when the contract holds user funds — limits exposure if the approved contract is compromised
453
- Pin fork tests to specific block numbers — ensures deterministic protocol state for reproducible integration tests
454
- Accrue interest before reading protocol state — Morpho requires explicit `accrueInterest()`; Aave auto-accrues on interaction but view functions may return stale data
455
- Verify oracle price formats match protocol expectations — Morpho: `36 + borrowDecimals - collateralDecimals`; Aave: 8 decimals via Chainlink. Wrong scaling causes incorrect liquidation thresholds
456
- Validate both `msg.sender` and `initiator` in flash loan callbacks — `msg.sender` check prevents unauthorized callers, `initiator` check prevents other contracts from triggering your callback
457
- Set Aave borrow rate mode explicitly (2 for variable) — stable rate (mode 1) is deprecated on most markets
458
- Morpho: pass either `assets` or `shares`, other must be 0 — passing both causes unpredictable behavior