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