Somnia 백서

Somnia

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 36쪽

ort "./SomniaFactory.sol"; import "./SomniaRouter.sol"; import "./IERC20.sol"; contract TestDEX { SomniaFactory public factory; SomniaRouter public router; constructor() { factory = new SomniaFactory(); router = new SomniaRouter(address(factory)); } function testCreatePair(address tokenA, address tokenB) external returns (address) { return factory.createPair(tokenA, tokenB); } function testAddLiquidity( address tokenA, address tokenB, uint256 amountA, uint256 amountB ) external { IERC20(tokenA).transferFrom(msg.sender, address(this), amountA); IERC20(tokenB).transferFrom(msg.sender, address(this), amountB); IERC20(tokenA).approve(address(router), amountA); IERC20(tokenB).approve(address(router), amountB); router.addLiquidity( tokenA, tokenB, amountA, amountB, 0, 0, msg.sender, block.timestamp + 3600 ); } } ``` </details> ## Conclusion This tutorial has walked you through implementing a fully functional decentralized exchange (DEX)on Somnia, demonstrating the core Smart Contract architecture that powers Automated Market Makers - AMM. By building these contracts from scratch, you've gained hands-on experience with the fundamental mechanics of DEXs:\ 1\. How Liquidity Pools maintain token reserves.\ 2\. How the Constant Product Formula enables permissionless trading.\ 3\. How router contracts abstract complex operations into user-friendly interfaces.\ \ The implementation covers essential features including liquidity provision with LP token minting, atomic swaps with built-in slippage protection, and multi-hop routing for indirect trading pairs. While this represents a complete Smart Contract foundation, production deployments would benefit from additional features such as concentrated liquidity (as seen in Uniswap V3), dynamic fee tiers, flash loan functionality, and comprehensive governance mechanisms. The modular architecture we've implemented makes these enhancements straightforward to integrate.\ \ As you continue developing on Somnia, remember that the Smart Contracts presented here are just one layer of a complete DEX ecosystem; You'll need to consider frontend interfaces, liquidity incentives, and integration with other DeFi protocols to create a thriving exchange. Most importantly, ensure thorough testing on Somnia's testnet and seek professional security audits before deploying any contracts handling real value. This foundation provides you with the knowledge and code necessary to contribute to Somnia's DeFi ecosystem, whether by deploying your own DEX or building innovative features on top of existing protocols. # Security # Smart Contract Security 101 Smart contract vulnerabilities can lead to devastating financial losses and compromise user trust. This guide examines three critical vulnerability categories with hands-on examples that demonstrate both vulnerable patterns and secure implementations. Learn to identify and prevent the most critical security vulnerabilities in smart contracts through practical examples. This section covers real attack vectors with vulnerable and secure code implementations, along with comprehensive prevention strategies. What you'll achieve: Recognize vulnerable code patterns, understand attack mechanisms, and learn to implement secure alternatives. ## Prerequisites ✅ Required: * Understanding of Solidity function execution * Basic knowledge of EVM call mechanics ✅ Recommended: * Access to Remix IDE for testing examples * Somnia Testnet setup for deployment testing ## Vulnerability Overview The following table categorizes vulnerabilities by severity and implementation difficulty: | Vulnerability Type | Severity | Frequency | Detection Difficulty | Financial Impact | | ------------------ | -------- | --------- | -------------------- | ---------------- | | Reentrancy | Critical | High | Medium | Very High | | Access Control | Critical | Medium | Low | High | | Integer Overflow | High | Low | Low | Medium | *** ## 1. Reentrancy Vulnerabilities #### Understanding the Attack Reentrancy occurs when a contract calls an external contract before updating its internal state, allowing the external contract to call back and exploit the inconsistent state. #### Vulnerable Implementation Analysis <details> <summary>ReentrancyVulnerable Contract</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; /** * @title ReentrancyVulnerable * @notice Classic vulnerable withdraw pattern for tutorial exploitation in Remix. * @dev Demonstrates why updating state after external calls is dangerous. * * WARNING: Use only in local/Remix test environments. Do NOT deploy vulnerable contracts with real funds on mainnet. */ contract ReentrancyVulnerable { mapping(address => uint256) public deposits; event Deposited(address indexed who, uint256 amount); event Withdrawn(address indexed who, uint256 amount); function deposit() external payable { require(msg.value > 0, "zero deposit"); deposits[msg.sender] += msg.value; emit Deposited(msg.sender, msg.value); } function withdraw(uint256 amount) external { require(deposits[msg.sender] >= amount, "insufficient balance"); // INTERACTION before EFFECTS -> vulnerable to reentrancy (bool sent, ) = msg.sender.call{value: amount}(""); require(sent, "transfer failed"); // EFFECTS: update balance after external call -> attacker can re-enter here deposits[msg.sender] -= amount; emit Withdrawn(msg.sender, amount); } function contractBalance() external view returns (uint256) { return address(this).balance; } } ``` </details> Why This Contract Is Vulnerable: * Interaction (external call) occurs before updating internal state. * No reentrancy guard or checks-effects-interactions. * Attackers can re-enter withdraw() during the external call and drain funds. #### Attack Mechanism {% stepper %} {% step %} **Attack contract & setup** Attacker contract: ```solidity contract Attacker { ReentrancyVulnerable target; uint256 public constant ATTACK_AMOUNT = 1 ether; constructor(address _target) { target = ReentrancyVulnerable(_target); } function attack() external payable { require(msg.value >= ATTACK_AMOUNT, "need at least 1 ETH"); target.deposit{value: ATTACK_AMOUNT}(); target.withdraw(ATTACK_AMOUNT); } receive() external payable { if (address(target).balance >= ATTACK_AMOUNT) { target.withdraw(ATTACK_AMOUNT); } } } ``` Initial state example: * Vulnerable contract has 10 ETH from other users * Attacker deposits 1 ETH {% endstep %} {% step %} **First withdraw and reentry** * Attacker calls `target.withdraw(1 ether)`. * Contract sends 1 ETH via `call` to attacker -> triggers attacker's `receive()`. * Because deposits were not updated yet, `deposits[attacker]` still equals 1 ETH. * In `receive()`, attacker calls `target.withdraw(1 ether)` again. {% endstep %} {% step %} **Recursive drain and final impact** * Reentrancy repeats until contract balance < attack amount. * Attacker drains nearly all ETH while only depositing 1 ETH. * Final state: contract drained, attacker profit huge, other users lose funds. {% endstep %} {% endstepper %} #### Secure Implementation <details> <summary>Secure Reentrancy Contract</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract ReentrancySecure { mapping(address => uint256) private _deposits; uint256 private constant _NOT_ENTERED = 1; uint256 private constant _ENTERED = 2; uint256 private _status = _NOT_ENTERED; event Deposited(address indexed who, uint256 amount); event Withdrawn(address indexed who, uint256 amount); modifier nonReentrant() { require(_status == _NOT_ENTERED, "reentrant"); _status = _ENTERED; _; _status = _NOT_ENTERED; } function deposit() external payable { require(msg.value > 0, "zero deposit"); _deposits[msg.sender] += msg.value; emit Deposited(msg.sender, msg.value); } function withdraw(uint256 amount) external nonReentrant { uint256 bal = _deposits[msg.sender]; require(bal >= amount, "insufficient balance"); // EFFECTS _deposits[msg.sender] = bal - amount; // INTERACTIONS (bool sent, ) = msg.sender.call{value: amount}(""); require(sent, "transfer failed"); emit Withdrawn(msg.sender, amount); } function depositOf(address who) external view returns (uint256) { return _deposits[who]; } function contractBalance() external view returns (uint256) { return address(this).balance; } } ``` </details> This secure contract uses a reentrancy guard to prevent recursive calls: **Key Components:** 1. **Reentrancy Guard Variables:** * `_NOT_ENTERED = 1` and `_ENTERED = 2`: Lock states * `_status`: Tracks if function is currently executing 2. **nonReentrant Modifier:** * Checks if function is already running (`_status == _NOT_ENTERED`) * Sets lock before execution (`_status = _ENTERED`) * Releases lock after completion (`_status = _NOT_ENTERED`) 3. **Secure withdraw() Function:** * Uses `nonReentrant` modifier to block recursive calls * Updates balance BEFORE making external call (checks-effects-interactions pattern) * External call cannot trigger another withdrawal due to the guard **How It Prevents Attacks:** * First call sets `_status = _ENTERED` * Any reentrant call fails the `require(_status == _NOT_ENTERED)` check * Transaction reverts with "reentrant" error * Only one withdrawal per transaction is possible **Security Features:** * Reentrancy protection through state locking * State updates before external calls * Automatic revert on attack attempts Security fixes: * Checks-Effects-Interactions: state updated before external calls. * Reentrancy guard (mutex) via nonReentrant modifier. * Double protection: both CEI and guard prevent recursive drains. #### Prevention Strategies (summary) | Strategy | Implementation | Effectiveness | Gas Cost | | ---------------------------- | ---------------------------------- | ------------- | -------- | | CEI Pattern | Update state before external calls | High | Low | | Reentrancy Guard | Mutex-style protection | Very High | Medium | | Pull Payment | Users withdraw instead of push | High | Low | | OpenZeppelin ReentrancyGuard | Battle-tested implementation | Very High | Medium | *** ## 2. Access Control Vulnerabilities #### Understanding the Flaw Poor access control allows unauthorized users to execute privileged functions, leading to complete contract compromise. #### Vulnerable Implementation Analysis <details> <summary>Access Control Vulnerable Contract</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract AccessControlVulnerable { mapping(bytes32 => mapping(address => bool)) public roles; bytes32 public constant ADMIN = keccak256("ADMIN"); bytes32 public constant WRITER = keccak256("WRITER"); string public data; constructor() { roles[ADMIN][msg.sender] = true; roles[WRITER][msg.sender] = true; } function grantRole(bytes32 role, address account) external { require(account != address(0), "zero account"); require(roles[role][msg.sender], "only role-holder"); roles[role][account] = true; } function revokeRole(bytes32 role, address account) external { require(roles[role][msg.sender], "only role-holder"); roles[role][account] = false; } function write(string calldata newData) external { require(roles[WRITER][msg.sender], "not writer"); data = newData; } function emergencyReset() external { require(roles[ADMIN][msg.sender], "not admin"); data = ""; } function hasRole(bytes32 role, address account) external view returns (bool) { return roles[role][account]; } }solid ``` </details> Critical issues: * Any role-holder can grant the same role to others → role escalation. * No owner/admin separation. * No events emitted for role changes (no audit trail). #### Attack Mechanism {% stepper %} {% step %} **Setup & initial compromise** Initial state: * Deployer has ADMIN and WRITER. * Legitimate user Alice has WRITER. * Attacker Bob has no roles. Attacker compromises Alice or Alice becomes malicious. {% endstep %} {% step %} **Role escalation sequence** * Alice calls `grantRole(WRITER, bob)` → Bob becomes WRITER. * Bob (now a role-holder) calls `grantRole(ADMIN, bob)` if the model allows it. * Because the contract lets role-holders grant roles, escalation to ADMIN can occur. {% endstep %} {% step %} **Final state & impact** * Bob gains ADMIN powers: can grant/revoke roles and perform privileged actions. * No events: attack may be undetected. * Complete contract compromise possible. {% endstep %} {% endstepper %} #### Secure Implementation <details> <summary>Secure Access Control Contract</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract AccessControlSecure { mapping(bytes32 => mapping(address => bool)) private _roles; bytes32 public constant ADMIN = keccak256("ADMIN"); bytes32 public constant WRITER = keccak256("WRITER"); address public immutable owner; string public data; event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); event DataWritten(address indexed sender, string newData); constructor() { owner = msg.sender; _roles[ADMIN][msg.sender] = true; emit RoleGranted(ADMIN, msg.sender, msg.sender); } modifier onlyOwner() { require(msg.sender == owner, "only owner"); _; } modifier onlyAdmin() { require(_roles[ADMIN][msg.sender], "only admin"); _; } function grantRole(bytes32 role, address account) external { require(account != address(0), "zero address"); if (role == ADMIN) { require(msg.sender == owner, "only owner can grant admin"); } else { require(_roles[ADMIN][msg.sender], "only admin can grant"); } if (!_roles[role][account]) { _roles[role][account] = true; emit RoleGranted(role, account, msg.sender); } } function revokeRole(bytes32 role, address account) external { require(account != address(0), "zero address"); if (role == ADMIN) { require(msg.sender == owner, "only owner can revoke admin"); } else { require(_roles[ADMIN][msg.sender], "only admin can revoke"); } if (_roles[role][account]) { _roles[role][account] = false; emit RoleRevoked(role, account, msg.sender); } } function write(string calldata newData) external { require(_roles[WRITER][msg.sender], "not writer"); data = newData; emit DataWritten(msg.sender, newData); } function hasRole(bytes32 role, address account) external view returns (bool) { return _roles[role][account]; } } ``` </details> This secure contract implements proper role hierarchy: **Key Security Features:** 1. **Immutable Owner:** * Owner address cannot be changed after deployment * Only owner can grant/revoke ADMIN roles 2. **Role Hierarchy:** * Owner > Admin > Writer * Only ADMIN can grant WRITER roles * WRITER cannot grant any roles 3. **Secure Role Management:** * Different permissions for granting ADMIN vs other roles * Input validation prevents zero address assignments * Events log all role changes for audit trail **How it prevents attacks:** * WRITER cannot escalate to ADMIN (only owner can grant ADMIN) * Clear separation of permissions * Complete audit trail of all role changes **How Security Works:** **Attack Prevention:** * Alice (WRITER) tries to grant ADMIN to Bob → Fails (only owner can grant ADMIN) * Bob tries to grant himself ADMIN → Fails (only owner can grant ADMIN) * Role escalation is impossible **Legitimate Operations:** * Owner can grant ADMIN roles * ADMIN can grant WRITER roles * All changes are logged with events **Result:** * Clear hierarchy prevents unauthorized escalation * Complete audit trail of all role changes * Attack is blocked by proper permission checks #### Best Practices (summary) | Practice | Purpose | Implementation | | ------------------ | ---------------------------- | ------------------------ | | Role Hierarchy | Prevent privilege escalation | Owner > Admin > User | | Immutable Owner | Prevent ownership takeover | Set in constructor | | Event Logging | Enable audit trails | Emit on all role changes | | Zero Address Check | Prevent accidental locks | Validate addresses | *** ## 3. Integer Overflow/Underflow #### Understanding the Issue Integer overflow/underflow occurs when arithmetic operations exceed the maximum or minimum values for the data type, potentially causing unexpected behavior or security vulnerabilities in financial calculations. #### Vulnerable Implementation Analysis <details> <summary>Integer Overflow Vulnerable Contract</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract IntegerOverflowVulnerable { mapping(address => uint256) public balance; uint256 public totalSupply; function mint(address to, uint256 amount) external { require(to != address(0), "zero address"); balance[to] += amount; totalSupply += amount; } function transfer(address from, address to, uint256 amount) external { require(to != address(0), "zero address"); // Missing check: require(balance[from] >= amount) balance[from] -= amount; // may revert in 0.8+; would underflow silently pre-0.8 balance[to] += amount; } function batchAdd(address[] calldata recipients, uint256[] calldata amounts) external { require(recipients.length == amounts.length, "length mismatch"); for (uint256 i = 0; i < recipients.length; i++) { require(recipients[i] != address(0), "zero addr in batch"); balance[recipients[i]] += amounts[i]; totalSupply += amounts[i]; } } function balanceOf(address who) external view returns (uint256) { return balance[who]; } } ``` </details> Critical vulnerabilities: * Unchecked addition (overflow) on balances and totalSupply. * Unchecked subtraction (underflow) in transfer. * Batch operations amplify overflow risks. * No access control on minting. #### Attack Mechanisms {% stepper %} {% step %} **Overflow via mint()** Initial state: * totalSupply = expected supply * Attacker computes amount = max\_uint256 - totalSupply + 1 * Calling mint(attacker, amount) causes totalSupply and balance to wrap to 0 (pre-0.8 behavior). * State becomes corrupted. {% endstep %} {% step %} **Underflow via transfer() (pre-0.8 behavior)** * Attacker with 0 balance attempts to transfer a huge amount. * balance\[attacker] -= amount underflows to near max uint256. * Attacker ends up with immense balance. {% endstep %} {% step %} **Batch amplification** * Attacker uses many entries with large but individually safe amounts. * Cumulative additions overflow totalSupply or balances. * Economic model breaks. {% endstep %} {% endstepper %} #### Secure Implementation <details> <summary>Secure Integer Overflow Contract</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract IntegerOverflowSecure { mapping(address => uint256) public balances; address public owner; uint256 public totalSupply; uint256 public constant MAX_SUPPLY = type(uint256).max; // example constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function mint(address to, uint256 amount) external onlyOwner { require(to != address(0), "Zero address"); require(amount > 0, "Zero amount"); require(balances[to] + amount >= balances[to], "Balance overflow"); require(totalSupply + amount >= totalSupply, "Total supply overflow"); require(totalSupply + amount <= MAX_SUPPLY, "exceeds max supply"); balances[to] += amount; totalSupply += amount; } function transfer(address to, uint256 amount) external { require(to != address(0), "Zero address"); require(amount > 0, "Zero amount"); require(balances[msg.sender] >= amount, "Insufficient balance"); require(balances[to] + amount >= balances[to], "Balance overflow"); balances[msg.sender] -= amount; balances[to] += amount; } function batchAdd(uint256[] calldata amounts) external { uint256 currentBalance = b
프로젝트로 돌아가기