# Migration Guide: Avoiding OpenZeppelin Dependencies ## Overview This guide provides patterns and best practices for creating new contracts without OpenZeppelin dependencies, following the patterns used in the new WETH contracts. ## Patterns to Follow ### 1. Minimal IERC20 Interface Instead of using OpenZeppelin's IERC20, use a minimal interface: ```solidity // Minimal IERC20 interface for token operations interface IERC20 { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } // Usage require(IERC20(token).transferFrom(msg.sender, address(this), amount), "Transfer failed"); require(IERC20(token).approve(spender, amount), "Approval failed"); ``` **Reference**: `contracts/ccip/CCIPWETH9Bridge.sol` --- ### 2. Custom Admin Pattern Instead of using OpenZeppelin's Ownable, use a custom admin pattern: ```solidity contract MyContract { address public admin; modifier onlyAdmin() { require(msg.sender == admin, "MyContract: only admin"); _; } constructor(address _admin) { require(_admin != address(0), "MyContract: zero address"); admin = _admin; } function changeAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "MyContract: zero address"); admin = newAdmin; } } ``` **Reference**: `contracts/ccip/CCIPWETH9Bridge.sol` --- ### 3. Standard ERC20 Calls Instead of using SafeERC20, use standard ERC20 calls with require statements: ```solidity // Instead of SafeERC20 // IERC20(token).safeTransferFrom(msg.sender, address(this), amount); // Use standard ERC20 calls require(IERC20(token).transferFrom(msg.sender, address(this), amount), "Transfer failed"); require(IERC20(token).approve(spender, amount), "Approval failed"); ``` **Note**: This works for standard ERC20 tokens. If you need to handle non-standard tokens, consider using SafeERC20 or a try-catch pattern. **Reference**: `contracts/ccip/CCIPWETH9Bridge.sol` --- ### 4. Error Handling Always use require statements for error handling: ```solidity // Good: Explicit error messages require(amount > 0, "MyContract: invalid amount"); require(recipient != address(0), "MyContract: zero recipient"); require(balance >= amount, "MyContract: insufficient balance"); // Bad: Silent failures if (amount == 0) return; ``` --- ## Migration Checklist ### For New Contracts - [ ] Use minimal IERC20 interface instead of OpenZeppelin's IERC20 - [ ] Use custom admin pattern instead of Ownable - [ ] Use standard ERC20 calls instead of SafeERC20 - [ ] Add explicit error messages with require statements - [ ] Test with standard ERC20 tokens - [ ] Document any non-standard token requirements ### For Existing Contracts - [ ] Identify OpenZeppelin dependencies - [ ] Replace SafeERC20 with standard ERC20 calls - [ ] Replace Ownable with custom admin pattern - [ ] Replace IERC20 with minimal interface - [ ] Update tests - [ ] Verify security - [ ] Update documentation --- ## Examples ### Example 1: Token Transfer Contract ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; // Minimal IERC20 interface interface IERC20 { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } contract TokenTransfer { address public admin; address public token; modifier onlyAdmin() { require(msg.sender == admin, "TokenTransfer: only admin"); _; } constructor(address _admin, address _token) { require(_admin != address(0), "TokenTransfer: zero admin"); require(_token != address(0), "TokenTransfer: zero token"); admin = _admin; token = _token; } function transferTokens(address to, uint256 amount) external onlyAdmin { require(to != address(0), "TokenTransfer: zero recipient"); require(amount > 0, "TokenTransfer: invalid amount"); require(IERC20(token).transfer(to, amount), "TokenTransfer: transfer failed"); } function changeAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "TokenTransfer: zero address"); admin = newAdmin; } } ``` --- ### Example 2: Cross-Chain Bridge ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; interface IERC20 { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } contract CrossChainBridge { address public admin; address public token; address public router; modifier onlyAdmin() { require(msg.sender == admin, "CrossChainBridge: only admin"); _; } constructor(address _admin, address _token, address _router) { require(_admin != address(0), "CrossChainBridge: zero admin"); require(_token != address(0), "CrossChainBridge: zero token"); require(_router != address(0), "CrossChainBridge: zero router"); admin = _admin; token = _token; router = _router; } function bridgeTokens(address recipient, uint256 amount) external { require(recipient != address(0), "CrossChainBridge: zero recipient"); require(amount > 0, "CrossChainBridge: invalid amount"); require(IERC20(token).transferFrom(msg.sender, address(this), amount), "CrossChainBridge: transfer failed"); // Bridge logic here } function changeAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "CrossChainBridge: zero address"); admin = newAdmin; } } ``` --- ## Best Practices ### 1. Always Validate Inputs ```solidity // Good require(amount > 0, "MyContract: invalid amount"); require(recipient != address(0), "MyContract: zero recipient"); // Bad if (amount == 0) return; ``` ### 2. Use Explicit Error Messages ```solidity // Good require(balance >= amount, "MyContract: insufficient balance"); // Bad require(balance >= amount); ``` ### 3. Check Return Values ```solidity // Good require(IERC20(token).transfer(to, amount), "Transfer failed"); // Bad IERC20(token).transfer(to, amount); ``` ### 4. Validate Addresses ```solidity // Good require(admin != address(0), "MyContract: zero address"); // Bad admin = _admin; ``` --- ## Security Considerations ### 1. Non-Standard ERC20 Tokens Standard ERC20 calls may fail with non-standard tokens. If you need to handle non-standard tokens: - Use SafeERC20 (requires OpenZeppelin) - Use try-catch pattern - Document token requirements ### 2. Access Control Custom admin pattern provides same security as Ownable: - Simple address-based access control - Same security level - No external dependencies ### 3. Error Handling Always use require statements: - Explicit error messages - Revert on failure - Gas-efficient --- ## Testing ### Test with Standard ERC20 Tokens ```solidity function testTransfer() public { // Deploy standard ERC20 token ERC20 token = new ERC20(); // Test transfer require(token.transfer(recipient, amount), "Transfer failed"); } ``` ### Test Error Cases ```solidity function testInvalidAmount() public { vm.expectRevert("MyContract: invalid amount"); contract.transferTokens(recipient, 0); } function testZeroRecipient() public { vm.expectRevert("MyContract: zero recipient"); contract.transferTokens(address(0), amount); } ``` --- ## References ### Contract Examples - `contracts/ccip/CCIPWETH9Bridge.sol` - Minimal IERC20 interface, custom admin pattern - `contracts/ccip/CCIPWETH10Bridge.sol` - Minimal IERC20 interface, custom admin pattern - `contracts/tokens/WETH10.sol` - No external dependencies ### Documentation - [Contract Inventory](./CONTRACT_INVENTORY.md) - [OpenZeppelin Usage Analysis](./OPENZEPPELIN_USAGE_ANALYSIS.md) - [Dependencies Guide](./DEPENDENCIES.md) --- ## Summary ### Key Patterns 1. ✅ Minimal IERC20 interface 2. ✅ Custom admin pattern 3. ✅ Standard ERC20 calls 4. ✅ Explicit error handling ### Benefits - ✅ No external dependencies - ✅ Smaller code size - ✅ Lower gas costs - ✅ Better maintainability ### When to Use OpenZeppelin - ⚠️ Complex security features needed - ⚠️ Battle-tested implementation required - ⚠️ Non-standard token handling needed --- ## Next Steps 1. ✅ Follow patterns from new WETH contracts 2. ✅ Use minimal interfaces 3. ✅ Use custom admin pattern 4. ✅ Test thoroughly 5. ✅ Document dependencies --- ## Questions? For questions about migration patterns, refer to: - [Contract Inventory](./CONTRACT_INVENTORY.md) - [OpenZeppelin Usage Analysis](./OPENZEPPELIN_USAGE_ANALYSIS.md) - [Dependencies Guide](./DEPENDENCIES.md)