university.alchemy7

Your Goal: Is Member 你的目标:是否为成员

Create an external, view function called isMember which takes an address and returns a bool indicating whether or not the address is a member.
创建一个名为isMember的外部视图函数,该函数接受一个地址</b1并返回一个布尔值</b2,用于指示该地址</b3是否为成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
mapping(address => bool) public members;
function addMember(address addr) external {
//如何通过在members映射中该地址对应的数据源位置存储true来实现这一点。
members[addr] = true;
}
function isMember(address addr) external view returns(bool) {
//该函数接受一个地址</b1并返回一个布尔值</b2,用于指示该地址</b3是否为成员。
return members[addr];
}
}

Your Goal: Remove Member 你的目标:移除成员

Create an external function called removeMember that will take an address and set its corresponding value in the members mapping to false.
创建一个名为removeMember的外部函数,该函数将接收一个地址</b1,并将members映射中与其对应的 value 设置为false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
mapping(address => bool) public members;
function addMember(address addr) external {
//如何通过在members映射中该地址对应的数据源位置存储true来实现这一点。
members[addr] = true;
}
function isMember(address addr) external view returns(bool) {
//该函数接受一个地址</b1并返回一个布尔值</b2,用于指示该地址</b3是否为成员。
return members[addr];
}
function removeMember(address addr) external returns(bool) {
return members[addr] = false;
}
}

什么时候用view,什么时候函数不用view?

在 Solidity 中,view 是函数修饰符,核心规则是:**不修改合约状态的函数用 view,修改合约状态的函数不能用 view**。其本质是告诉编译器和链上节点:该函数仅读取数据、无副作用,可避免不必要的Gas消耗(纯本地调用时),同时明确函数的行为边界。

一、先明确:view 函数的核心限制(必须满足以下所有条件)

view 函数承诺不修改任何合约状态变量,一旦函数做了以下操作,就不能加 view

  1. 不修改状态变量(包括合约自身的状态变量、映射 mapping、数组 array 的元素);
  2. 不触发状态变更(如转账 transfer/send、调用非 view/pure 的外部合约函数);
  3. 不发送交易(如 call{value: ...} 带ETH转账,或调用会修改状态的外部函数);
  4. 不使用 selfdestructdelegatecall 等会改变合约状态的操作。

简单说:view 函数只能“读”,不能“写”。

二、什么时候必须用 view

只要函数满足「仅读取数据、不修改任何状态」,就应该加 view(语法允许省略,但不推荐,会降低代码可读性且可能误导调用者)。常见场景:

1. 查询合约状态变量(纯读取)

函数仅返回合约中定义的状态变量(如用户余额、配置参数、列表数据),无任何修改操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract Token {
mapping(address => uint256) public userBalances; // 状态变量:用户余额
uint256 public totalSupply; // 状态变量:总供给
address public owner; // 状态变量:拥有者

// 查用户余额:仅读取 mapping,无修改 → 用 view
function getBalance(address _user) public view returns (uint256) {
return userBalances[_user];
}

// 查总供给+拥有者:仅读取两个状态变量 → 用 view
function getTokenInfo() public view returns (uint256, address) {
return (totalSupply, owner);
}
}

2. 基于状态变量的计算(无副作用)

函数接收参数,结合合约状态做计算后返回结果,但不修改任何状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
contract MathContract {
uint256 public taxRate = 5; // 税率:5%

// 计算税后金额:接收输入+读取税率,仅计算不修改 → 用 view
function calculateAfterTax(uint256 _amount) public view returns (uint256) {
return _amount * (100 - taxRate) / 100;
}

// 验证是否为VIP:读取用户余额判断,无修改 → 用 view
function isVIP(address _user) public view returns (bool) {
return userBalances[_user] >= 1000 ether;
}
}

3. 读取链上全局变量(无修改)

仅读取 block.numberblock.timestampmsg.sender 等全局变量(不结合状态修改):

1
2
3
4
5
6
7
8
contract TimeLock {
uint256 public unlockTime;

// 检查是否已解锁:读取 unlockTime + block.timestamp,无修改 → 用 view
function isUnlocked() public view returns (bool) {
return block.timestamp >= unlockTime;
}
}

4. 调用其他 view/pure 函数(无状态变更)

