相关背景
在前面的系列文章中,我们已经学习了solidity的一些基础概念和知识,单纯的知识点学习起来比较枯燥,这一节我们会通过一个简单的示例来将这些知识整合起来。
我们期望实现一个名为Bank的合约。这个合约有以下要求:
需要实现的核心功能
- 1、合约部署之后,每个人都可以向合约里面转钱,好比,我们现实生活中向银行存钱一样。
- 2、但是我们的Bank合约比较特殊的地方在于,只有合约的所有者才能从合约中取钱,这个是不是有点反直觉,这个部分我们在后面会针对性的讲解。
- 3、我们还会统计出来存钱最多的几个用户。这个是后续奖励活动的凭证。
实现步骤
搭建合约的基础框架
题目中要求只有合约所有者才能从合约中取钱,这个很明显是个权限管理相关的需求,我们可以使用modified这个知识点实现。
一般的,合约的所有者是合约的部署者(特殊指定权限除外),这个权限在部署合约的时候就已经确定了,我们可以在合约的构造函数中来实现这个逻辑,来看代码的基础框架:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Bank {
// State variable owner, the owner of the contract
address public owner;
constructor() {
// In the constructor, set the owner to msg.sender, setting the deployer as the contract owner
owner = msg.sender;
}
}
就是这样,我们第一步已经完成了,我们声明了一个状态变量owner
,它是一个地址类型,可以外部可见(合约的所有者并不是一个秘密)
在构造函数中,我们将当前的EOA账户的值赋值给owner
变量,就这样,owner
的值除非我们有意修改,它永远的存储在链上了。
实现合约可以接受外部转账的功能
这里涉及到了一个知识点:
在以太坊智能合约中,如果没有处理发送到合约的以太币,转账操作将会被拒绝。为了处理这种情况,智能合约通常会实现一个receive()函数或fallback()函数来接收以太币。
关于receive和fallback函数的区别,我们在之前的文章中有所介绍,不太清楚的地方可以先停下来复习先关的知识点。
我们这里使用receive函数来实现转账功能。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Bank {
// State variable owner, the owner of the contract
address public owner;
constructor() {
// In the constructor, set the owner to msg.sender, setting the deployer as the contract owner
owner = msg.sender;
}
// Declare receive callback function, allowing the contract to receive native currency
receive() external payable {
// When someone sends Ether to the contract, directly call the deposit function
deposit();
}
}
上面代码中,我们加入了receive
函数,它是可以被外部调用的,具备payable
属性。可以接收以太币。
你可能已经发现了,在receive
函数中,我们调用了deposit()
函数,目前为止我们还没有实现这个函数。
我们为什么要在receive
函数内部调用deposit
函数呢,其实有个核心考虑,就是即使外部账户不显示的调用deposit
函数也可以给合约转账。
deposit函数的实现
deposit函数的实现比较简单,就是给转进钱来的账户更新余额,但是如果去管理这些账户和余额的关系也是个问题,我们这里可以使用mapping
结构,key代表的是账户地址,value代表这个账户的余额。
基于上面的思考,我们完善deposit函数的实现。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Bank {
// State variable owner, the owner of the contract
address public owner;
// Declare a mapping, address type -> account balance, internal access within the contract
mapping (address => uint256) private balances;
constructor() {
// In the constructor, set the owner to msg.sender, setting the deployer as the contract owner
owner = msg.sender;
}
// Declare receive callback function, allowing the contract to receive native currency
receive() external payable {
// When someone sends Ether to the contract, directly call the deposit function
deposit();
}
// Deposit function, allowing external accounts to deposit Ether into the contract
function deposit() public payable {
// The amount of native currency sent by the user must be greater than 0
require(msg.value > 0, "Deposit amount must be greater than zero");
// Update the balance of each address
balances[msg.sender] += msg.value;
}
}
我们在deposit函数中做了前置校验,只有用户转进来的钱大于0才有意义,这个很好理解。然后把用户的余额进行更新即可。需要注意的是deposit这个函数外部可见,我们显示的调用这个函数也是可以向合约中转钱的。
提现函数的实现
上文已经说到,只有合约的管理者才能提现,我们要实现一个modified来做权限管控,还有一个重点是,我们支持分批提现。这里需要注意对边界条件的判断。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Bank {
// State variable owner, the owner of the contract
address public owner;
// Declare a mapping, address type -> account balance, internal access within the contract
mapping (address => uint256) private balances;
constructor() {
// In the constructor, set the owner to msg.sender, setting the deployer as the contract owner
owner = msg.sender;
}
// Access control, only the contract owner can execute certain operations
modifier onlyOwner () {
require(msg.sender == owner, "Only owner can call this function");
_;
}
// Declare receive callback function, allowing the contract to receive native currency
receive() external payable {
// When someone sends Ether to the contract, directly call the deposit function
deposit();
}
// Deposit function, allowing external accounts to deposit Ether into the contract
function deposit() public payable {
// The amount of native currency sent by the user must be greater than 0
require(msg.value > 0, "Deposit amount must be greater than zero");
// Update the balance of each address
balances[msg.sender] += msg.value;
}
// Withdrawal function, only allows the contract owner to withdraw
function withdraw(uint256 amount) public onlyOwner {
// Get the current balance of the Bank contract
uint256 balance = address(this).balance;
// If the balance is insufficient, withdrawal is not supported
require(balance > 0, "Contract balance is zero");
// The amount to be withdrawn should be less than the current balance in the contract
require(amount <= balance, "Insufficient contract balance");
// The contract owner can withdraw, the unit is wei, note the conversion of units.
payable(owner).transfer(amount);
}
}
对转入钱的用户进行排序管理
我们很自然的想到使用数组这种数据结构来实现这个功能。具体的思路是,将新转入钱的账户添加到数组中,进行排序,我们的排名仅仅记录前几名,为了节省空间,我们会将后面名次的地址移除。
这里的排序,我们使用冒泡排序算法。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Bank {
// State variable owner, the owner of the contract
address public owner;
// Declare a mapping, address type -> account balance, internal access within the contract
mapping (address => uint256) private balances;
// An array to store the users with the highest deposits
address[] public topDepositors;
constructor() {
// In the constructor, set the owner to msg.sender, setting the deployer as the contract owner
owner = msg.sender;
}
// Access control, only the contract owner can execute certain operations
modifier onlyOwner () {
require(msg.sender == owner, "Only owner can call this function");
_;
}
// Declare receive callback function, allowing the contract to receive native currency
receive() external payable {
// When someone sends Ether to the contract, directly call the deposit function
deposit();
}
// Deposit function, allowing external accounts to deposit Ether into the contract
function deposit() public payable {
// The amount of native currency sent by the user must be greater than 0
require(msg.value > 0, "Deposit amount must be greater than zero");
// Update the balance of each address
balances[msg.sender] += msg.value;
}
// Withdrawal function, only allows the contract owner to withdraw
function withdraw(uint256 amount) public onlyOwner {
// Get the current balance of the Bank contract
uint256 balance = address(this).balance;
// If the balance is insufficient, withdrawal is not supported
require(balance > 0, "Contract balance is zero");
// The amount to be withdrawn should be less than the current balance in the contract
require(amount <= balance, "Insufficient contract balance");
// The contract owner can withdraw, the unit is wei, note the conversion of units.
payable(owner).transfer(amount);
}
// Continuously update the ranking
function updateTopDepositors(address depositor) internal {
// Set a flag to avoid duplication
bool exists = false;
// Traverse the array, if the current address already exists in topDepositors, exit the loop
for (uint256 i = 0; i < topDepositors.length; i++) {
if (topDepositors[i] == depositor) {
exists = true;
break;
}
}
// If it does not exist, add the current address to the array
if (!exists) {
topDepositors.push(depositor);
}
for (uint256 i = 0; i < topDepositors.length; i++) {
for (uint256 j = i + 1; j < topDepositors.length; j++) {
if (balances[topDepositors[i]] < balances[topDepositors[j]]) {
address temp = topDepositors[i];
topDepositors[i] = topDepositors[j];
topDepositors[j] = temp;
}
}
}
// After the array length exceeds 3, only keep the top three.
if (topDepositors.length > 3) {
topDepositors.pop();
}
}
}
最后实现辅助函数和相关事件。
为了能够便捷的获取排序后的数据,我们可以包装一个函数去返回数据,在我们转钱和提现的时候,需要抛出一些事件供外部分析使用。
完整代码如下
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Bank {
// State variable owner, the owner of the contract
address public owner;
// Declare a mapping, address type -> account balance, internal access within the contract
mapping (address => uint256) private balances;
// An array to store the users with the highest deposits
address[] public topDepositors;
// Declare an event triggered on withdrawal
event Withdrawal(address indexed to, uint256 amount);
// Declare an event triggered on deposit
event Deposit(address indexed from, uint256 amount);
constructor() {
// In the constructor, set the owner to msg.sender, setting the deployer as the contract owner
owner = msg.sender;
}
// Access control, only the contract owner can execute certain operations
modifier onlyOwner () {
require(msg.sender == owner, "Only owner can call this function");
_;
}
// Declare receive callback function, allowing the contract to receive native currency
receive() external payable {
// When someone sends Ether to the contract, directly call the deposit function
deposit();
}
// Deposit function, allowing external accounts to deposit Ether into the contract
function deposit() public payable {
// The amount of native currency sent by the user must be greater than 0
require(msg.value > 0, "Deposit amount must be greater than zero");
// Update the balance of each address
balances[msg.sender] += msg.value;
// Update the leaderboard
updateTopDepositors(msg.sender);
// Trigger the event
emit Deposit(msg.sender, msg.value);
}
// Withdrawal function, only allows the contract owner to withdraw
function withdraw(uint256 amount) public onlyOwner {
// Get the current balance of the Bank contract
uint256 balance = address(this).balance;
// If the balance is insufficient, withdrawal is not supported
require(balance > 0, "Contract balance is zero");
// The amount to be withdrawn should be less than the current balance in the contract
require(amount <= balance, "Insufficient contract balance");
// The contract owner can withdraw, the unit is wei, note the conversion of units.
payable(owner).transfer(amount);
// Trigger the withdrawal event
emit Withdrawal(owner, amount);
}
// Continuously update the ranking
function updateTopDepositors(address depositor) internal {
// Set a flag to avoid duplication
bool exists = false;
// Traverse the array, if the current address already exists in topDepositors, exit the loop
for (uint256 i = 0; i < topDepositors.length; i++) {
if (topDepositors[i] == depositor) {
exists = true;
break;
}
}
// If it does not exist, add the current address to the array
if (!exists) {
topDepositors.push(depositor);
}
for (uint256 i = 0; i < topDepositors.length; i++) {
for (uint256 j = i + 1; j < topDepositors.length; j++) {
if (balances[topDepositors[i]] < balances[topDepositors[j]]) {
address temp = topDepositors[i];
topDepositors[i] = topDepositors[j];
topDepositors[j] = temp;
}
}
}
// After the array length exceeds 3, only keep the top three.
if (topDepositors.length > 3) {
topDepositors.pop();
}
}
// View the account balance based on the specified address
function getBalance(address addr) public view returns(uint256) {
return balances[addr];
}
// Return the deposit leaderboard
function getTopDepositors() public view returns (address[] memory) {
return topDepositors;
}
}
总结
这个合约包含了多个Solidity编程的核心知识点:
-
状态变量:
address public owner;
mapping(address => uint256) private balances;
address[] public topDepositors;
-
事件(Events):
event Withdrawal(address indexed to, uint256 amount);
event Deposit(address indexed from, uint256 amount);
-
访问控制(Access Control):
- 使用
onlyOwner
修饰符限制某些函数只能由合约所有者调用:
soliditymodifier onlyOwner () { require(msg.sender == owner, "Only owner can call this function"); _; }
- 使用
-
构造函数(Constructor):
- 在合约部署时执行一次,用于初始化合约状态:
solidityconstructor() { owner = msg.sender; }
-
接收以太币(Receiving Ether):
- 使用
receive()
函数允许合约接收以太币:
solidityreceive() external payable { deposit(); }
- 使用
-
函数修饰符(Function Modifiers):
onlyOwner
修饰符用于限制函数访问权限。
-
存款函数(Deposit Function):
deposit()
函数允许用户向合约存入以太币,同时更新用户余额和存款排行榜:
solidityfunction deposit() public payable { require(msg.value > 0, "Deposit amount must be greater than zero"); balances[msg.sender] += msg.value; updateTopDepositors(msg.sender); emit Deposit(msg.sender, msg.value); }
-
取款函数(Withdrawal Function):
withdraw()
函数允许合约所有者从合约中提取指定数量的以太币:
solidityfunction withdraw(uint256 amount) public onlyOwner { require(address(this).balance > 0, "Contract balance is zero"); require(amount <= address(this).balance, "Insufficient contract balance"); payable(owner).transfer(amount); emit Withdrawal(owner, amount); }
-
内部函数(Internal Functions):
updateTopDepositors()
用于更新存款最多用户的排行榜:
solidityfunction updateTopDepositors(address depositor) internal { // 排名更新逻辑 }
-
视图函数(View Functions):
getBalance()
返回指定地址的余额:
solidityfunction getBalance(address addr) public view returns(uint256) { return balances[addr]; }
getTopDepositors()
返回存款排行榜:
solidityfunction getTopDepositors() public view returns (address[] memory) { return topDepositors; }
-
地址类型和单位转换(Address Type and Unit Conversion):
- 使用
payable
关键字将地址转换为可以接收以太币的地址。 wei
是以太坊的最小单位,用于处理货币值。
- 使用
-
数组操作(Array Operations):
- 操作数组
topDepositors
来管理和更新存款排行榜。
- 操作数组
通过这些知识点,这个合约实现了一个基本的银行功能,相信看到这里你已经完全掌握了。