Michael.W基于Foundry精读Openzeppelin第45期——ERC20FlashMint.sol
- 0. 版本
- 0.1 ERC20FlashMint.sol
- 1. 目标合约
- 2. 代码精读
- 2.1 maxFlashLoan(address token)
- 2.2 flashFee(address token, uint256 amount)
- 2.3 flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
0. 版本
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
0.1 ERC20FlashMint.sol
Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/token/ERC20/extensions/ERC20FlashMint.sol
ERC20FlashMint库是ERC20的拓展,也是关于闪电贷ERC3156的实现。ERC20FlashMint库在ERC20的基础上实现了IERC3156FlashLender接口,在token层面上支持了闪电贷功能。但是该库默认没有闪电贷手续费,开发者可以通过重写flashFee()
方法来自定义手续费计算逻辑。
EIP3156详情参见:https://eips.ethereum.org/EIPS/eip-3156
1. 目标合约
继承ERC20FlashMint合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/token/ERC20/extensions/MockERC20FlashMint.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20FlashMint.sol";
contract MockERC20FlashMint is ERC20FlashMint {
bool private _customizedFlashFeeAndReceiver;
constructor(
string memory name,
string memory symbol,
address richer,
uint totalSupply
)
ERC20(name, symbol)
{
_mint(richer, totalSupply);
}
function customizedFlashFeeAndReceiver() external {
_customizedFlashFeeAndReceiver = true;
}
// customized flash fee 10% amount
function _flashFee(address token, uint amount) internal view override returns (uint) {
return _customizedFlashFeeAndReceiver ?
amount / 10 : ERC20FlashMint._flashFee(token, amount);
}
// customized fee receiver address(1024)
function _flashFeeReceiver() internal view override returns (address) {
return _customizedFlashFeeAndReceiver ?
address(1024) : ERC20FlashMint._flashFeeReceiver();
}
}
全部foundry测试合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/token/ERC20/extensions/ERC20FlashMint/ERC20FlashMint.t.sol
测试使用的物料合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/token/ERC20/extensions/ERC20FlashMint/ERC3156FlashBorrower.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
contract ERC3156FlashBorrower is IERC3156FlashBorrower {
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
bool private _enableApprove;
bool private _enableValidReturnValue;
event ParamsIn(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes data
);
event Monitor(
address owner,
uint balance,
uint totalSupply
);
// implementation of IERC3156FlashBorrower.onFlashLoan()
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32){
IERC20 erc20Token = IERC20(token);
// show the params input
emit ParamsIn(
initiator,
token,
amount,
fee,
data
);
// show the token status during IERC3156FlashBorrower.onFlashLoan()
emit Monitor(
address(this),
erc20Token.balanceOf(address(this)),
erc20Token.totalSupply()
);
if (data.length != 0) {
(bool ok,) = token.call(data);
require(ok, "fail to call");
}
if (_enableApprove) {
erc20Token.approve(token, amount + fee);
}
return _enableValidReturnValue ? _RETURN_VALUE : bytes32(0);
}
function flipApprove() external {
_enableApprove = !_enableApprove;
}
function flipValidReturnValue() external {
_enableValidReturnValue = !_enableValidReturnValue;
}
}
2. 代码精读
2.1 maxFlashLoan(address token)
IERC3156FlashLender中的标准方法实现,返回输入token最大可借贷的数量。
// 如果IERC3156FlashBorrower.onFlashLoan()方法返回该常量值表示该方法执行有效
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
// 参数:
// - token: 要借出token的地址
function maxFlashLoan(address token) public view virtual override returns (uint256) {
// 如果传入的token地址为本ERC20地址,返回最大可借贷数量为type(uint256).max-当前本ERC20的总供应量。否则返回0
return token == address(this) ? type(uint256).max - ERC20.totalSupply() : 0;
}
foundry代码验证:
contract ERC20FlashMintTest is Test {
MockERC20FlashMint private _testing = new MockERC20FlashMint("test name", "test symbol", address(this), 10000);
function test_MaxFlashLoan() external {
uint totalSupply = _testing.totalSupply();
assertEq(totalSupply, 10000);
// query for self
assertEq(_testing.maxFlashLoan(address(_testing)), type(uint).max - totalSupply);
// query for other
assertEq(_testing.maxFlashLoan(address(0)), 0);
}
}
2.2 flashFee(address token, uint256 amount)
IERC3156FlashLender中的标准方法实现,返回借出数量为amount、地址为token的ERC20需要支付的手续费。该方法内部调用internal方法_flashFee()
,可以在子合约中重写_flashFee()
方法来实现需要的手续费计算逻辑。
// 参数:
// - token: 闪电贷的token地址
// - amount: 闪电贷的token数量
function flashFee(address token, uint256 amount) public view virtual override returns (uint256) {
// 要求token为本ERC20合约地址
require(token == address(this), "ERC20FlashMint: wrong token");
// 调用internal方法_flashFee(),返回对应手续费数量
return _flashFee(token, amount);
}
// internal方法,返回具体的借出数量为amount、地址为token的ERC20需要支付的手续费。在子合约中重写此方法,可自定义闪电贷手续费计算逻辑。同时也可以使得本ERC20具备通缩属性
// - token: 闪电贷的token地址
// - amount: 闪电贷的token数量
function _flashFee(address token, uint256 amount) internal view virtual returns (uint256) {
// 在不添加字节码的情况下,使得未使用传入变量在编译时不再报warning
token;
amount;
// 直接返回0。
// 注:在子合约中重写此方法,可自定义闪电贷手续费计算逻辑
return 0;
}
foundry代码验证:
contract ERC20FlashMintTest is Test {
MockERC20FlashMint private _testing = new MockERC20FlashMint("test name", "test symbol", address(this), 10000);
function test_FlashFee() external {
// case 1: default flash fee (0)
uint amountToLoan = 100;
assertEq(_testing.flashFee(address(_testing), amountToLoan), 0);
// revert with wrong token address
vm.expectRevert("ERC20FlashMint: wrong token");
_testing.flashFee(address(0), amountToLoan);
// case 2: customized flash fee (10% amountToLoan)
_testing.customizedFlashFeeAndReceiver();
assertEq(_testing.flashFee(address(_testing), amountToLoan), amountToLoan / 10);
// revert with wrong token address
vm.expectRevert("ERC20FlashMint: wrong token");
_testing.flashFee(address(0), amountToLoan);
}
}
2.3 flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
执行闪电贷。如果成功执行,返回true。
本ERC20合约会mint出新的token给receiver地址。闪电贷结束时需要满足如下条件才算成功:
- receiver名下持有至少amount(借贷数量)+ fee(对应手续费)的token;
- receiver已授权给本ERC20合约至少如上数量的授权额度,以便本合约可以burn掉receiver名下的token及转移fee。
// 参数:
// - receiver: 闪电贷借出token的接受者地址,要求该地址实现了接口IERC3156FlashBorrower
// - amount: 闪电贷借出token数量
// - data: 传给receiver,用于执行receiver.onFlashLoan()方法的参数
// 注:此方法未做重入检查,因为即使重入发生也不会带来风险——因为闪电贷mint出的token在后面都会被burn掉,一旦该平衡被打破整个函数会revert
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) public virtual override returns (bool) {
// 要求借贷token数量 <= 最大允许借贷数量,否则revert
require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan");
// 计算对应闪电贷手续费
uint256 fee = flashFee(token, amount);
// mint给receiver amount数量的token
_mint(address(receiver), amount);
// 执行receiver.onFlashLoan()方法,来触发receiver收到贷款后的执行逻辑。要求返回值为常量_RETURN_VALUE,否则revert
require(
receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE,
"ERC20FlashMint: invalid return value"
);
// 获取闪电贷手续费接受者地址
address flashFeeReceiver = _flashFeeReceiver();
// 消费receiver给本合约的授权额度,即amount(借贷token数量)+ fee(闪电贷手续费)
_spendAllowance(address(receiver), address(this), amount + fee);
if (fee == 0 || flashFeeReceiver == address(0)) {
// 如果手续费为0或者闪电贷手续费接受者地址为0地址,直接销毁该receiver名下amount+fee数量的token
_burn(address(receiver), amount + fee);
} else {
// 如果手续费不为0且闪电贷手续费接受者地址不为0地址
// 销毁receiver名下数量为amount的token
_burn(address(receiver), amount);
// 从receiver名下转移fee数量的手续费到手续费接受者地址
_transfer(address(receiver), flashFeeReceiver, fee);
}
// 返回true
return true;
}
// 返回闪电贷手续费的接受地址。如果该方法返回0地址,表示手续费被天然burn掉。如果需要换成其他地址,可以在子合约中重写该函数
function _flashFeeReceiver() internal view virtual returns (address) {
// 直接返回0地址
return address(0);
}
foundry代码验证:
contract ERC20FlashMintTest is Test {
address private constant CUSTOMIZED_FLASH_FEE_RECEIVER = address(1024);
MockERC20FlashMint private _testing = new MockERC20FlashMint("test name", "test symbol", address(this), 10000);
ERC3156FlashBorrower private flashBorrower = new ERC3156FlashBorrower();
event Transfer(address indexed from, address indexed to, uint256 value);
event ParamsIn(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes data
);
event Monitor(
address owner,
uint balance,
uint totalSupply
);
function test_FlashLoan_DefaultFlashFeeAndReceiver() external {
assertEq(_testing.totalSupply(), 10000);
// case 1: pass with flash borrower's approval and valid return value
uint amountToLoan = 20000;
uint defaultFee = 0;
flashBorrower.flipApprove();
flashBorrower.flipValidReturnValue();
// mint amountToLoan to flashBorrower
vm.expectEmit(address(_testing));
emit Transfer(address(0), address(flashBorrower), amountToLoan);
// check params input in IERC3156FlashBorrower.onFlashLoan()
vm.expectEmit(address(flashBorrower));
emit ParamsIn(address(this), address(_testing), amountToLoan, defaultFee, '');
// check the state during IERC3156FlashBorrower.onFlashLoan()
vm.expectEmit(address(flashBorrower));
emit Monitor(address(flashBorrower), amountToLoan, amountToLoan + 10000);
// burn amountToLoan + fee(0) from flashBorrower
vm.expectEmit(address(_testing));
emit Transfer(address(flashBorrower), address(0), amountToLoan + defaultFee);
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
// total supply not changed
assertEq(_testing.totalSupply(), 10000);
// case 2: revert if amountToLoan > maxFlashLoan
uint amountExceedsMaxFlashLoan = _testing.maxFlashLoan(address(_testing)) + 1;
vm.expectRevert("ERC20FlashMint: amount exceeds maxFlashLoan");
_testing.flashLoan(flashBorrower, address(_testing), amountExceedsMaxFlashLoan, '');
// case 3: revert if receiver.onFlashLoan() with invalid return value
flashBorrower.flipValidReturnValue();
vm.expectRevert("ERC20FlashMint: invalid return value");
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
flashBorrower.flipValidReturnValue();
// case 4: revert without approval in IERC3156FlashBorrower.onFlashLoan()
flashBorrower.flipApprove();
vm.expectRevert("ERC20: insufficient allowance");
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
flashBorrower.flipApprove();
// case 5: revert with different amounts can be minted and burned in onFlashLoan()
// transfer 1 to address(1) in IERC3156FlashBorrower.onFlashLoan()
bytes memory data = abi.encodeCall(_testing.transfer, (address(1), 1));
vm.expectRevert("ERC20: burn amount exceeds balance");
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, data);
}
function test_FlashLoan_CustomizedFlashFeeAndReceiver() external {
_testing.customizedFlashFeeAndReceiver();
assertEq(_testing.balanceOf(address(this)), 10000);
assertEq(_testing.balanceOf(address(flashBorrower)), 0);
assertEq(_testing.balanceOf(CUSTOMIZED_FLASH_FEE_RECEIVER), 0);
// case 1: pass with flash borrower's approval and valid return value
uint amountToLoan = 20000;
uint customizedFlashFee = amountToLoan / 10;
flashBorrower.flipApprove();
flashBorrower.flipValidReturnValue();
// transfer flash fee to flash borrower
_testing.transfer(address(flashBorrower), customizedFlashFee);
// mint amountToLoan to flashBorrower
vm.expectEmit(address(_testing));
emit Transfer(address(0), address(flashBorrower), amountToLoan);
// check params input in IERC3156FlashBorrower.onFlashLoan()
vm.expectEmit(address(flashBorrower));
emit ParamsIn(address(this), address(_testing), amountToLoan, customizedFlashFee, '');
// check the state during IERC3156FlashBorrower.onFlashLoan()
vm.expectEmit(address(flashBorrower));
emit Monitor(address(flashBorrower), amountToLoan + customizedFlashFee, amountToLoan + 10000);
// burn amountToLoan from flashBorrower
vm.expectEmit(address(_testing));
emit Transfer(address(flashBorrower), address(0), amountToLoan);
// transfer customizedFlashFee to customizedFlashFeeReceiver
vm.expectEmit(address(_testing));
emit Transfer(address(flashBorrower), CUSTOMIZED_FLASH_FEE_RECEIVER, customizedFlashFee);
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
// total supply not changed
assertEq(_testing.totalSupply(), 10000);
assertEq(_testing.balanceOf(address(this)), 10000 - customizedFlashFee);
assertEq(_testing.balanceOf(address(flashBorrower)), 0);
assertEq(_testing.balanceOf(CUSTOMIZED_FLASH_FEE_RECEIVER), customizedFlashFee);
// case 2: revert if amountToLoan > maxFlashLoan
uint amountExceedsMaxFlashLoan = _testing.maxFlashLoan(address(_testing)) + 1;
vm.expectRevert("ERC20FlashMint: amount exceeds maxFlashLoan");
_testing.flashLoan(flashBorrower, address(_testing), amountExceedsMaxFlashLoan, '');
// case 3: revert if receiver.onFlashLoan() with invalid return value
flashBorrower.flipValidReturnValue();
vm.expectRevert("ERC20FlashMint: invalid return value");
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
flashBorrower.flipValidReturnValue();
// case 4: revert without approval in IERC3156FlashBorrower.onFlashLoan()
flashBorrower.flipApprove();
vm.expectRevert("ERC20: insufficient allowance");
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
flashBorrower.flipApprove();
// case 5: revert with different amounts can be minted and burned in onFlashLoan()
// transfer 1 to address(1) in IERC3156FlashBorrower.onFlashLoan()
bytes memory data = abi.encodeCall(_testing.transfer, (address(1), 1));
vm.expectRevert("ERC20: burn amount exceeds balance");
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, data);
// case 6: revert with insufficient flash fee
_testing.transfer(address(flashBorrower), customizedFlashFee - 1);
vm.expectRevert("ERC20: transfer amount exceeds balance");
_testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
}
}
ps:
本人热爱图灵,热爱中本聪,热爱V神。
以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。
同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!
如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人