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

  • Louis
  • 更新于 2024-07-05 16:02
  • 阅读 983

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

相关背景

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

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

需要实现的核心功能

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

实现步骤

搭建合约的基础框架

题目中要求只有合约所有者才能从合约中取钱,这个很明显是个权限管理相关的需求,我们可以使用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函数的实现

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来管理和更新存款排行榜。

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

点赞 3
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis