Sitelet https://github.com/MagicalBridge/Blog/issues/83
Skip to content

手把手教你实现Bank智能合约 #83

Description

@MagicalBridge

相关背景

在前面的系列文章中,我们已经学习了solidity的一些基础概念和知识,单纯的知识点学习起来比较枯燥,这一节我们会通过一个简单的示例来将这些知识整合起来。

我们期望实现一个名为Bank的合约。这个合约有以下要求:

需要实现的核心功能

  • 1、合约部署之后,每个人都可以向合约里面转钱,好比,我们现实生活中向银行存钱一样。
  • 2、但是我们的Bank合约比较特殊的地方在于,只有合约的所有者才能从合约中取钱,这个是不是有点反直觉,这个部分我们在后面会针对性的讲解。
  • 3、我们还会统计出来存钱最多的几个用户。这个是后续奖励活动的凭证。

实现步骤

搭建合约的基础框架

题目中要求只有合约所有者才能从合约中取钱,这个很明显是个权限管理相关的需求,我们可以使用modified这个知识点实现。

一般的,合约的所有者是合约的部署者(特殊指定权限除外),这个权限在部署合约的时候就已经确定了,我们可以在合约的构造函数中来实现这个逻辑,来看代码的基础框架:

// 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函数的实现

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编程的核心知识点:

  1. 状态变量:

    • address public owner;
    • mapping(address => uint256) private balances;
    • address[] public topDepositors;
  2. 事件(Events):

    • event Withdrawal(address indexed to, uint256 amount);
    • event Deposit(address indexed from, uint256 amount);
  3. 访问控制(Access Control):

    • 使用onlyOwner修饰符限制某些函数只能由合约所有者调用:
    modifier onlyOwner () {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }
  4. 构造函数(Constructor):

    • 在合约部署时执行一次,用于初始化合约状态:
    constructor() {
        owner = msg.sender;
    }
  5. 接收以太币(Receiving Ether):

    • 使用receive()函数允许合约接收以太币:
    receive() external payable {
        deposit();
    }
  6. 函数修饰符(Function Modifiers):

    • onlyOwner修饰符用于限制函数访问权限。
  7. 存款函数(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);
    }
  8. 取款函数(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);
    }
  9. 内部函数(Internal Functions):

    • updateTopDepositors()用于更新存款最多用户的排行榜:
    function updateTopDepositors(address depositor) internal {
        // 排名更新逻辑
    }
  10. 视图函数(View Functions):

    • getBalance()返回指定地址的余额:
    function getBalance(address addr) public view returns(uint256) {
        return balances[addr];
    }
    • getTopDepositors()返回存款排行榜:
    function getTopDepositors() public view returns (address[] memory) {
        return topDepositors;
    }
  11. 地址类型和单位转换(Address Type and Unit Conversion):

    • 使用payable关键字将地址转换为可以接收以太币的地址。
    • wei是以太坊的最小单位,用于处理货币值。
  12. 数组操作(Array Operations):

    • 操作数组topDepositors来管理和更新存款排行榜。

通过这些知识点,这个合约实现了一个基本的银行功能,相信看到这里你已经完全掌握了。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions