university.alchemy8

Your Goal: Vote Changing 你的目标:更改投票

Modify the castVote function to allow voters to change their vote on a particular proposal.
修改castVote函数,允许投票者更改他们对特定提案的投票。

答案

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Voting {
struct Proposal {
address target;
bytes data;
uint yesCount;
uint noCount;
}

Proposal[] public proposals;

// Mapping to track if an address has voted on a particular proposal
mapping(uint => mapping(address => bool)) public hasVoted;

// Mapping to store the vote (true = yes, false = no) of each address for each proposal
mapping(uint => mapping(address => bool)) public votes;

// Function to create a new proposal
function newProposal(address addr, bytes calldata data) external {
Proposal memory newProposalInstance = Proposal(addr, data, 0, 0);
proposals.push(newProposalInstance);
}

// Function to cast or change a vote
function castVote(uint proposalId, bool support) external {
// Ensure the proposal exists
require(proposalId < proposals.length, "Proposal does not exist");

// Retrieve the proposal to update its vote counts
Proposal storage targetProposal = proposals[proposalId];

// If the voter has already voted, we need to change their vote
if (hasVoted[proposalId][msg.sender]) {
// If the voter previously voted "yes" and now votes "no"
if (votes[proposalId][msg.sender] == true && !support) {
targetProposal.yesCount -= 1; // Decrease yesCount
targetProposal.noCount += 1; // Increase noCount
}
// If the voter previously voted "no" and now votes "yes"
else if (votes[proposalId][msg.sender] == false && support) {
targetProposal.noCount -= 1; // Decrease noCount
targetProposal.yesCount += 1; // Increase yesCount
}
} else {
// If the voter has not voted yet, just increase the respective count
if (support) {
targetProposal.yesCount += 1;
} else {
targetProposal.noCount += 1;
}
}

// Update the voter's choice
votes[proposalId][msg.sender] = support;

// Mark that this address has voted for this proposal
hasVoted[proposalId][msg.sender] = true;
}
}

问题

我不太懂这两行:

1
2
3
4
// 使用二维映射跟踪每个提案上每个地址是否已经投过票
mapping(uint => mapping(address => bool)) public hasVoted;
// 使用二维映射跟踪每个地址在每个提案上的投票支持情况(true表示支持,false表示反对)
mapping(uint => mapping(address => bool)) public votes;

能不能解释的详细一点

好的,我来详细解释一下这两行代码。

1. mapping(uint => mapping(address => bool)) public hasVoted;

这行代码声明了一个 二维映射。我们来分解这个声明:

  • mapping(uint => mapping(address => bool)):

    • uint: 外层 mapping 的键是一个 uint 类型,即提案的编号(proposalId)。
    • mapping(address => bool): 内层 mapping 的键是一个 address 类型,表示每个选民(即每个地址)。对应的值是一个 bool 类型,表示该选民是否已经对该提案投过票。true 表示已经投过票,false 表示没有投票。

所以,这个映射的意思是:对于每个提案(通过 uint proposalId 标识),我们记录每个选民(address)是否已经投过票。它的作用是 检查选民是否已经投票

举个例子:

假设有两个提案,分别编号为 01。如果一个选民地址 0x123... 对提案 0 投过票,hasVoted[0][0x123...] 的值为 true。如果没有投票,则为 false

2. mapping(uint => mapping(address => bool)) public votes;

这行代码声明了另一个 二维映射,与上面的 hasVoted 类似,但作用不同:

  • mapping(uint => mapping(address => bool)): 还是和上面一样,外层的 uint 代表提案的编号(proposalId),内层是 address,表示选民地址,值是 bool,表示选民对某个提案的投票情况。

    • 这个 bool 值的含义是:true 表示选民支持该提案(投了赞成票),false 表示选民反对该提案(投了反对票)。

所以,这个映射的作用是 记录每个选民在每个提案上的投票支持情况

举个例子:

假设有两个提案,编号分别是 01。选民地址 0x123... 对提案 0 投了赞成票,那么 votes[0][0x123...] 的值就是 true。如果他投了反对票,那么 votes[0][0x123...] 就是 false

总结这两个映射的作用:

  1. hasVoted 映射:

    • 用来记录每个选民是否对特定提案投过票。
    • 主要用于防止选民重复投票。如果 hasVoted[proposalId][msg.sender] == true,说明该选民已经对该提案投过票,不能再次投票。
  2. votes 映射:

    • 用来记录每个选民在某个提案上投票的具体结果(支持或反对)。
    • 如果 votes[proposalId][msg.sender] == true,说明该选民支持该提案;如果 votes[proposalId][msg.sender] == false,说明该选民反对该提案。
    • 这个映射允许我们在选民更改投票时进行更新(例如,选民从支持变成反对,或反之)。

为什么需要两个映射?

  • hasVoted 主要用于判断选民是否已经投过票。这个映射只关心是否投过票,而不关心具体的投票内容(支持或反对)。
  • votes 主要用于存储选民在某个提案上的投票结果(支持或反对)。这可以用来在选民更改投票时更新票数。

因此,hasVoted 映射确保每个选民只能投一次票,而 votes 映射用于记录投票的具体选择,并在选民更改投票时更新。

普通mapping

Key键值对 value
address地址 bool布尔值

编写方法:
mapping(address => bool) public members;
members是这个映射的名字
members[address] = false; 这样赋值

两层映射

好的,下面是一个符合你要求的表格,展示了如何使用 两层映射 来存储数据:

两层映射(二维映射)示例表格:

Key1 (外层映射的键) Key2 (内层映射的键) Value (内层映射的值)
address address bool

示例代码:

1
2
// 定义一个二维映射,记录一个地址对另一个地址的投票状态(是否已经投票)
mapping(address => mapping(address => bool)) public votes;

在这个映射中:

  • 外层的 address 代表某个选民(或投票者)的地址。
  • 内层的 address 代表投票的目标地址(如提案的地址或其他合约地址)。
  • bool 值表示该选民是否已经对目标地址(如某个提案)投过票,true 表示已经投过,false 表示没有投过。

示例赋值:

1
2
// 设置选民 0x123... 对提案 0xabc... 是否已经投票
votes[0x123...][0xabc...] = true; // 选民 0x123... 对提案 0xabc... 投了票,记录为已投票

解释:

  • 外层 address 表示投票者的地址(如某个选民)。
  • 内层 address 表示该选民投票的目标地址(如提案的地址)。
  • bool 表示该选民是否对这个目标地址投过票,true 表示已投票,false 表示没有投票。

总结:

  • 两层映射 用于跟踪每个选民对多个目标的投票状态,可以在任何场景中使用,比如提案投票、权限控制等。

这两个address可以不可以替换位置?

是的,这两个 address 是可以交换位置的,但这样做会改变映射的含义。

具体来说,如果你交换外层和内层 address 的位置,你将改变映射的逻辑含义。让我们分别看一下两种情况。

1. 当前的映射(外层 address 是投票者,内层 address 是目标地址):

1
mapping(address => mapping(address => bool)) public votes;

含义

  • 外层 address 代表投票者的地址(例如,某个选民)。
  • 内层 address 代表目标地址,通常是提案或其他对象的地址。
  • bool 值表示该投票者是否对该目标地址(如提案)投过票。

例子:

  • votes[0x123...][0xabc...] = true;

    • 选民 0x123... 已经对提案 0xabc... 投了票。

2. 交换位置后的映射(外层 address 是目标地址,内层 address 是投票者):

1
mapping(address => mapping(address => bool)) public votes;

含义

  • 外层 address 代表目标地址(例如,提案地址)。
  • 内层 address 代表投票者的地址(例如,选民)。
  • bool 值表示该选民是否已经对该目标地址(如提案)投票。

例子:

  • votes[0xabc...][0x123...] = true;

    • 提案 0xabc... 已经收到选民 0x123... 的投票。

对比:

  • 当前版本:外层 address 是投票者,内层 address 是目标地址,适用于记录每个投票者的投票情况。
  • 交换版本:外层 address 是目标地址,内层 address 是投票者,适用于记录每个目标(如提案)的投票情况,能快速查看某个提案收到哪些选民的投票。