函数内部仅调用其他 viewpure 函数(不触发状态修改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract Helper {
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
}

contract MyContract {
Helper public helper;
uint256 public base = 100;

// 调用 pure 函数+读取状态变量,无修改 → 用 view
function getTotal(uint256 x) public view returns (uint256) {
return helper.add(base, x);
}
}

三、什么时候不能用 view

只要函数会修改合约状态触发状态变更,就绝对不能加 view(加了会编译报错,或运行时出现不可预期行为)。常见场景:

1. 修改合约状态变量

函数直接修改合约的状态变量(包括赋值、数组/映射的增删改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract Token {
mapping(address => uint256) public userBalances;
uint256 public totalSupply;

// mint 代币:修改 totalSupply 和 userBalances → 不能用 view
function mint(address _to, uint256 _amount) public {
totalSupply += _amount;
userBalances[_to] += _amount;
}

// 转移余额:修改两个地址的 balance → 不能用 view
function transfer(address _to, uint256 _amount) public {
require(userBalances[msg.sender] >= _amount, "Insufficient balance");
userBalances[msg.sender] -= _amount;
userBalances[_to] += _amount;
}
}

2. 发送/接收ETH(触发转账状态)

函数中包含 transfersendcall{value: ...} 等转账操作(会修改合约和接收者的ETH余额状态):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Payment {
address public owner;

constructor() {
owner = msg.sender;
}

// 提取合约ETH:调用 transfer 修改余额 → 不能用 view
function withdraw() public {
require(msg.sender == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}

// 接收ETH(fallback/receive 函数默认无 view,因会修改合约余额)
receive() external payable {}
}

3. 调用非 view/pure 的外部函数

函数调用了外部合约中不带 view/pure 的函数(即使自身不直接修改状态,也可能通过外部调用触发状态变更):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract ExternalContract {
// 非 view 函数:会修改自身状态
function modifyState() public {
// ... 状态修改逻辑
}
}

contract MyContract {
ExternalContract public externalContract;

// 调用外部非 view 函数 → 不能用 view
function callExternal() public {
externalContract.modifyState();
}
}

4. 执行状态变更操作

函数包含 selfdestruct(销毁合约)、delegatecall(委托调用,可能修改状态)、create2(创建合约)等操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
contract Destructible {
address public owner;

constructor() {
owner = msg.sender;
}

// 销毁合约:修改合约状态(销毁后不可用)→ 不能用 view
function destroy() public {
require(msg.sender == owner, "Not owner");
selfdestruct(payable(owner));
}
}

四、关键补充:view 的Gas消耗规则(避免踩坑)

  1. 本地调用(off-chain):如果通过Web3.js/Ethers.js等库直接调用 view 函数(不发送交易),节点仅本地执行,不消耗任何Gas(因为无需上链);
  2. 链上调用(on-chain):如果在另一个非 view 函数中调用 view 函数(作为交易的一部分),view 函数的执行会被计入交易Gas(因为整个交易需要上链打包);
  3. 语法允许省略 view:如果函数本应是 view 但省略了修饰符,编译器不会报错,但调用者无法识别其“只读”特性,且本地调用仍会被当作“可能修改状态”处理(无Gas优化),强烈不推荐省略

总结:核心判断标准

函数行为 是否用 view
仅读取状态/计算,不修改任何数据 必须用(推荐显式添加)
修改状态变量、转账、调用非 view 函数 绝对不能用

简单记:**“读”用 view,“写”不用 view**,显式添加 view 能提升代码可读性、帮助调用者优化Gas,还能让编译器提前拦截非法状态修改。

Your Goal: User Mapping Tokens 你的目标:用户映射代币

Let’s create a new token where every new user will receive 100 tokens!
让我们创建一种新代币,每个新用户都将获得100个代币!

Create a public mapping called users that will map an address to a User struct.
创建一个名为users的公共映射,该映射将address映射到User结构体。
Create an external function called createUser. This function should create a new user and associate it to the msg.sender address in the users mapping.
创建一个名为createUser的外部函数。该函数应创建一个新用户,并将其关联到users映射中的msg.sender地址。
The balance of the new user should be set to 100 and the isActive boolean should be set to true.
新用户的balance应设置为100,且isActive布尔值应设置为true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
struct User {
uint256 balance; // 用uint256(Solidity 0.8+推荐,避免溢出风险)
bool isActive;
}
mapping(address => User) public users;
//User[] public users;
function createUser() external {
//require(users.bool !== true); 这个写法是错误的
//保证该函数的地址没有和一个活跃的账户关联
require(!users[msg.sender].isActive, "User already exists (active)");

users[msg.sender] = User(100,true);
//新用户的balance应设置为100,且isActive布尔值应设置为true
//users[msg.sender] = newUser;
//该函数应创建一个新用户,并将其关联到users映射中的msg.sender地址。
}
}

Your Goal: Transfer Amount 你的目标:用户映射代币

Create an external function called transfer which takes two parameters: an address for the recipient and a uint for the amount.
创建一个名为transfer的外部函数,该函数接受两个参数:一个用于接收者的address和一个用于金额的uint。
In this function, transfer the amount specified from the balance of the msg.sender to the balance of the recipient address.
在此函数中,将指定的amount从msg.sender的余额转移到接收者address的余额。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
struct User {
uint256 balance; // 用uint256(Solidity 0.8+推荐,避免溢出风险)
bool isActive;
}
mapping(address => User) public users;
//User[] public users;
function createUser() external {
//require(users.bool !== true); 这个写法是错误的
//保证该函数的地址没有和一个活跃的账户关联
require(!users[msg.sender].isActive, "User already exists (active)");

users[msg.sender] = User(100,true);
//新用户的balance应设置为100,且isActive布尔值应设置为true
//users[msg.sender] = newUser;
//该函数应创建一个新用户,并将其关联到users映射中的msg.sender地址。
}
function transfer(address addr,uint balance) external {
// 1. 校验调用者(转账方)状态:必须是活跃用户
require(users[msg.sender].isActive, "User already exists (active)");

// 2. 校验接收者状态:必须是活跃用户(符合注释要求)
require(users[addr].isActive, "User already exists (active)");

// 3. 校验接收者地址有效(非零地址)
require(addr != address(0), "Recipient address cannot be zero");

// 4. 校验转账金额大于0
require(balance > 0, "Transfer amount must be greater than 0");

// 5. 校验调用者余额充足
require(users[msg.sender].balance >= balance, "Insufficient balance");

//payable(msg.sender,balance,addr);
//将指定的amount从msg.sender的余额转移到接收者address的余额。
users[msg.sender].balance -= balance; // 扣减转账方余额
users[addr].balance += balance; // 增加接收方余额

// (可选)触发转账事件,方便前端追踪(推荐添加)
//emit transfer(msg.sender, addr, balance);
}
}

Your Goal: Make Connections 你的目标:建立连接

Create a public mapping called connections which will map an address to a mapping of an address to a ConnectionTypes enum value.
创建一个名为connections的公共映射,该映射会将一个address映射到另一个从address到ConnectionTypes枚举值的映射。
In theconnectWith function, create a connection from the msg.sender to the other address.
在connectWith函数中,创建一个从msg.sender到other地址的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
enum ConnectionTypes {
Unacquainted,
Friend,
Family
}

// TODO: create a public nested mapping `connections`
//mapping(address => User) public connections;
mapping(address => mapping(address => ConnectionTypes)) public connections;

function connectWith(address other, ConnectionTypes connectionType) external {
// TODO: make the connection from msg.sender => other => connectionType
//connections[msg.sender][ConnectionTypes] = other; 错误的代码
require(other != msg.sender, "Cannot connect with yourself");
connections[msg.sender][other] = connectionType;
}
}

