掌握 Solidity:以太坊开发者必知的面试问题

Solidity 中一些实践中经常遇到的问题

介绍

在以太坊开发的动态领域中,Solidity 专业知识至关重要。Solidity 开发人员的面试通常包括实际的编码挑战,以评估候选人解决现实问题的能力。本文提供了一个全面的指南,涵盖了重要的面试问题,附有 Solidity 0.8.18 的示例代码、现实场景、常见陷阱和最佳实践。

简单问题

1. 🔄 transfersend 的区别

问题: 演示 Solidity 中 transfersend 函数的区别。

pragma solidity ^0.8.18;

contract TransferSendDemo {  
    address payable public recipient = payable(address(0x123));

    function transferFunds() public payable {  
        recipient.transfer(msg.value);  
    }

    function sendFunds() public payable {  
        bool sent = recipient.send(msg.value);  
        require(sent, "Failed to send Ether");  
    }  
}

解释:

  • transfer 如果转账失败会回滚,转发 2300 gas,足以完成大多数基本操作,但如果接收合约较复杂可能会失败。
  • send 返回一个布尔值表示成功或失败,也转发 2300 gas,不会回滚。它不太安全,但可以用于条件逻辑。

现实场景:

  • 对于简单交易使用 transfer。如果与可能消耗更多 gas 的合约交互,优先使用带有适当检查的 send

陷阱:

  • 避免在没有正确处理失败情况的情况下使用 send,因为它可能会静默失败。

2. 🌀 高效的 for 循环

问题: 在 Solidity 中编写一个高效的 for 循环。

pragma solidity ^0.8.18;

contract EfficientLoop {  
    uint256[] public numbers;

    function addNumbers(uint256 _count) public {  
        for (uint256 i = 0; i < _count; i++) {  
            numbers.push(i);  
        }  
    }  
}

解释:

  • 确保循环经过优化,避免过多的迭代导致 out-of-gas 错误。使用较小的循环和批处理操作可以节省 gas。

现实场景:

  • 对于需要遍历大量数据集的合约函数,考虑使用链下处理或优化循环以避免高 gas 成本。

陷阱:

  • 大型循环可能导致高 gas 成本和交易失败。始终使用不同的输入大小进行测试。

3. 🔍 代理合约中的存储冲突

问题: 解释代理合约中的存储冲突并用示例演示。

pragma solidity ^0.8.18;

contract Proxy {  
    address public implementation;

    function setImplementation(address _impl) public {  
        implementation = _impl;  
    }

    fallback() external payable {  
        (bool success, ) = implementation.delegatecall(msg.data);  
        require(success, "Delegatecall failed");  
    }  
}

contract ImplementationV1 {  
    uint256 public value;  
}

contract ImplementationV2 {  
    uint256 public newValue;   
}

解释:

  • 存储冲突发生在不同的实现有重叠的存储布局时。确保可升级合约仔细管理存储槽。

现实场景:

  • 在设计可升级合约时使用清晰且一致的存储布局。考虑使用库来避免冲突。

陷阱:

  • 意外覆盖存储槽可能会破坏合约状态。使用像 OpenZeppelin 的可升级合约这样的工具来降低风险。

中等问题

4. 📊 存储与内存中的数组处理

问题: 存储与内存中的数组在存储槽使用上有何不同?

pragma solidity ^0.8.18;

contract ArrayDemo {  
    uint64[] public storageArray = [1, 2, 3, 4, 5];

    function memoryArray() public pure returns (uint64[] memory) {  
        uint64[] memory tempArray = new uint64[](5);  
        tempArray[0] = 1;  
        tempArray[1] = 2;  
        tempArray[2] = 3;  
        tempArray[3] = 4;  
        tempArray[4] = 5;  
        return tempArray;  
    }  
}

解释:

  • storage 中的数组是持久的,访问和修改成本更高。memory 中的数组是临时的,成本较低但不持久。

现实场景:

  • 对于临时计算使用 memory,对于需要持久化的数据使用 storage。选择时注意 gas 成本。

陷阱:

  • 错误管理存储和内存可能导致高 gas 成本和低效。始终使用 memory 进行临时操作以节省 gas。

5. 🛠️ abi.encodeabi.encodePacked

问题: 展示 abi.encodeabi.encodePacked 的区别。

pragma solidity ^0.8.18;

contract EncodingDemo {  
    function encodeData(uint256 num, string memory str) public pure returns (bytes memory, bytes memory) {  
        bytes memory encoded = abi.encode(num, str);  
        bytes memory packed = abi.encodePacked(num, str);  
        return (encoded, packed);  
    }  
}