总结:

  • 交换位置是 可以的,但这样会改变映射的逻辑含义。原本的结构是按“投票者 → 目标地址”来记录的,交换后会变成“目标地址 → 投票者”。
  • 具体选择哪种方式取决于你要存储和查询的数据类型及用途。如果你关心的是每个投票者对哪些提案投票,使用原来的结构;如果你关心的是每个提案收到哪些投票,使用交换后的结构。

我记不住用哪种方式,该怎么办

如果你很难记住两种方式的具体用法,这里有一些方法和技巧可以帮助你做出决定并记住正确的方式:

1. 根据需求来选择

  • 如果你关心的是每个投票者的投票情况,例如:你想知道每个选民对哪个提案投了票,那么应该使用 **mapping(address => mapping(address => bool))**。这种方式能方便地查询某个选民在所有提案中的投票状态。

    • 示例应用:比如一个选民 0x123... 是否已经投票,或者他们是否支持提案 0xabc...
  • 如果你关心的是每个提案的投票情况,例如:你想知道每个提案收到哪些选民的投票,那么应该使用 **mapping(address => mapping(address => bool))**,将外层 address 设置为提案地址,内层 address 设置为投票者的地址。这样可以快速查询某个提案得到了哪些选民的支持或反对。

    • 示例应用:比如查看提案 0xabc... 有没有选民 0x123... 投票。

2. 通过函数命名或注释提醒自己

  • 函数命名:通过函数命名来帮助自己记住是哪种方式。

    • 例如:如果你是处理每个提案的投票情况,可以使用 castVoteOnProposal(address proposalAddress)
    • 如果你是处理每个投票者的投票情况,可以命名为 hasVoted(address voterAddress)
  • 注释:在代码中加入注释,明确你使用的映射的含义,帮助自己快速理解:

    1
    2
    3
    // 外层是提案地址,内层是投票者地址
    // 记录投票者是否对特定提案投票
    mapping(address => mapping(address => bool)) public votesOnProposals;

3. 图示方法

  • 图示帮助记忆:你可以用图示的方式帮助自己记住映射的含义。比如画一个图,标明外层映射和内层映射代表的内容:

    • 外层address 代表选民或提案。
    • 内层address 代表提案或选民。
  • 例如:

    • 外层 address → 投票者,内层 address → 提案:votes[voter][proposal]
    • 外层 address → 提案,内层 address → 投票者:votes[proposal][voter]

4. 记忆法:使用“谁对谁投票”

  • 谁对谁投票:你可以用这个简单的规则来记忆:

    • **”外层是投票者,内层是提案”**:如果你关心的是每个选民对哪个提案投票,就用外层 address 为投票者,内层 address 为提案。
      例如:votes[voter][proposal]
    • **”外层是提案,内层是投票者”**:如果你关心的是每个提案的投票情况,外层 address 为提案,内层 address 为投票者。
      例如:votes[proposal][voter]

5. 总结规则

  • 外层 address 是谁?如果是投票者,外层就是投票者的地址;如果是提案,外层就是提案的地址。
  • 内层 address 是谁?如果是投票者,内层就是投票者的地址;如果是提案,内层就是提案的地址。

最后,最重要的提示:

无论你选择哪种方式,最关键的是 明确你需要追踪的数据,然后根据需求选择适合的映射结构。你不需要记住所有细节,只需要记住 “你要存什么数据” 和 **”你希望查询什么数据”**,然后根据这些来选择映射的结构。

希望这些技巧能帮你更好地理解和记住两层映射的使用!

Your Goal: Proposal Created & Vote Cast Events

