Install
$ agentstack add skill-san-npm-skills-ws-defi-integration Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Reads credentials/environment and may exfiltrate them.
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
DeFi Protocol Integration
> SAFETY — read first. Every contract in this skill is an unaudited teaching template, not production code. Before any mainnet use you MUST: (1) add slippage bounds derived from a live quote/oracle (never 0/1), (2) set real deadlines, (3) add reentrancy protection (ReentrancyGuard / checks-effects-interactions) on any function that calls external contracts and moves funds, (4) use SafeERC20 for all token transfers/approvals, (5) dry-run via a fork or Tenderly simulation, (6) verify every address against the official deployment registry for the target chain, and (7) get an independent security audit. Do not broadcast a money-moving transaction without explicit user confirmation. See §7 (Slippage & MEV) and §11 (Pre-flight Safety Checklist).
1. Uniswap Integration
Uniswap V3 — Exact Input Swap
amountOutMin is the caller's only protection against sandwich attacks and stale routes. The caller MUST derive it from a fresh quote (QuoterV2, see §7) times a slippage factor — never 0, never 1. We use SafeERC20 because non-standard tokens (USDT, BNB) return no bool and revert plain approve/transferFrom. deadline is passed in by the caller (computed off-chain), not block.timestamp, which a validator can satisfy with an arbitrarily delayed inclusion.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SwapHelper is ReentrancyGuard {
using SafeERC20 for IERC20;
ISwapRouter public constant router =
ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); // Mainnet SwapRouter
/// @notice Swap exact amount of tokenIn for tokenOut.
/// @param amountOutMin REQUIRED: quote * (1 - slippageBps/1e4). Reverts the tx
/// if the pool can't deliver it. Passing 0 = guaranteed sandwich.
/// @param deadline Unix ts computed off-chain (e.g. now + 60s). Not block.timestamp.
function swapExactInput(
address tokenIn,
address tokenOut,
uint24 fee, // 100 (0.01%), 500 (0.05%), 3000 (0.3%), 10000 (1%)
uint256 amountIn,
uint256 amountOutMin,
uint256 deadline
) external nonReentrant returns (uint256 amountOut) {
require(amountOutMin > 0, "slippage: amountOutMin must be > 0");
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
// forceApprove resets to 0 first, so it is safe for USDT-style tokens
// that revert on a non-zero -> non-zero approval change.
IERC20(tokenIn).forceApprove(address(router), amountIn);
amountOut = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: fee,
recipient: msg.sender,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: amountOutMin, // slippage protection — see §7
sqrtPriceLimitX96: 0
})
);
// Revoke residual allowance (defense-in-depth; exact-input usually spends all).
IERC20(tokenIn).forceApprove(address(router), 0);
}
}
> Permit2 / Universal Router: New integrations should prefer the Universal Router > (0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD) with Permit2 > (0x000000000022D473030F116dDEE9F6B43aC78BA3). Permit2 lets users sign a single > gasless, time-bounded approval instead of one approve per token/spender, and supports > batched/expiring allowances. forceApprove(router, amount) above is the minimal-allowance > ERC-20 path for the legacy SwapRouter.
Uniswap V3 — Multi-Hop Swap
// Inside SwapHelper (uses SafeERC20 + ReentrancyGuard as above).
function swapMultiHop(
address tokenIn, // first token in the path
bytes calldata path, // abi.encodePacked(tokenA, fee1, tokenB, fee2, tokenC)
uint256 amountIn,
uint256 amountOutMin, // REQUIRED quote-derived bound (see §7); never 0
uint256 deadline // off-chain Unix ts
) external nonReentrant returns (uint256 amountOut) {
require(amountOutMin > 0, "slippage: amountOutMin must be > 0");
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
IERC20(tokenIn).forceApprove(address(router), amountIn);
amountOut = router.exactInput(ISwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: amountOutMin
}));
IERC20(tokenIn).forceApprove(address(router), 0);
}
Uniswap V3 — Add Liquidity
Two things the original snippet got dangerously wrong and that you must always do:
- Pull the tokens in. The position manager spends
token0/token1from this contract, so the contract must firstsafeTransferFromboth tokens from the user. Approving without transferring just reverts (or, worse, spends a balance the contract happens to hold). - Set real
amount0Min/amount1Min.0/0lets the pool consume your tokens at any ratio after a sandwich. Derive them fromamountDesired * (1 - slippageBps/1e4).mintreturns the actually-usedamount0/amount1; refund the remainder and revoke approvals.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract LiquidityHelper is ReentrancyGuard {
using SafeERC20 for IERC20;
INonfungiblePositionManager public constant positionManager =
INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
/// @param amount0Min,amount1Min REQUIRED slippage floors (e.g. desired * (1 - bps/1e4)).
/// Never pass 0/0. token0 0 && amount1Min > 0, "slippage: mins must be > 0");
// 1. Pull both tokens from the user into this contract.
IERC20(token0).safeTransferFrom(msg.sender, address(this), amount0Desired);
IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1Desired);
// 2. Minimal approvals (forceApprove handles USDT-style tokens).
IERC20(token0).forceApprove(address(positionManager), amount0Desired);
IERC20(token1).forceApprove(address(positionManager), amount1Desired);
uint256 used0;
uint256 used1;
(tokenId, liquidity, used0, used1) = positionManager.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: fee,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: amount0Min, // slippage protection — never 0
amount1Min: amount1Min,
recipient: msg.sender, // NFT goes to the user
deadline: deadline
})
);
// 3. Revoke approvals and refund unused tokens to the user.
IERC20(token0).forceApprove(address(positionManager), 0);
IERC20(token1).forceApprove(address(positionManager), 0);
if (used0 V4 `PoolManager`, `PositionManager`, `Universal Router`, and `V4Quoter` addresses are
> chain-specific and listed in the Uniswap v4 deployments docs — fetch them at integration
> time rather than hardcoding.
---
## 2. Aave V3
### Supply (Deposit)
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IPool} from "@aave/core-v3/contracts/interfaces/IPool.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract AaveHelper {
using SafeERC20 for IERC20;
IPool constant POOL = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); // Mainnet
function supply(address asset, uint256 amount) external {
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
IERC20(asset).forceApprove(address(POOL), amount);
POOL.supply(asset, amount, msg.sender, 0); // onBehalfOf = user -> user gets aTokens
}
// Borrowing on behalf of the user requires the user to have delegated credit to this
// contract (POOL.borrowAllowance via the debt token's approveDelegation). Otherwise call
// POOL.borrow directly from the user's own account. interestRateMode: 2 = variable
// (stable-rate borrowing has been disabled on Aave V3 mainnet markets).
function borrow(address asset, uint256 amount, address user) external {
POOL.borrow(asset, amount, 2, 0, user);
}
}
Flash Loan
Inherit Aave's FlashLoanSimpleReceiverBase — it wires up POOL and ADDRESSES_PROVIDER for you, so you don't re-implement those getters. The two non-negotiable security checks in executeOperation: (1) msg.sender == address(POOL) (only the Pool may call back) and (2) initiator == address(this) (the loan was started by this contract, not griefed by a third party). The premium is read from the callback (premium) — do not hardcode it; Aave's FLASHLOAN_PREMIUM_TOTAL is governance-configurable per deployment and can change.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {FlashLoanSimpleReceiverBase}
from "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import {IPoolAddressesProvider}
from "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract AaveFlashLoanReceiver is FlashLoanSimpleReceiverBase {
using SafeERC20 for IERC20;
// Mainnet PoolAddressesProvider. Resolve per-chain from Aave's address book.
constructor()
FlashLoanSimpleReceiverBase(
IPoolAddressesProvider(0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e)
)
{}
function executeFlashLoan(address asset, uint256 amount) external {
POOL.flashLoanSimple(address(this), asset, amount, "", 0); // params, referralCode
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata /* params */
) external override returns (bool) {
require(msg.sender == address(POOL), "caller != Aave Pool");
require(initiator == address(this), "untrusted initiator");
// --- YOUR ARBITRAGE / LIQUIDATION LOGIC HERE ---
// You hold `amount` of `asset`. It MUST profitably return at least
// amount + premium, or this whole transaction reverts (atomic).
// Approve the Pool to pull repayment. premium comes from the callback,
// NOT a hardcoded bps — the flash-loan fee is governance-configurable.
uint256 totalDebt = amount + premium;
IERC20(asset).forceApprove(address(POOL), totalDebt);
return true;
}
}
Aave V3 Key Addresses (Mainnet)
Pool: 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2
PoolAddressesProvider: 0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e
Oracle: 0x54586bE62E3c3580375aE3723C145253060Ca0C2
> Flash-loan fee is NOT a constant. FLASHLOAN_PREMIUM_TOTAL was historically 0.05% > (5 bps) on mainnet but is a governance parameter and varies by deployment. Read it at > runtime: POOL.FLASHLOAN_PREMIUM_TOTAL() (returns bps), and in executeOperation always > repay amount + premium from the callback. Aave V3 addresses are chain-specific — resolve > them per network from Aave's official address book, never reuse mainnet values on an L2.
3. Compound V3 (Comet)
Comet's supply/withdraw act on msg.sender — i.e. the contract, not the end user. A user-facing wrapper must either (a) have the user call Comet directly, or (b) use supplyFrom/withdrawFrom with the right account args. The snippet below pulls collateral from the user and credits the user's own Comet account with supplyFrom(address(this), user, …) — from is the contract (supplying its own freshly-pulled tokens) so this needs no operator grant. To borrow, it pulls base out of the user's account with withdrawFrom(user, user, …); because src is the user, the user MUST first grant this contract operator rights via COMET.allow(wrapper, true). Note Comet is single-base-asset (here USDC): you supply approved collateral assets and can only borrow the base asset.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IComet {
function supplyFrom(address from, address dst, address asset, uint256 amount) external;
function withdrawFrom(address src, address to, address asset, uint256 amount) external;
function baseToken() external view returns (address);
function isLiquidatable(address account) external view returns (bool);
function allow(address manager, bool isAllowed) external;
}
contract CometWrapper {
using SafeERC20 for IERC20;
IComet constant COMET_USDC = IComet(0xc3d688B66703497DAA19211EEdff47f25384cdc3); // cUSDCv3
// Supply path needs NO COMET_USDC.allow() grant: `from` below is this contract
// (supplying tokens it just pulled in), so it acts only on its own Comet account.
function supplyCollateral(address asset, uint256 amount) external {
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
IERC20(asset).forceApprove(address(COMET_USDC), amount);
// Credit the USER's account (dst = msg.sender), not the contract's.
COMET_USDC.supplyFrom(address(this), msg.sender, asset, amount);
}
// Borrow base (USDC) out of the user's OWN Comet account and send it to the user.
// src = the user, so the user MUST first call COMET_USDC.allow(address(this), true)
// to make this contract an operator for their account; otherwise this reverts.
function borrowBaseTo(uint256 amount) external {
COMET_USDC.withdrawFrom(msg.sender, msg.sender, COMET_USDC.baseToken(), amount);
}
function isLiquidatable(address account) external view returns (bool) {
return COMET_USDC.isLiquidatable(account);
}
}
4. Curve Finance
Swap on Curve Stable Pool
min_dy is the slippage floor — derive it from get_dy(i, j, dx) * (1 - slippageBps/1e4) and never pass 0. Pull the input token from the user first, then return the output. (The classic 3pool below is a plain-ERC20 pool; some Curve pools use native ETH or have a separate exchange_underlying for wrapped assets — check the pool.)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface ICurvePool {
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external returns (uint256);
function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256);
function coins(uint256 i) external view returns (address);
}
contract CurveSwapHelper {
using SafeERC20 for IERC20;
ICurvePool constant THREE_
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [san-npm](https://github.com/san-npm)
- **Source:** [san-npm/skills-ws](https://github.com/san-npm/skills-ws)
- **License:** MIT
- **Homepage:** https://skills-ws.vercel.app
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.