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

  • Louis
  • 更新于 2024-07-06 10:29
  • 阅读 1067

在手把手教你实现Bank智能合约这篇文章中,我们认真拆解了需求,罗列了相关的知识点,已经实现了基于权限控制的存款和取款逻辑。这篇文章,我们会提升难度,不仅仅要实现功能,还要思考怎么合理的去设计一个合约。

相关背景

手把手教你实现Bank智能合约这篇文章中,我们认真拆解了需求,罗列了相关的知识点,已经实现了基于权限控制的存款和取款逻辑。这篇文章,我们会提升难度,不仅仅要实现功能,还要思考怎么合理的去设计一个合约。

新的需求如下:

1、新的BigBank合约要继承Bank合约;

2、BigBank合约支持转移自己的管理员权限到其他的指定账户;

3、需要编写一个Ownable合约,将BigBank合约的管理员权限转移给Ownable合约,转移之后,只有Ownable合约才可以调用BigBank合约的 withdraw()方法。

4、新的管理员调用withdraw()方法,可以成功从BigBank合约中把钱取走。

5、需要实现对存款前三名用户的排序。

合约基础架构搭建

继承的好处

题目的要求中涉及到了继承的知识点,我在Solidity中的继承这篇文章中介绍过,Solidity是一种面向对象的编程语言,它支持合约之间的继承。继承允许一个合约获取另一个合约的所有非私有属性和函数,这样就可以重复使用代码,降低重复工作量。

接口的相关概念

接口可以理解为一种约束,我们可以通过接口定义标准,通过接口定义一组函数签名,为智能合约提供标准化的接口。这对于创建可互操作的合约系统非常有用。

从抽象角度理解,接口允许你抽象出合约的功能,而不需要关心具体实现。这有助于提高代码的可读性和可维护性。

从多态的角度理解,通过接口,允许不同的合约实现相同的接口,但有不同的具体实现。

在类型检查层面,编译器可以使用接口进行类型检查,确保合约正确实现了所有必要的函数,实现过接口的朋友们都理解,如果继承了一个接口却没有实现它的全部函数,编译器就会报错,大大提高了代码的规范和安全性。

回顾了上面两个基础的概念,我们规划下整体合约的设计:

合约整体设计:

1、我们需要在上层抽象出一个接口,这个接口就叫做IBank,它的作用是抽象出Bank合约的能力,包括存款、取款、事件、还有一些可见的状态变量的定义。

2、我们在手把手教你实现Bank智能合约这篇文章中实现的Bank合约是需要继承IBank接口的,只需要稍加改造即可实现(包含对于函数的重写)

3、BigBank 合约是我们要新创建的合约,这个合约需要继承Bank合约,从这里就可以看出继承的好处了,根据题目要求,我们仅仅需要重写deposit方法,添加一些自己的方法即可,一些其他的公用部分都可以通过继承获得。

4、Ownable这个合约我们也需要新创建,BigBank合约可以将管理员权限交给它,用于只能通过Ownable合约去调用withdraw函数。

整体的调用关系可以如下图所示:

Xnip2024-07-06_09-52-26.png

IBank接口实现

IBank这个接口实现很简单,我们只需要将Bank合约的相关功能抽象出来即可:

// Define IBank interface
interface IBank {
    // Event definitions
    event Withdrawal(address indexed to, uint256 amount);
    event Deposit(address indexed from, uint256 amount);

    // Function definitions to be implemented
    // Getter function for public state variable
    function owner() external view returns (address);

    // Deposit function
    function deposit() external payable;

    // Withdraw function
    function withdraw(uint256 amount) external;

    // Get balance for a specific address
    function getBalance(address addr) external view returns (uint256);

    // Get top depositors
    function getTopDepositors() external view returns (address[] memory);
}

这个接口提供了一个标准化的结构。它定义了基本的存款、取款功能,以及一些辅助功能如查询余额和获取top存款人。通过使用这个接口,可以确保任何实现它的合约都会包含这些基本功能,从而提高代码的一致性和可互操作性。

修改Bank合约的实现

接下来,我们需要对Bank合约做一些简单的改造,添加一些重写的关键字,这部分并不复杂,我们直接贴代码:

// OriginalBank contract implementing IBank interface
contract OriginalBank is IBank {
    address public owner;
    mapping(address => uint256) private balances;
    address[] public topDepositors;

    // Constructor to set the contract owner
    constructor() {
        owner = msg.sender;
    }

    // Modifier to restrict function access to owner only
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }

    // Fallback function to handle direct ETH transfers
    receive() external payable {
        deposit();
    }

    // Deposit function to add funds to the contract
    function deposit() public payable virtual override {
        balances[msg.sender] += msg.value;
        updateTopDepositors(msg.sender);
        emit Deposit(msg.sender, msg.value);
    }

    // Withdraw function to transfer funds to the owner
    function withdraw(uint256 amount) public override onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "Contract balance is zero");
        require(amount <= balance, "Insufficient contract balance");
        payable(owner).transfer(amount);
        emit Withdrawal(owner, amount);
    }

    // Internal function to update the list of top depositors
    function updateTopDepositors(address depositor) internal {
        bool exists = false;
        for (uint256 i = 0; i < topDepositors.length; i++) {
            if (topDepositors[i] == depositor) {
                exists = true;
                break;
            }
        }
        if (!exists) {
            topDepositors.push(depositor);
        }

        // Sort depositors based on their balance
        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;
                }
            }
        }

        // Keep only the top 3 depositors
        if (topDepositors.length > 3) {
            topDepositors.pop();
        }
    }

    // Get balance for a specific address
    function getBalance(address addr) public view override returns (uint256) {
        return balances[addr];
    }

    // Get the list of top depositors
    function getTopDepositors()
        public
        view
        override
        returns (address[] memory)
    {
        return topDepositors;
    }
}

为了便于理解,我将Bank合约换了一个名字OriginalBank,这里稍微说明一下,从实现上可以看到,我们在实现相关接口函数定义的时候,需要使用override关键字,这个知识点,我在Solidity中的继承也做了说明。

实现BigBank合约

题目中要求,我们需要对用户的存款金额做个限制,在OriginalBank合约中,我们并没有做相关的功能,所以在BigBank合约中我们需要重写这部分的能力,还是使用modifier这个修饰器。

为了可以转移自己的所有权,我们还需要提供一个函数,将权限向指定的地址移交。

// BigBank contract inheriting from OriginalBank
contract BigBank is OriginalBank {
    // Define minimum deposit amount
    uint256 private constant MIN_DEPOSIT = 1_000_000_000_000_000; // 0.001 ETH in wei

    // Event for ownership transfer
    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );

    // Modifier to check if deposit amount is greater than minimum required
    modifier minDepositRequired() {
        require(msg.value > MIN_DEPOSIT, "Deposit must be greater 0.001 ether");
        _;
    }

    // Override deposit function with minDepositRequired modifier
    function deposit() public payable override minDepositRequired {
        super.deposit();
    }

    // Function to transfer ownership to a new address
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "New owner cannot be the zero address");
        require(newOwner != owner, "New owner cannot be the current owner");
        address oldOwner = owner;
        owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

super关键字

deposit函数中,我们用到了super关键字,在Solidity中用于调用父合约的函数。当一个合约继承另一个合约并重写(override)了某个函数时,使用super可以调用父合约中的原始实现。这在你想扩展父合约功能而不是完全替换它时特别有用。

实现Ownable合约

Ownable合约中会提供一个withdraw函数,这个函数是对BigBank函数中withdraw函数的包装,换句话说,我们需要在内部实现具体的合约地址调用其方法,这里还需要实现一个IBigBank接口;

// Define IBigBank interface
interface IBigBank {
    function withdraw(uint256 amount) external;
}

具体的,Ownable合约可以这样写:

// Ownable contract to manage ownership and interact with BigBank
contract Ownable {
    address public owner; // Owner's address

    IBigBank public bigBank;

    // Constructor to set the initial owner
    constructor() {
        owner = msg.sender;
    }

    // Modifier to restrict function access to owner only
    modifier onlyOwner() {
        require(msg.sender == owner, "Only the owner can call this function.");
        _;
    }

    // Function to set the BigBank contract address
    function setBigBankAddress(address _bigBankAddress) public onlyOwner {
        bigBank = IBigBank(_bigBankAddress);
    }

    // Function to withdraw from BigBank
    function withdraw(uint256 amount) public onlyOwner {
        require(address(bigBank) != address(0), "BigBank address not set");
        bigBank.withdraw(amount);
    }

    // Fallback function to receive ETH
    receive() external payable {}
}

部署相关

BigBank合约

Xnip2024-07-06_10-17-01.png

Ownable合约

Xnip2024-07-06_10-17-09.png

相关测试流程

1、部署顺序,先部署BigBank合约, 验证取款的金额必须大于0.001 ether;

2、验证其他三个账号向其中转账,金额都不同,最后只有部署合约的账号可以取款;

3、部署Ownable合约,得到一个地址,将BigBank合约的所有权转移给它,此时,BigBank 就无法调用提款方法了。因为合约所有权已经转移;

4、切换为部署Ownable合约的账号,调用取款方法,可以成功取款不会报错。

总结

1. 接口定义(IBank)

  • 使用 interface 关键字定义接口
  • 定义事件(event)和函数签名
  • 接口中的函数不需要实现,只需声明

2. 合约继承

  • OriginalBank 实现 IBank 接口
  • BigBank 继承自 OriginalBank
  • 使用 override 关键字重写父合约的函数

3. 状态变量

  • 使用 publicprivate 关键字控制变量可见性
  • mapping 用于存储键值对(地址到余额的映射)
  • 使用数组存储 top depositors

4. 构造函数

  • 使用 constructor 关键字定义
  • 初始化合约状态(如设置 owner)

5. 修饰器(Modifiers)

  • 使用 modifier 关键字定义
  • 用于在函数执行前检查条件(如 onlyOwner

6. 函数可见性

  • publicexternalinternalprivate 关键字
  • view 关键字用于不修改状态的函数
  • payable 关键字用于可接收以太币的函数

7. 事件(Events)

  • 使用 event 关键字定义
  • 用于记录重要的状态变化(如存款、提款)

8. 错误处理

  • 使用 require 语句进行条件检查和错误处理

9. 接收以太币

  • receive() 函数用于接收以太币

10. 常量

  • 使用 constant 关键字定义不可变的值

11. 地址和转账

  • 使用 address 类型表示以太坊地址
  • payable(address).transfer() 用于转账

12. 安全考虑

  • 检查合约余额before 提款
  • 实现所有权转移功能

13. 接口实现和调用

  • IBigBank 接口定义了 BigBank 合约的部分功能
  • Ownable 合约通过接口调用 BigBankwithdraw 函数

14. 合约间交互

  • 使用接口定义其他合约的函数
  • 通过接口调用其他合约的函数

15. 可升级性考虑

  • 通过设置外部合约地址(setBigBankAddress)实现一定程度的可升级性

这个合约展示了 Solidity 编程中的多个高级概念,包括接口、继承、访问控制、事件处理和合约间交互等。它提供了一个基本的银行系统实现,同时考虑了安全性和可扩展性。

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

1 条评论

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