你的目标:提案创建和投票事件
Create an event ProposalCreated which takes a single argument: a uint proposal ID. Emit this event whenever a new Proposal struct is created.
创建一个事件ProposalCreated,它接受一个参数:一个uint类型的提案ID。每当创建新的Proposal结构体时,就触发该事件。
Create an event VoteCast which takes two arguments: a uint proposal ID and an address for the voter’s address. Emit this event any time a new vote is cast.
创建一个名为VoteCast的事件,它接受两个参数:一个uint类型的提案ID和一个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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Voting {
struct Proposal {
address target;
bytes data;
uint yesCount;
uint noCount;
}

Proposal[] public proposals;
event ProposalCreated(uint proposalId);
event VoteCast(uint proposalId, address voter);
// Mapping to track if an address has voted on a particular proposal
mapping(uint => mapping(address => bool)) public hasVoted;

// Mapping to store the vote (true = yes, false = no) of each address for each proposal
mapping(uint => mapping(address => bool)) public votes;

// Function to create a new proposal
function newProposal(address addr, bytes calldata data) external {
Proposal memory newProposalInstance = Proposal(addr, data, 0, 0);
proposals.push(newProposalInstance);
emit ProposalCreated(proposals.length - 1); // proposalId is the index of the newly added proposal
//每当创建新的Proposal结构体时,就触发该事件。
}

// Function to cast or change a vote
function castVote(uint proposalId, bool support) external {
// Ensure the proposal exists
require(proposalId < proposals.length, "Proposal does not exist");

// Retrieve the proposal to update its vote counts
Proposal storage targetProposal = proposals[proposalId];

// If the voter has already voted, we need to change their vote
if (hasVoted[proposalId][msg.sender]) {
emit VoteCast(proposalId, msg.sender);
// If the voter previously voted "yes" and now votes "no"
if (votes[proposalId][msg.sender] == true && !support) {
targetProposal.yesCount -= 1; // Decrease yesCount
targetProposal.noCount += 1; // Increase noCount
}
// If the voter previously voted "no" and now votes "yes"
else if (votes[proposalId][msg.sender] == false && support) {
targetProposal.noCount -= 1; // Decrease noCount
targetProposal.yesCount += 1; // Increase yesCount
}
} else {
emit VoteCast(proposalId, msg.sender);
// If the voter has not voted yet, just increase the respective count
if (support) {
targetProposal.yesCount += 1;
} else {
targetProposal.noCount += 1;
}
}

// Update the voter's choice
votes[proposalId][msg.sender] = support;

// Mark that this address has voted for this proposal
hasVoted[proposalId][msg.sender] = true;
}
}

知识点

以下是对 emit ProposalCreated(proposals.length - 1); 语句的详细解释:

语句解析:

  1. **emit**:
    emit 是 Solidity 中用来触发事件的关键字。当使用 emit 时,它表示事件已经发生,并将事件数据记录到区块链的交易日志中。这些日志对外部消费者(如前端应用程序)是可访问的,可以用于追踪和通知。

  2. **ProposalCreated**:
    这是正在触发的事件。在你的合约中,ProposalCreated 是一个自定义事件,它在创建新提案时被触发,并且接收一个 proposalId(类型为 uint)作为参数:

    1
    event ProposalCreated(uint proposalId);

    这个事件用于在创建新的提案时触发,并记录提案的 ID(即 proposalId)。

  3. **proposals.length - 1**:
    proposals 是一个数组,用于存储所有已创建的提案。proposals.length 返回数组中当前的提案数量,即已经创建的提案数量。

    由于 Solidity 中的数组是零索引的,因此数组中的最后一个元素的索引是 proposals.length - 1。因此,proposals.length - 1 返回的是最新添加的提案的索引,也就是当前交易中新创建的提案的 proposalId

发生了什么:

  • 每当创建一个新的提案时,它会被添加到 proposals 数组中。
  • 在提案添加后,proposals.length 会反映数组的新长度(比之前的长度大 1)。
  • 为了获取新创建提案的 索引(即 proposalId),我们将数组的长度减去 1,因为 Solidity 中的数组是从 0 开始的。

例如:

  • 如果数组中已有 2 个提案(proposals.length == 2),那么添加一个新的提案后,数组的长度将变为 3,但新添加的提案的索引是 2(proposals.length - 1 = 2)。

因此,emit ProposalCreated(proposals.length - 1) 会触发一个包含新提案索引的事件。

示例工作流程:

  1. 创建提案前
    假设 proposals 数组中没有任何提案:

    1
    proposals.length == 0
  2. 创建提案
    在调用 newProposal 函数后,新的提案会被添加到 proposals 数组中:

    1
    proposals.push(newProposalInstance);

    现在,proposals.length == 1(数组中有一个提案)。

  3. 触发事件

    • 语句 emit ProposalCreated(proposals.length - 1) 会触发事件,并将 proposalId 作为参数传递。在这种情况下,proposalId 将是 0,因为新创建的提案的索引是 0。
    • 触发的事件将是:
    1
    ProposalCreated(0);
  4. 下一个提案
    如果再添加一个提案,proposals.length 将变为 2,触发的事件将有 proposalId 为 1(即 proposals.length - 1 = 1)。

为什么这很重要?

触发的事件对外部交互非常有用。通过发出 proposalId,外部消费者(如前端应用程序或其他智能合约)可以轻松追踪哪个提案被创建,并基于这些信息采取进一步的操作。

总结:

  • emit ProposalCreated(proposals.length - 1) 触发了 ProposalCreated 事件,并将 proposalId(即新创建提案的索引)作为参数传递。
  • 索引是 proposals.length - 1,因为数组是从 0 开始的。

Your Goal: Members 你的目标:成员

Create a public constructor which takes an array of address. These addresses, plus the deployer of the function, should all be allowed to create new proposals and vote on those proposals.
创建一个公共的constructor,该函数接受一个address数组作为参数。这些地址以及该函数的部署者都应被允许创建新提案并对这些提案进行投票。
If anyone else attempts to create a proposal or vote, the call should be reverted.
如果有其他人试图创建提案或进行投票,调用应被回滚。

是先写还是后写constructor,在solidity里面有顺序要求吗?

在 Solidity 中,constructor 的定义顺序没有强制要求,既可以写在合约代码的开头,也可以写在其他函数/状态变量之后,编译器都能正常识别。

核心结论

Solidity 对 constructor 的位置没有语法限制,以下两种写法均合法且功能完全一致:

写法1:constructor 写在开头(推荐风格)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.8.0;

contract Example {
uint256 public x;

// constructor 写在状态变量之后、普通函数之前(最常见风格)
constructor(uint256 _x) {
x = _x;
}

function getX() public view returns (uint256) {
return x;
}
}

