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

相关背景

在前面的系列文章中,我们已经学习了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编程的核心知识点:

  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修饰符限制某些函数只能由合约所有者调用:
    solidity 复制代码
    modifier onlyOwner () {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }
  4. 构造函数(Constructor):

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

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

    • onlyOwner修饰符用于限制函数访问权限。
  7. 存款函数(Deposit Function):

    • deposit()函数允许用户向合约存入以太币,同时更新用户余额和存款排行榜:
    solidity 复制代码
    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()函数允许合约所有者从合约中提取指定数量的以太币:
    solidity 复制代码
    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()用于更新存款最多用户的排行榜:
    solidity 复制代码
    function updateTopDepositors(address depositor) internal {
        // 排名更新逻辑
    }
  10. 视图函数(View Functions):

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

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

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

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

相关推荐
mirrornan4 小时前
什么是Web3D?有何优势?有哪些应用场景?
3d·web3·webgl·3d模型
Roun35 小时前
Web3和区块链如何促进数据透明与隐私保护的平衡
web3·区块链·隐私保护
dingzd9513 小时前
Web3的核心技术:区块链如何确保信息安全与共享
web3·去中心化·区块链
清 晨13 小时前
Web3与智能合约:区块链技术下的数字信任体系
web3·区块链·智能合约
脆皮泡泡13 小时前
Ultiverse 和web3新玩法?AI和GameFi的结合是怎样
人工智能·web3
CertiK14 小时前
Web3.0安全开发实践:Clarity最佳实践总结
web3·区块链·clarity
web3探路者1 天前
深入探索Solana链上的Meme生态:创新、潜力与挑战#区块链开发#dapp开发
web3·区块链·团队开发·dapp开发·区块链技术·链游开发·交易所开发
程序猿阿伟3 天前
《C++编写以太坊智能合约:安全至上的编程之道》
c++·安全·智能合约
MavenTalk3 天前
solana链上智能合约开发案例一则
rust·区块链·智能合约·dapp·solana
Sui_Network5 天前
World Wide Walrus:下一代数据存储协议
大数据·人工智能·web3·去中心化·区块链