Skip to main content

token-integration-safety

Safe integration patterns for non-standard ERC-20 tokens. Use when building protocols that accept arbitrary tokens. Covers USDT (no return value), USDC (6 decimals, blacklist), fee-on-transfer, rebasing tokens, ERC-777 callbacks, pausable tokens, and Permit2.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
ccashwell/evm-cortex
آخر نشاط في المصدر
١٠ أبريل ٢٠٢٦ في ١٦:٣١
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٣١
التفرعات
١٨

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
token-integration-safety
description
Safe integration patterns for non-standard ERC-20 tokens. Use when building protocols that accept arbitrary tokens. Covers USDT (no return value), USDC (6 decimals, blacklist), fee-on-transfer, rebasing tokens, ERC-777 callbacks, pausable tokens, and Permit2.
# Token Integration Safety ## Always Use SafeERC20 This is the single most important rule. Never call `transfer`, `transferFrom`, or `approve` directly. ```solidity import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract Vault { using SafeERC20 for IERC20; function deposit(IERC20 token, uint256 amount) external { token.safeTransferFrom(msg.sender, address(this), amount); } function withdraw(IERC20 token, uint256 amount) external { token.safeTransfer(msg.sender, amount); } function approve(IERC20 token, address spender, uint256 amount) external { token.forceApprove(spender, amount); } } ``` ## USDT — No Return Value USDT's `transfer()` and `transferFrom()` don't return a `bool`. Direct calls via the `IERC20` interface will revert because Solidity expects return data. ```solidity // BROKEN with USDT: IERC20(usdt).transfer(to, amount); // reverts — no return data // WORKS with USDT: IERC20(usdt).safeTransfer(to, amount); // SafeERC20 handles missing return ``` USDT also requires approval to be set to 0 before changing to a non-zero value: ```solidity // BROKEN with USDT: IERC20(usdt).approve(spender, newAmount); // reverts if current approval != 0 // WORKS with USDT: IERC20(usdt).forceApprove(spender, newAmount); // sets to 0 first, then newAmount ``` ## USDC — 6 Decimals and Blacklist ```solidity // USDC uses 6 decimals, not 18 // 1 USDC = 1_000_000 (1e6), NOT 1e18 // VULNERABLE: hardcoded 18 decimals uint256 usdcAmount = ethAmount * ethPrice / 1e18; // wrong! // SAFE: use actual decimals uint8 decimals = IERC20Metadata(address(usdc)).decimals(); // returns 6 uint256 usdcAmount = ethAmount * ethPrice / (10 ** (18 - decimals + 18)); ``` USDC has a blacklist. Blacklisted addresses cannot send or receive USDC. Your protocol must handle transfer failures gracefully. ```solidity // If a blacklisted user has funds in your vault, they cannot withdraw // Consider: allow withdrawal to a different address (with proper auth) function withdrawTo(address token, address recipient, uint256 amount) external { if (balances[msg.sender] < amount) revert InsufficientBalance(); balances[msg.sender] -= amount; IERC20(token).safeTransfer(recipient, amount); } ``` ## Fee-on-Transfer Tokens Some tokens deduct a fee on every transfer. The received amount is less than the sent amount. ```solidity // VULNERABLE: assumes received amount equals sent amount function deposit(IERC20 token, uint256 amount) external { token.safeTransferFrom(msg.sender, address(this), amount); balances[msg.sender] += amount; // WRONG: received less than amount } // SAFE: measure actual received amount function deposit(IERC20 token, uint256 amount) external { uint256 balanceBefore = token.balanceOf(address(this)); token.safeTransferFrom(msg.sender, address(this), amount); uint256 received = token.balanceOf(address(this)) - balanceBefore; balances[msg.sender] += received; emit Deposited(msg.sender, address(token), received); } ``` **Decision**: Many protocols explicitly reject fee-on-transfer tokens for simplicity. Document this clearly if so. ## Rebasing Tokens (stETH, aTokens) Rebasing tokens change balances automatically. `balanceOf()` returns different values over time without transfers. ```solidity // PROBLEM: stored balance becomes stale mapping(address => uint256) public balances; function deposit(uint256 amount) external { stETH.safeTransferFrom(msg.sender, address(this), amount); balances[msg.sender] += amount; // balance will change via rebasing } // SOLUTION 1: Use the wrapped, non-rebasing version // stETH → wstETH (wrapped stETH, non-rebasing) // aUSDC → use share-based accounting internally // SOLUTION 2: Share-based accounting (ERC-4626 pattern) function deposit(uint256 assets) external returns (uint256 shares) { shares = convertToShares(assets); _mint(msg.sender, shares); stETH.safeTransferFrom(msg.sender, address(this), assets); } ``` ## ERC-777 Callbacks ERC-777 tokens call `tokensToSend()` on the sender and `tokensReceived()` on the recipient. This creates reentrancy vectors. ```solidity // Risk: any transfer of an ERC-777 token triggers a callback // If your contract receives an ERC-777 token, tokensReceived() is called // An attacker can exploit this callback for reentrancy // Defense: ReentrancyGuard on all functions that transfer tokens function deposit(IERC20 token, uint256 amount) external nonReentrant { token.safeTransferFrom(msg.sender, address(this), amount); _updateBalance(msg.sender, amount); } ``` ## Tokens with Blocklists USDC, USDT, and others can freeze/blocklist addresses. Your protocol should handle this. ```solidity // Consider: what happens if a user gets blocklisted after depositing? // - They can't receive tokens back // - Their position may become unliquidatable // - Protocol accounting may break // Mitigation: allow withdrawal to alternative addresses // Mitigation: allow admin rescue for stuck funds (with timelock) ``` ## Pausable Tokens Some tokens (USDC, USDT) can be paused globally by their admin, blocking all transfers. ```solidity // If the underlying token is paused: // - deposits fail // - withdrawals fail // - liquidations fail (potentially dangerous) // Mitigation: emergency mode that accounts for paused underlying // Mitigation: alternative settlement paths ``` ## Permit2 Integration Permit2 provides a universal approval mechanism for all ERC-20 tokens (even those without EIP-2612). ```solidity import {IPermit2} from "permit2/interfaces/IPermit2.sol"; contract VaultWithPermit2 { IPermit2 public immutable PERMIT2; function depositWithPermit2( address token, uint256 amount, uint256 nonce, uint256 deadline, bytes calldata signature ) external { PERMIT2.permitTransferFrom( IPermit2.PermitTransferFrom({ permitted: IPermit2.TokenPermissions({token: token, amount: amount}), nonce: nonce, deadline: deadline }), IPermit2.SignatureTransferDetails({ to: address(this), requestedAmount: amount }), msg.sender, signature ); _recordDeposit(msg.sender, token, amount); } } ``` ## Token Quirks Reference | Token | Decimals | Return Bool | Blocklist | Pausable | Fee-on-Transfer | Rebasing | |-------|----------|-------------|-----------|----------|-----------------|----------| | USDT | 6 | No | Yes | Yes | No | No | | USDC | 6 | Yes | Yes | Yes | No | No | | DAI | 18 | Yes | No | No | No | No | | WETH | 18 | Yes | No | No | No | No | | WBTC | 8 | Yes | No | No | No | No | | stETH | 18 | Yes | No | No | No | Yes | | PAXG | 18 | Yes | Yes | Yes | Yes (0.02%) | No | ## Token Integration Checklist - [ ] SafeERC20 used for all token operations - [ ] `forceApprove` used instead of `approve` (USDT compatibility) - [ ] No assumption of 18 decimals — query `decimals()` - [ ] Fee-on-transfer handled (measure balance diff) or explicitly rejected - [ ] Rebasing tokens: use wrapped version or share-based accounting - [ ] ERC-777 callbacks: ReentrancyGuard on all token-handling functions - [ ] Blocklist handling: alternative withdrawal paths - [ ] Pausable token failure paths considered - [ ] Permit2 supported for gasless approvals - [ ] Token behavior documented in protocol docs
عرض على GitHub