写法2:constructor 写在末尾(语法合法但不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.8.0;

contract Example {
uint256 public x;

function getX() public view returns (uint256) {
return x;
}

// constructor 写在普通函数之后(语法允许,但可读性差)
constructor(uint256 _x) {
x = _x;
}
}

关键补充说明

  1. 唯一性限制:一个合约只能有一个 constructor(Solidity 0.8.0+ 不支持重载 constructor,早期版本曾允许但已废弃)。
  2. 可见性关键字(可选):Solidity 0.7.0 之前,constructor 可加 public/internal 关键字(默认 public);0.7.0 及之后,public 关键字被废弃,默认即为公共构造函数,internal 仍可使用(表示合约仅可被继承,不能直接部署)。
  3. 最佳实践:虽然语法无限制,但推荐将 constructor 写在状态变量定义之后、普通函数之前。原因是:
    • 构造函数是合约初始化逻辑,位置靠前更符合“先初始化、后定义业务逻辑”的代码逻辑;
    • 提高可读性,其他开发者可快速找到合约的初始化参数和逻辑。

特殊场景:继承合约的 constructor 顺序

如果涉及合约继承,父合约的 constructor 会在子合约 constructor 执行前自动调用(与代码中定义的顺序无关),但子合约需显式指定父合约的构造参数(或父合约有默认构造函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.8.0;

contract Parent {
uint256 public parentX;
constructor(uint256 _parentX) {
parentX = _parentX; // 先执行父合约构造函数
}
}

contract Child is Parent {
uint256 public childY;

// 子合约 constructor 后执行,需显式传递父合约参数
constructor(uint256 _parentX, uint256 _childY) Parent(_parentX) {
childY = _childY;
}
}

总结

  • 语法层面:constructor 的定义位置无强制要求,任意顺序都能编译通过;
  • 工程实践:推荐放在状态变量之后、业务函数之前,提升代码可读性;
  • 继承场景:父合约 constructor 优先执行,子合约需显式传递父合约构造参数。

为什么写memory不写calldata?

1
2
3
constructor(address[] memory proposals) {
//该函数接受一个address数组作为参数。这些地址以及该函数的部署者都应被允许创建新提案并对这些提案进行投票。
} 为什么写memory不写calldata

在 Solidity 中,constructor引用类型参数(如数组、结构体、字符串) 选择 memory 而非 calldata,核心原因是:构造函数的参数存储位置规则与普通外部函数不同,calldata 对构造函数是非法的,只有 memory 是合法选择

先明确核心结论

对于 constructor(address[] ...) 这类参数:

  • ✅ 必须用 memory(Solidity 0.5+ 语法要求);
  • ❌ 不能用 calldata(编译报错,calldata 不适用于构造函数)。

关键原因:calldata 的适用场景限制

calldata 是 Solidity 中只读、临时的存储位置,专门用于:

  • 普通 external 函数的引用类型参数(如 function f(address[] calldata) external);
  • 普通 public 函数的引用类型参数(0.8.19+ 版本支持,之前版本仅 external 支持)。

它的设计目的是直接读取交易的输入数据(calldata 区域),不复制到内存,节省gas,但有一个硬性限制:
calldata 不能用于构造函数、内部函数(internal)、私有函数(private)的参数—— 因为这些场景的参数传递逻辑不经过“外部交易的 calldata 区域”,编译器不允许指定 calldata 存储位置。

为什么 memory 对构造函数是合法的?

memory 是 Solidity 中临时、可修改的存储位置,适用于:

  • 构造函数的引用类型参数(唯一合法选择);
  • 函数内部临时创建的引用类型变量;
  • 普通函数(internal/private)的引用类型参数。

构造函数的参数传递逻辑:

  1. 部署合约时,你传入的地址数组(如 ["0x123", "0x456"])会被编码为交易的输入数据;
  2. 编译器会自动将这些输入数据复制到 memory,供构造函数读取;
  3. 你在构造函数中对数组的操作(如循环遍历、读取元素),本质是操作 memory 中的副本,语法合法且高效。

对比:普通外部函数 vs 构造函数

为了更清晰,用表格对比两者的参数存储位置选择:

场景 引用类型参数(如 address[])允许的存储位置 推荐选择 原因
构造函数 constructor memory memory calldata 对构造函数非法
普通 external 函数 calldata / memory calldata 只读场景下更省gas(无需复制到内存)
普通 internal 函数 memory memory calldata 不适用于内部函数

示例:普通外部函数可用 calldata,但构造函数不行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.8.0;

contract Example {
mapping(address => bool) public hasPermission;

// 构造函数:必须用 memory(calldata 编译报错)
constructor(address[] memory proposers) {
hasPermission[msg.sender] = true;
for (uint i = 0; i < proposers.length; i++) {
hasPermission[proposers[i]] = true;
}
}

// 普通 external 函数:可用 calldata(更省gas)
function addProposers(address[] calldata newProposers) external {
for (uint i = 0; i < newProposers.length; i++) {
hasPermission[newProposers[i]] = true;
}
}
}

补充:为什么不用 storage

storage持久化存储位置(对应合约的状态变量,存储在链上),不能用于函数/构造函数的参数—— 因为参数是临时输入数据,不是合约的持久化状态,指定 storage 会编译报错。

总结

构造函数中写 address[] memory proposals 而非 calldata 的核心原因:

  1. calldata 仅适用于普通 external(或 0.8.19+ 的 public)函数,对构造函数是非法语法;
  2. memory 是构造函数引用类型参数的唯一合法选择,编译器会自动处理输入数据到内存的复制;
  3. 这种写法完全符合 Solidity 语法规范,且不会额外增加gas(构造函数的参数无论用什么存储位置,都会经过一次复制,memory 是最优解)。

简单记:构造函数的引用类型参数,直接用 memory 准没错