Voting Contract 投票合约

In this tutorial we’re going to build a voting contract! We’ll use the lessons learned here to understand how the Governor standard emerged
在本教程中,我们将构建一个投票合约!我们会运用在这里学到的知识,来理解Governor标准是如何形成的。

Proposals 提案
We’re going to focus on creating a voting contract that will allow members to create new proposals. This contract can contain many proposals which will be voted on by a group of members. Each proposal will keep track of its votes to decide when its time to execute.
我们将专注于创建一个投票合约,该合约将允许成员创建新提案。这个合约可以包含许多提案,供一群成员进行投票。每个提案都会记录其票数,以决定何时执行。

At execution time, these proposals will send calldata to a smart contract. The calldata could be anything! We could have a Voting system that allows 100 members to decide when to upgrade a protocol. The calldata might target a function with the signature upgrade(address) and send over the new protocol implementation. That would be a very cool use of your Voting contract!
在执行时,这些提案会将调用数据发送到智能合约。调用数据可以是任何内容!我们可以有一个投票系统,允许100名成员决定何时升级协议。调用数据可能会以签名upgrade(address)为目标函数,并发送新的协议实现。这将是对你的投票合约非常酷的一种使用方式!

Your Goal: Proposals 你的目标:提案

Create a public array of type Proposal and call it proposals.
创建一个类型为Proposal的公共数组,并将其命名为proposals。
Create an external function newProposal which takes two arguments:
创建一个外部函数newProposal,它接受两个参数:
An address argument which will be the target address of the proposal. We’ll send some calldata to this address.
一个address参数,它将是提案的目标地址。我们会向这个地址发送一些调用数据。
A bytes argument which will be the calldata to eventually send to the smart contract when the proposal is executed.
一个bytes参数,它将是提案执行时最终发送给智能合约的调用数据。
In the newProposal function create a new Proposal with the arguments passed in and the yes/no vote counts are initialized at 0.
在newProposal函数中,使用传入的参数创建一个新的Proposal,赞成/反对票数初始化为0。
Add the new Proposal to the proposals array.
将新的Proposal添加到proposals数组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Voting {
struct Proposal {
address target;
bytes data;
uint yesCount;
uint noCount;
}
//Proposal[] public = proposals; 这样写是错误的
Proposal[] public proposals; // 语法:数组类型 可见性 变量名;
//创建一个类型为Proposal的公共数组,并将其命名为proposals。
function newProposal(address addr, bytes calldata data) external {
//创建一个外部函数newProposal,它接受两个参数
//一个address参数,它将是提案的目标地址。我们会向这个地址发送一些调用数据。
//一个bytes参数,它将是提案执行时最终发送给智能合约的调用数据。
//Proposal = Proposal[addr,data,0,0]; Proposal 是类型名,不能直接用 类型名 = 类型名[参数] 创建实例(这是其他语言的语法,Solidity 不支持)
Proposal memory newProposalInstance = Proposal(addr,data,0,0);

//在newProposal函数中,使用传入的参数创建一个新的Proposal,赞成/反对票数初始化为0。
proposals.push(newProposalInstance);
}
}