解释:

  • abi.encode 提供完整的 ABI 兼容编码,包含类型信息,而 abi.encodePacked 生成更紧凑的字节数组,可用于哈希但可能导致冲突。

现实场景:

  • 使用 abi.encode 编码合约交互数据,使用 abi.encodePacked 创建紧凑的哈希。

陷阱:

  • 如果不小心使用,abi.encodePacked 可能导致哈希冲突,特别是在连接多个变量时。

6. 🔄 ERC4626 中的通胀攻击

问题: 什么是 ERC4626 中的通胀攻击,如何演示?

pragma solidity ^0.8.18;

contract ERC4626Vault {  
    mapping(address => uint256) public balances;  
    uint256 public totalSupply;

    function deposit(uint256 amount) public {  
        balances[msg.sender] += amount;  
        totalSupply += amount;  
    }  
}

解释:

  • 如果攻击者利用代币发行或份额计算中的漏洞,可能会发生 ERC4626 中的通胀攻击,可能会铸造过多的代币。

现实场景:

  • 始终审核份额计算逻辑,并考虑可能导致意外代币通胀的边缘情况。

陷阱:

  • 确保计算准确,并考虑实施保护措施以防止未经授权的代币创建。

困难问题

7. ⚙️ 自定义错误与带错误字符串的 require

问题: 比较自定义错误和带错误字符串的 require 在 EVM 层的编码方式。

pragma solidity ^0.8.18;

contract ErrorDemo {  
    error CustomError(string message);

    function requireError(bool condition) public pure {  
        require(condition, "Error occurred");  
    }

    function customErrorFunction(bool condition) public pure {  
        if (!condition) {  
            revert CustomError("Custom error occurred");  
        }  
    }  
}

解释:

  • 自定义错误更节省 gas,因为它们在交易数据中使用的空间比 require 中的字符串少。当错误频繁或错误消息较大时,自定义错误可以节省 gas。

现实场景:

  • 在频繁交互或处理大量数据的合约中使用自定义错误进行错误处理,以降低交易成本。

陷阱:

  • 自定义错误需要在客户端代码中仔细处理,以正确解码和显示错误消息。

8. 🧩 代理中的函数选择器冲突

问题: 什么是代理中的函数选择器冲突,它是如何发生的?

pragma solidity ^0.8.18;

contract Proxy {  
    address public implementation;

    function setImplementation(address _impl) public {  
        implementation = _impl;  
    }

    fallback() external payable {  
        (bool success, ) = implementation.delegatecall(msg.data);  
        require(success, "Delegatecall failed");  
    }  
}

contract ClashExample {  
    function clash(uint256 x) public pure returns (uint256) {  
        return x * 2;  
    }

    function clash(uint256 x, uint256 y) public pure returns (uint256) {  
        return x + y;  
    }  
}

解释:

  • 函数选择器冲突发生在不同的函数具有相同的选择器时,这可能导致错误的函数执行。避免在不同的实现中使用相同的函数签名。

现实场景:

  • 实现版本控制或唯一的函数选择器以防止可升级合约中的冲突。

陷阱:

  • 在合约升级期间测试选择器冲突以避免意外行为。

9. 📜 代理上下文中的信标

问题: 在代理上下文中,什么是信标,它是如何使用的?

pragma solidity ^0.8.18;

contract Beacon {  
    address public implementation;

    function setImplementation(address _impl) public {  
        implementation = _impl;  
    }  
}

contract Proxy {  
    Beacon public beacon;

    function setBeacon(address _beacon) public {  
        beacon = Beacon(_beacon);  
    }

    fallback() external payable {  
        address impl = beacon.implementation();  
        (bool success, ) = impl.delegatecall(msg.data);  
        require(success, "Delegatecall failed");  
    }  
}

解释:

  • 信标合约集中管理当前实现的地址,允许多个代理使用单个信标来解析其实现。

现实场景:

  • 在可升级代理模式中使用信标,以高效管理多个代理的合约升级。

陷阱:

  • 确保信标合约安全且妥善管理,以避免漏洞。

结论

掌握 Solidity 不仅需要理论知识,还需要处理实际场景的实践技能。本文提供的问题和示例涵盖了 Solidity 开发的基本实践方面,从处理 gas 高效循环到管理代理合约和自定义错误。理解这些概念,识别常见陷阱,并应用最佳实践,将为你在以太坊开发面试和实际应用中取得成功做好准备。

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Magda Jankowska
Magda Jankowska
Security Researcher for Web3