相关背景
在手把手教你实现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
函数。
整体的调用关系可以如下图所示:
IBank接口实现
IBank
这个接口实现很简单,我们只需要将Bank
合约的相关功能抽象出来即可:
solidity
// 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
合约做一些简单的改造,添加一些重写的关键字,这部分并不复杂,我们直接贴代码:
solidity
// 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这个修饰器。
为了可以转移自己的所有权,我们还需要提供一个函数,将权限向指定的地址移交。
solidity
// 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
接口;
solidity
// Define IBigBank interface
interface IBigBank {
function withdraw(uint256 amount) external;
}
具体的,Ownable
合约可以这样写:
solidity
// 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合约
Ownable合约
相关测试流程
1、部署顺序,先部署BigBank
合约, 验证取款的金额必须大于0.001 ether;
2、验证其他三个账号向其中转账,金额都不同,最后只有部署合约的账号可以取款;
3、部署Ownable合约,得到一个地址,将BigBank合约的所有权转移给它,此时,BigBank 就无法调用提款方法了。因为合约所有权已经转移;
4、切换为部署Ownable合约的账号,调用取款方法,可以成功取款不会报错。
总结
1. 接口定义(IBank)
- 使用
interface
关键字定义接口 - 定义事件(
event
)和函数签名 - 接口中的函数不需要实现,只需声明
2. 合约继承
OriginalBank
实现IBank
接口BigBank
继承自OriginalBank
- 使用
override
关键字重写父合约的函数
3. 状态变量
- 使用
public
、private
关键字控制变量可见性 mapping
用于存储键值对(地址到余额的映射)- 使用数组存储 top depositors
4. 构造函数
- 使用
constructor
关键字定义 - 初始化合约状态(如设置 owner)
5. 修饰器(Modifiers)
- 使用
modifier
关键字定义 - 用于在函数执行前检查条件(如
onlyOwner
)
6. 函数可见性
public
、external
、internal
、private
关键字view
关键字用于不修改状态的函数payable
关键字用于可接收以太币的函数
7. 事件(Events)
- 使用
event
关键字定义 - 用于记录重要的状态变化(如存款、提款)
8. 错误处理
- 使用
require
语句进行条件检查和错误处理
9. 接收以太币
receive()
函数用于接收以太币
10. 常量
- 使用
constant
关键字定义不可变的值
11. 地址和转账
- 使用
address
类型表示以太坊地址 payable(address).transfer()
用于转账
12. 安全考虑
- 检查合约余额before 提款
- 实现所有权转移功能
13. 接口实现和调用
IBigBank
接口定义了BigBank
合约的部分功能Ownable
合约通过接口调用BigBank
的withdraw
函数
14. 合约间交互
- 使用接口定义其他合约的函数
- 通过接口调用其他合约的函数
15. 可升级性考虑
- 通过设置外部合约地址(
setBigBankAddress
)实现一定程度的可升级性
这个合约展示了 Solidity 编程中的多个高级概念,包括接口、继承、访问控制、事件处理和合约间交互等。它提供了一个基本的银行系统实现,同时考虑了安全性和可扩展性。