calldata的问题

function newProposal(address addr, bytes calldata data) external {

在 Solidity 中,calldata 是一个特殊的数据存储位置,专门用于函数参数,尤其是外部函数(external)的参数。它是只读的,具有一定的效率和燃气优化优势。

为什么需要使用 calldata

  1. 函数签名和燃气效率

    • 当一个函数被标记为 external 时,函数的参数是从外部传入的。参数通常存储在 memorystorage 中,取决于如何处理。而 calldata 是一个为外部函数调用优化的数据位置,它直接从交易输入数据中读取,这样可以减少燃气消耗。
    • 在你提供的例子中,data 参数是通过外部函数调用传递的。使用 calldata 可以确保该参数直接从交易输入数据中获取,而不需要复制到 memory 中,这样节省了燃气。
  2. calldata 是只读的

    • calldata 是一个只读数据存储位置,意味着它不能在函数中被修改。这对于只读取传入数据的函数来说非常合适。在你这个例子中,data 参数看起来只是用来读取的,所以使用 calldata 确保了数据的不可变性,并且 Solidity 编译器会相应地进行优化。
    • 相比之下,memory 是一个临时数据存储位置,允许修改,但使用 memory 比使用 calldata 更加消耗燃气。
  3. 外部函数调用的优化

    • 对于 external 函数,Solidity 会优化它们与其他合约的交互,而 calldata 是这种优化的核心部分。通过使用 calldata,数据不需要被复制到其他地方,因此可以减少内存和存储开销。

总结

使用 calldata 可以提高函数参数的读取效率,减少燃气费用,因为它是专为外部调用设计的,只读且不需要复制到 memory 中。

Your Goal: Cast Vote 你的目标:投票

Create an external function castVote which takes a uint proposalId and a bool which indicates whether the vote supports the proposal (true for yes, false for no).
创建一个外部函数castVote,它接收一个uint类型的proposalId和一个bool类型的参数,该参数用于表示投票是否支持该提案(true表示赞成,false表示反对)。
For each vote cast, update the yesCount and noCount in the referenced proposal accordingly.
对于每一张所投的票,相应地更新所引用提案中的yesCount和noCount。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Voting {
struct Proposal {
address target;
bytes data;
uint yesCount;
uint noCount;
}
//Proposal[] public = proposals; 这样写是错误的
Proposal[] public proposals; // 语法:数组类型 可见性 变量名;
//创建一个类型为Proposal的公共数组,并将其命名为proposals。
function newProposal(address addr, bytes calldata data) external {
//创建一个外部函数newProposal,它接受两个参数
//一个address参数,它将是提案的目标地址。我们会向这个地址发送一些调用数据。
//一个bytes参数,它将是提案执行时最终发送给智能合约的调用数据。
//Proposal = Proposal[addr,data,0,0]; Proposal 是类型名,不能直接用 类型名 = 类型名[参数] 创建实例(这是其他语言的语法,Solidity 不支持)
Proposal memory newProposalInstance = Proposal(addr,data,0,0);

//在newProposal函数中,使用传入的参数创建一个新的Proposal,赞成/反对票数初始化为0
proposals.push(newProposalInstance);
}
function castVote(uint proposalId, bool support) external {
//创建一个外部函数castVote,它接收一个uint类型的proposalId和一个bool类型的参数,该参数用于表示投票是否支持该提案(true表示赞成,false表示反对)
//对于每一张所投的票,相应地更新所引用提案中的yesCount和noCount
// 1. 校验:提案是否存在(proposalId 从 0 开始,需小于数组长度)
require(proposalId < proposals.length, "proposal not exist");

// 2. 校验:是否已投票(防止重复投票)
//require(!hasVoted[proposalId][msg.sender], "has voted");

// 3. 找到已存在的提案(用 storage 引用,直接修改链上数据,不用新建)
Proposal storage targetProposal = proposals[proposalId];

if (support == true) {
targetProposal.yesCount += 1;

}
else {
targetProposal.noCount += 1; // 反对:noCount +1
}
//hasVoted[proposalId][msg.sender] = true;
}
}