2.1 简述 Solidity 的数据类型、作用域、函数修饰符。
-
数据类型:
-
值类型(Value Types):
uint
,int
,bool
,address
,bytes1
到bytes32
,enum
-
引用类型(Reference Types):
array
,struct
,mapping
-
特殊类型:
string
(动态字节数组)
-
-
作用域(Scope):
-
状态变量(State variables):存储在区块链上的合约存储中,合约生命周期内存在。
-
局部变量(Local variables):函数内部声明的变量,生命周期仅限于函数调用。
-
全局变量(Global variables):Solidity提供的如
msg.sender
,block.timestamp
等,访问链上信息。
-
-
函数修饰符(Function Modifiers):
用于改变函数行为的代码片段,可以控制访问权限、前置条件、后置操作等。常见如onlyOwner
限制调用者,payable
允许函数接收以太币等。修饰符可以复用代码,提高合约安全性。
2.2 require、assert、revert 区别?使用场景?
-
require(condition, message):
用于输入校验和外部条件判断。失败时回退交易并返还剩余Gas。常用于验证用户输入、状态或权限。 -
assert(condition):
用于检查不变量(程序内部错误)。失败时消耗所有Gas,通常表示合约出现严重BUG。 -
revert(message):
显式回退交易,通常用于复杂的错误处理流程。与require类似,但可嵌套调用并提供错误信息。
总结:
-
用
require
验证外部输入和条件,防止错误操作。 -
用
assert
检测不应发生的内部错误。 -
用
revert
做复杂错误处理或在条件不满足时回退。
2.3 什么是重入攻击(Reentrancy)?如何防御?
-
重入攻击指攻击者在一个合约调用过程中,通过递归调用同一合约的漏洞函数,重复利用尚未更新状态的合约逻辑,窃取资产。最经典的例子是The DAO攻击。
-
防御方法:
-
“检查-效果-交互”模式:先修改合约状态,再调用外部合约。
-
使用
ReentrancyGuard
修饰器(如OpenZeppelin库),防止函数被重入调用。 -
限制调用次数和控制流。
-
减少对外部合约的依赖。
-
2.4 如何通过 Node.js 与 Solidity 合约交互?
-
主要借助 web3.js 或 ethers.js 这两个库。流程如下:
-
通过ABI和合约地址创建合约实例。
-
配置钱包私钥和Provider(如Infura、Alchemy或本地节点)。
-
调用合约的读取方法(call)或写入方法(send),写入需签名交易。
-
监听交易回执和事件。
-
示例代码(用ethers.js):
const { ethers } = require("ethers");
const provider = new ethers.providers.InfuraProvider("homestead", "INFURA_API_KEY");
const wallet = new ethers.Wallet("PRIVATE_KEY", provider);
const contractABI = [...]; // 合约ABI
const contractAddress = "0xYourContractAddress";
const contract = new ethers.Contract(contractAddress, contractABI, wallet);
async function transferTokens(to, amount) {
const tx = await contract.transfer(to, amount);
await tx.wait();
console.log("Transfer successful:", tx.hash);
}
2.5 写一个简单的ERC-20或ERC-721合约,并用 JavaScript 发起转账交易。
示例:一个简单的ERC-20合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleToken {
string public name = "SimpleToken";
string public symbol = "STK";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
}
function transfer(address _to, uint256 _value) public returns (bool) {
require(balanceOf[msg.sender] >= _value, "Insufficient balance");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}
JavaScript 发送转账交易示例(用ethers.js):
const { ethers } = require("ethers");
const provider = new ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_ID");
const privateKey = "YOUR_PRIVATE_KEY";
const wallet = new ethers.Wallet(privateKey, provider);
const contractAddress = "YOUR_CONTRACT_ADDRESS";
const abi = [
"function transfer(address to, uint amount) public returns (bool)",
];
const contract = new ethers.Contract(contractAddress, abi, wallet);
async function sendToken(to, amount) {
const decimals = 18;
const amountInWei = ethers.utils.parseUnits(amount.toString(), decimals);
const tx = await contract.transfer(to, amountInWei);
console.log("Transaction hash:", tx.hash);
await tx.wait();
console.log("Transfer confirmed");
}
sendToken("0xRecipientAddress", 10).catch(console.error);