AgentStack
SKILL unreviewed MIT Self-run

Defi Integration

skill-san-npm-skills-ws-defi-integration · by san-npm

Build Solidity/TypeScript DeFi integrations — Uniswap V3/V4, Aave V3, Compound V3, Curve, 1inch/Velora, ERC-4626 vaults: swaps, lending, liquidity, flash loans, yield. Use when writing contract or wallet code that swaps, LPs, borrows, or flash-loans; simulate, set slippage from a live quote, confirm with the user before mainnet.

No reviews yet
0 installs
9 views
0.0% view→install

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

⚠ Flagged

1 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.

Are you the author of Defi Integration? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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:

  1. Pull the tokens in. The position manager spends token0/token1 from this contract, so the contract must first safeTransferFrom both tokens from the user. Approving without transferring just reverts (or, worse, spends a balance the contract happens to hold).
  2. Set real amount0Min/amount1Min. 0/0 lets the pool consume your tokens at any ratio after a sandwich. Derive them from amountDesired * (1 - slippageBps/1e4). mint returns the actually-used amount0/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.

Versions

  • v0.1.0 Imported from the upstream source.