- Introduced Aggregator.sol for Chainlink-compatible oracle functionality, including round-based updates and access control. - Added OracleWithCCIP.sol to extend Aggregator with CCIP cross-chain messaging capabilities. - Created .gitmodules to include OpenZeppelin contracts as a submodule. - Developed a comprehensive deployment guide in NEXT_STEPS_COMPLETE_GUIDE.md for Phase 2 and smart contract deployment. - Implemented Vite configuration for the orchestration portal, supporting both Vue and React frameworks. - Added server-side logic for the Multi-Cloud Orchestration Portal, including API endpoints for environment management and monitoring. - Created scripts for resource import and usage validation across non-US regions. - Added tests for CCIP error handling and integration to ensure robust functionality. - Included various new files and directories for the orchestration portal and deployment scripts.
9.1 KiB
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:
// 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:
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:
// 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:
// 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
// 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
// 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
// Good
require(amount > 0, "MyContract: invalid amount");
require(recipient != address(0), "MyContract: zero recipient");
// Bad
if (amount == 0) return;
2. Use Explicit Error Messages
// Good
require(balance >= amount, "MyContract: insufficient balance");
// Bad
require(balance >= amount);
3. Check Return Values
// Good
require(IERC20(token).transfer(to, amount), "Transfer failed");
// Bad
IERC20(token).transfer(to, amount);
4. Validate Addresses
// 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
function testTransfer() public {
// Deploy standard ERC20 token
ERC20 token = new ERC20();
// Test transfer
require(token.transfer(recipient, amount), "Transfer failed");
}
Test Error Cases
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 patterncontracts/ccip/CCIPWETH10Bridge.sol- Minimal IERC20 interface, custom admin patterncontracts/tokens/WETH10.sol- No external dependencies
Documentation
Summary
Key Patterns
- ✅ Minimal IERC20 interface
- ✅ Custom admin pattern
- ✅ Standard ERC20 calls
- ✅ 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
- ✅ Follow patterns from new WETH contracts
- ✅ Use minimal interfaces
- ✅ Use custom admin pattern
- ✅ Test thoroughly
- ✅ Document dependencies
Questions?
For questions about migration patterns, refer to: