在前面的系列文章中,我们已经学习了solidity的一些基础概念和知识,单纯的知识点学习起来比较枯燥,这一节我们会通过一个简单的示例来将这些知识整合起来。我们期望实现一个名为Bank的合约。
在前面的系列文章中,我们已经学习了solidity的一些基础概念和知识,单纯的知识点学习起来比较枯燥,这一节我们会通过一个简单的示例来将这些知识整合起来。
我们期望实现一个名为Bank的合约。这个合约有以下要求:
题目中要求只有合约所有者才能从合约中取钱,这个很明显是个权限管理相关的需求,我们可以使用modifier这个知识点实现。
一般的,合约的所有者是合约的部署者(特殊指定权限除外),这个权限在部署合约的时候就已经确定了,我们可以在合约的构造函数中来实现这个逻辑,来看代码的基础框架:
// 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函数来实现转账功能。
// 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函数的实现比较简单,就是给转进钱来的账户更新余额,但是如果去管理这些账户和余额的关系也是个问题,我们这里可以使用mapping
结构,key代表的是账户地址,value代表这个账户的余额。
基于上面的思考,我们完善deposit函数的实现。
// 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来做权限管控,还有一个重点是,我们支持分批提现。这里需要注意对边界条件的判断。
// 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);
}
}
我们很自然的想到使用数组这种数据结构来实现这个功能。具体的思路是,将新转入钱的账户添加到数组中,进行排序,我们的排名仅仅记录前几名,为了节省空间,我们会将后面名次的地址移除。
这里的排序,我们使用冒泡排序算法
// 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();
}
}
}
为了能够便捷的获取排序后的数据,我们可以包装一个函数去返回数据,在我们转钱和提现的时候,需要抛出一些事件供外部分析使用。
// 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
修饰符限制某些函数只能由合约所有者调用:
modifier onlyOwner () {
require(msg.sender == owner, "Only owner can call this function");
_;
}
构造函数(Constructor):
constructor() {
owner = msg.sender;
}
接收以太币(Receiving Ether):
receive()
函数允许合约接收以太币:
receive() external payable {
deposit();
}
函数修饰符(Function Modifiers):
onlyOwner
修饰符用于限制函数访问权限。存款函数(Deposit Function):
deposit()
函数允许用户向合约存入以太币,同时更新用户余额和存款排行榜:
function 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()
函数允许合约所有者从合约中提取指定数量的以太币:
function 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()
用于更新存款最多用户的排行榜:
function updateTopDepositors(address depositor) internal {
// 排名更新逻辑
}
视图函数(View Functions):
getBalance()
返回指定地址的余额:
function getBalance(address addr) public view returns(uint256) {
return balances[addr];
}
getTopDepositors()
返回存款排行榜:
function getTopDepositors() public view returns (address[] memory) {
return topDepositors;
}
地址类型和单位转换(Address Type and Unit Conversion):
payable
关键字将地址转换为可以接收以太币的地址。wei
是以太坊的最小单位,用于处理货币值。数组操作(Array Operations):
topDepositors
来管理和更新存款排行榜。通过这些知识点,这个合约实现了一个基本的银行功能,相信看到这里你已经完全掌握了。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!