Full Report
Different blockchains have different settings and quirks that lead to various items. For instance, the average block time, timestamp and many others. This talk is inspired by a this Github repository about the same thing as well. Many of the chains have documentation on how it differs from Ethereum as well. The average block on Ethereum is about 12 seconds since the proof of stake merge. On Polygon, this is 6 seconds. block.number may not reflect proper timing on other chains. On Arbitrum, this represents the Layer 1 block number and is updated once per minute. This means it can jump from 1000 to 1004. In some cases, calculations can be wrong, especially when using this as a time reference. An L2 sequencer is a method of the transactions going from L2 to L1. If the sequencer is down then updates from things like Chainlink will not work. For things like pricing oracles, it's important to ensure these are up-to-date. Otherwise, unfair liquidation or oracle manipulations could occur. An additional chainlink feature is checking the price feed address, as it may differ between other chains or may not exist entirely. On Uniswap, the token order matters. Typically, this is sorted by address. On different chains, the addresses may vary, changing the ordering on the call. This is true of the ordering on Chainlink as well. Modified opcodes. Some chains may have an older version of EVM, such as not having PUSH0 or difficulty being removed in the proof of stake update being changed. tx.origin and msg.sender on the L2 have slightly different meanings. precompiles can be different as well. Modified opcodes. Some chains may have an older version of EVM, such as not having PUSH0 or difficulty being removed in the proof of stake update being changed. tx.origin and msg.sender on the L2 have slightly different meanings. precompiles can be different as well. Frontrunning is weird on L2s. Optimism has a private mempool, making frontrunning infeasible. However, this may be decentralized in the future, resulting in the ability to frontrun. Frontrunning is weird on L2s. Optimism has a private mempool, making frontrunning infeasible. However, this may be decentralized in the future, resulting in the ability to frontrun. Various tokens are different. Decimals, such USDC and USDT are 6 on Ethereum but 18 on BSC. Harcode coded addresses of tokens, such as WETH. Contract interfaces may be different, such as USDT on Ethereum missing the return value but this isn't the case on Polygon. From the reports being read, it's fairly common that projects only test on a single blockchain. Considering all of the issues mentioned above, simply running the code on a different chain or reviewing these issues will net you some findings on occasion.
Analysis Summary
# Best Practices: Multi-Chain Smart Contract Deployment
## Overview
These practices address the security risks associated with deploying EVM-compatible smart contracts across different blockchain networks (L1s and L2s). While many chains appear identical to Ethereum, subtle differences in block timing, opcode support, and oracle behavior can lead to critical vulnerabilities, including unfair liquidations, broken logic, and lost funds.
## Key Recommendations
### Immediate Actions
1. **Validate Block Logic:** Replace `block.number` with `block.timestamp` for time-sensitive calculations if deploying on L2s (like Arbitrum) where block numbers represent L1 updates rather than L2 sequence.
2. **Verify Token Decimals:** Audit all hardcoded decimal values. Ensure `USDC`/`USDT` logic accounts for 6 decimals on Ethereum/Polygon but potential 18-decimal variations on other chains like BSC.
3. **Check Opcode Compatibility:** Confirm the target chain supports recent EVM updates (e.g., `PUSH0`). If deploying to an older EVM-compatible chain, adjust the compiler version or settings to avoid deployment failure.
### Short-term Improvements (1-3 months)
1. **Implement Sequencer Uptime Checks:** For L2 deployments using Chainlink, integrate a "Sequencer Uptime Feed" check to prevent using stale price data if the L2 sequencer goes down.
2. **Abstract Hardcoded Addresses:** Remove hardcoded addresses for tokens like `WETH` or `USDC`. Use a configuration registry or factory pattern to load the correct address based on the `chainId`.
3. **Normalize Token Interfaces:** Implement a wrapper or use `SafeERC20` to handle non-standard tokens (like Ethereum's `USDT` which lacks a return value) that may behave differently on alt-chains.
### Long-term Strategy (3+ months)
1. **Cross-Chain Integration Testing:** Establish a CI/CD pipeline that runs full integration tests on forks of *every* target network, rather than relying solely on a local Anvil/Hardhat Ethereum environment.
2. **Mempool Monitoring & MEV Strategy:** Develop logic that accounts for variations in frontrunning environments—from private mempools (Optimism) to high-MEV environments—ensuring slippage protection is robust across all.
## Implementation Guidance
### For Small Organizations
- **Manual Checklist:** Maintain a "Chain Differences" spreadsheet (Block time, Gas limit, Opcode support) to review before every deployment.
- **Consult Documentation:** Always read the specific "Differences from Ethereum" page in the documentation of the target chain (e.g., Arbitrum/Optimism docs).
### For Medium Organizations
- **Forked Testing:** Mandate that 100% of test suites must pass on network forks (using Foundry or Hardhat) for all intended deployment chains to catch address mismatches and opcode errors.
- **Dynamic Configuration:** Use deployment scripts that pull network-specific parameters from a verified configuration file.
### For Large Enterprises
- **Multi-Chain Governance:** Implement a centralized registry for validated internal and external contract addresses across all supported chains.
- **Security Audits per Chain:** Require specific "Multi-chain Delta" security reviews when porting existing, audited Ethereum code to a new L2/L1.
## Configuration Examples
### Correcting for Arbitrum/L2 Block Number issues:
solidity
// AVOID: Using block.number for duration/vesting
uint256 public endBlock = block.number + 1000;
// RECOMMENDED: Use block.timestamp for cross-chain consistency
uint256 public endTime = block.timestamp + 1 hours;
### Chainlink Sequencer Check (L2 Best Practice):
solidity
// Logic to check if the L2 Sequencer is active before trusting a price feed
( , int256 answer, uint256 startedAt, , ) = sequencerUptimeFeed.latestRoundData();
bool isSequencerUp = answer == 0;
if (!isSequencerUp) revert SequencerDown();
// Add additional check for 'startedAt' to ensure enough time has passed since restart
## Compliance Alignment
- **SCSVS (Smart Contract Security Verification Standard):** Aligns with Business Logic and Infrastructure requirements regarding environment-specific assumptions.
- **SWC Registry (Smart Contract Weakness Classification):** Specifically addresses SWC-116 (Block values as a proxy for time).
## Common Pitfalls to Avoid
- **Assuming Address Parity:** Assuming `token.address` is the same on all chains (e.g., Uniswap pair addresses change if the factory or token addresses differ).
- **Ignoring Return Values:** Deployment on Ethereum might work with a specific token (like USDT), but the same code might revert on a chain where the token follows the ERC20 standard more strictly (returning a boolean).
- **Hardcoding Uniswap Logic:** Assuming token sorting (`token0`, `token1`) is identical without checking if the underlying contract addresses resulted in the same sort order.
## Resources
- **Chainlink Documentation:** [Price Feed Uptime Feeds]
- **Arbitrum/Optimism Docs:** "Differences from Ethereum" sections.
- **GitHub - Cross-chain Quirks:** hxxps://github[.]com/pcaversaccio/cross-chain-architectures