Skip to content

Commit

Permalink
Merge the develop branch into the master branch, v1.1.0-rc1
Browse files Browse the repository at this point in the history
This merge contains the following set of changes:
* Add burnFrom function into the token implementation (#41)
* Add useful minting middlewares for CDP and other purposes (#48)
akolotov authored Mar 29, 2023
2 parents b69d3d5 + 97ee6aa commit 0072688
Showing 14 changed files with 1,492 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/interfaces/IBurnableERC20.sol
Original file line number Diff line number Diff line change
@@ -4,4 +4,5 @@ pragma solidity 0.8.15;

interface IBurnableERC20 {
function burn(uint256 amount) external;
function burnFrom(address user, uint256 amount) external;
}
58 changes: 58 additions & 0 deletions src/minters/BalancedMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "./BaseMinter.sol";

/**
* @title BalancedMinter
* BOB minting/burning middleware with simple usage quotas.
*/
contract BalancedMinter is BaseMinter {
int128 public mintQuota; // remaining minting quota
int128 public burnQuota; // remaining burning quota

event UpdateQuotas(int128 mintQuota, int128 burnQuota);

constructor(address _token, uint128 _mintQuota, uint128 _burnQuota) BaseMinter(_token) {
mintQuota = int128(_mintQuota);
burnQuota = int128(_burnQuota);
}

/**
* @dev Adjusts mint/burn quotas for the given address.
* Callable only by the contract owner.
* @param _dMint delta for minting quota.
* @param _dBurn delta for burning quota.
*/
function adjustQuotas(int128 _dMint, int128 _dBurn) external onlyOwner {
(int128 newMintQuota, int128 newBurnQuota) = (mintQuota + _dMint, burnQuota + _dBurn);
(mintQuota, burnQuota) = (newMintQuota, newBurnQuota);

emit UpdateQuotas(newBurnQuota, newBurnQuota);
}

/**
* @dev Internal function for adjusting quotas on tokens mint.
* @param _amount amount of minted tokens.
*/
function _beforeMint(uint256 _amount) internal override {
int128 amount = int128(uint128(_amount));
unchecked {
require(mintQuota >= amount, "BalancedMinter: exceeds minting quota");
(mintQuota, burnQuota) = (mintQuota - amount, burnQuota + amount);
}
}

/**
* @dev Internal function for adjusting quotas on tokens burn.
* @param _amount amount of burnt tokens.
*/
function _beforeBurn(uint256 _amount) internal override {
int128 amount = int128(uint128(_amount));
unchecked {
require(burnQuota >= amount, "BalancedMinter: exceeds burning quota");
(mintQuota, burnQuota) = (mintQuota + amount, burnQuota - amount);
}
}
}
104 changes: 104 additions & 0 deletions src/minters/BaseMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "../utils/Ownable.sol";
import "../interfaces/IMintableERC20.sol";
import "../interfaces/IBurnableERC20.sol";
import "../interfaces/IERC677Receiver.sol";

/**
* @title BaseMinter
* Base contract for BOB minting/burning middleware
*/
abstract contract BaseMinter is IMintableERC20, IBurnableERC20, IERC677Receiver, Ownable {
address public immutable token;

mapping(address => bool) public isMinter;

event Mint(address minter, address to, uint256 amount);
event Burn(address burner, address from, uint256 amount);

constructor(address _token) {
token = _token;
}

/**
* @dev Updates mint/burn permissions for the given address.
* Callable only by the contract owner.
* @param _account managed minter account address.
* @param _enabled true, if enabling minting/burning, false otherwise.
*/
function setMinter(address _account, bool _enabled) external onlyOwner {
isMinter[_account] = _enabled;
}

/**
* @dev Mints the specified amount of tokens.
* This contract should have minting permissions assigned to it in the token contract.
* Callable only by one of the minter addresses.
* @param _to address of the tokens receiver.
* @param _amount amount of tokens to mint.
*/
function mint(address _to, uint256 _amount) external override {
require(isMinter[msg.sender], "BaseMinter: not a minter");

_beforeMint(_amount);
IMintableERC20(token).mint(_to, _amount);

emit Mint(msg.sender, _to, _amount);
}

/**
* @dev Burns tokens sent to the address.
* Callable only by one of the minter addresses.
* Caller should send specified amount of tokens to this contract, prior to calling burn.
* @param _amount amount of tokens to burn.
*/
function burn(uint256 _amount) external override {
require(isMinter[msg.sender], "BaseMinter: not a burner");

_beforeBurn(_amount);
IBurnableERC20(token).burn(_amount);

emit Burn(msg.sender, msg.sender, _amount);
}

/**
* @dev Burns pre-approved tokens from the other address.
* Callable only by one of the burner addresses.
* Minters should handle with extra care cases when first argument is not msg.sender.
* @param _from account to burn tokens from.
* @param _amount amount of tokens to burn. Should be less than or equal to account balance.
*/
function burnFrom(address _from, uint256 _amount) external override {
require(isMinter[msg.sender], "BaseMinter: not a burner");

_beforeBurn(_amount);
IBurnableERC20(token).burnFrom(_from, _amount);

emit Burn(msg.sender, _from, _amount);
}

/**
* @dev ERC677 callback for burning tokens atomically.
* @param _from tokens sender, should correspond to one of the minting addresses.
* @param _amount amount of sent/burnt tokens.
* @param _data extra data, not used.
*/
function onTokenTransfer(address _from, uint256 _amount, bytes calldata _data) external override returns (bool) {
require(msg.sender == address(token), "BaseMinter: not a token");
require(isMinter[_from], "BaseMinter: not a burner");

_beforeBurn(_amount);
IBurnableERC20(token).burn(_amount);

emit Burn(_from, _from, _amount);

return true;
}

function _beforeMint(uint256 _amount) internal virtual;

function _beforeBurn(uint256 _amount) internal virtual;
}
145 changes: 145 additions & 0 deletions src/minters/DebtMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "./BaseMinter.sol";

/**
* @title DebtMinter
* BOB minting/burning middleware for generic debt-minting use-cases.
*/
contract DebtMinter is BaseMinter {
struct Parameters {
uint104 maxDebtLimit; // max possible debt limit
uint104 minDebtLimit; // min possible debt limit
uint48 raiseDelay; // min delay between debt limit raises
uint96 raise; // debt limit raising step
address treasury; // receiver of repaid debt surplus
}

struct State {
uint104 debtLimit; // current debt limit, minDebtLimit <= debtLimit <= maxDebtLimit
uint104 debt; // current debt value
uint48 lastRaise; // timestamp of last debt limit raise
}

Parameters internal parameters;
State internal state;

event UpdateDebt(uint104 debt, uint104 debtLimit);

constructor(
address _token,
uint104 _maxDebtLimit,
uint104 _minDebtLimit,
uint48 _raiseDelay,
uint96 _raise,
address _treasury
)
BaseMinter(_token)
{
require(_minDebtLimit + uint104(_raise) <= _maxDebtLimit, "DebtMinter: invalid raise");
parameters = Parameters(_maxDebtLimit, _minDebtLimit, _raiseDelay, _raise, _treasury);
state = State(_minDebtLimit, 0, uint48(block.timestamp));
}

function getState() external view returns (State memory) {
return state;
}

function getParameters() external view returns (Parameters memory) {
return parameters;
}

/**
* @dev Tells remaining mint amount subject to immediate debt limit.
* @return available mint amount.
*/
function maxDebtIncrease() external view returns (uint256) {
Parameters memory p = parameters;
State memory s = state;
_updateDebtLimit(p, s);
return s.debtLimit - s.debt;
}

/**
* @dev Updates limit configuration.
* Callable only by the contract owner.
* @param _params new parameters to apply.
*/
function updateParameters(Parameters calldata _params) external onlyOwner {
require(_params.minDebtLimit + uint104(_params.raise) <= _params.maxDebtLimit, "DebtMinter: invalid raise");
parameters = _params;

State memory s = state;
_updateDebtLimit(_params, s);
state = s;

emit UpdateDebt(s.debt, s.debtLimit);
}

/**
* @dev Internal function for adjusting debt limits on tokens mint.
* @param _amount amount of minted tokens.
*/
function _beforeMint(uint256 _amount) internal override {
Parameters memory p = parameters;
State memory s = state;

_updateDebtLimit(p, s);
uint256 newDebt = uint256(s.debt) + _amount;
require(newDebt <= s.debtLimit, "DebtMinter: exceeds debt limit");
s.debt = uint104(newDebt);

state = s;

emit UpdateDebt(s.debt, s.debtLimit);
}

/**
* @dev Internal function for adjusting debt limits on tokens burn.
* @param _amount amount of burnt tokens.
*/
function _beforeBurn(uint256 _amount) internal override {
Parameters memory p = parameters;
State memory s = state;

unchecked {
if (_amount <= s.debt) {
s.debt -= uint104(_amount);
} else {
IMintableERC20(token).mint(p.treasury, _amount - s.debt);
s.debt = 0;
}
}
_updateDebtLimit(p, s);
state = s;

emit UpdateDebt(s.debt, s.debtLimit);
}

/**
* @dev Internal function for recalculating immediate debt limit.
*/
function _updateDebtLimit(Parameters memory p, State memory s) internal view {
if (s.debt >= p.maxDebtLimit) {
s.debtLimit = s.debt;
} else {
uint104 newDebtLimit = s.debt + p.raise;
if (newDebtLimit < p.minDebtLimit) {
s.debtLimit = p.minDebtLimit;
return;
}

if (newDebtLimit > p.maxDebtLimit) {
newDebtLimit = p.maxDebtLimit;
}
if (newDebtLimit <= s.debtLimit) {
s.debtLimit = newDebtLimit;
} else if (s.lastRaise + p.raiseDelay < block.timestamp) {
s.debtLimit = newDebtLimit;
s.lastRaise = uint48(block.timestamp);
}
}
}
}
35 changes: 35 additions & 0 deletions src/minters/FaucetMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "../interfaces/IMintableERC20.sol";

/**
* @title FaucetMinter
* Simplest contract for faucet minting.
*/
contract FaucetMinter is IMintableERC20 {
address public immutable token;
uint256 public immutable limit;

event Mint(address minter, address to, uint256 amount);

constructor(address _token, uint256 _limit) {
token = _token;
limit = _limit;
}

/**
* @dev Mints the specified amount of tokens.
* This contract should have minting permissions assigned to it in the token contract.
* @param _to address of the tokens receiver.
* @param _amount amount of tokens to mint.
*/
function mint(address _to, uint256 _amount) external override {
require(_amount <= limit, "FaucetMinter: too much");

IMintableERC20(token).mint(_to, _amount);

emit Mint(msg.sender, _to, _amount);
}
}
122 changes: 122 additions & 0 deletions src/minters/FlashMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../interfaces/IMintableERC20.sol";
import "../interfaces/IBurnableERC20.sol";
import "../utils/Ownable.sol";

/**
* @title FlashMinter
* BOB flash minter middleware.
*/
contract FlashMinter is IERC3156FlashLender, ReentrancyGuard, Ownable {
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");

address public immutable token;

uint96 public limit; // max limit for flash mint amount
address public treasury; // receiver of flash mint fees

uint64 public fee; // fee percentage * 1 ether
uint96 public maxFee; // max fee in absolute values

event FlashMint(address indexed _receiver, uint256 _amount, uint256 _fee);

constructor(address _token, uint96 _limit, address _treasury, uint64 _fee, uint96 _maxFee) {
require(_treasury != address(0) || _fee == 0, "FlashMinter: invalid fee config");
token = _token;
limit = _limit;
treasury = _treasury;
_setFees(_fee, _maxFee);
}

function updateConfig(uint96 _limit, address _treasury, uint64 _fee, uint96 _maxFee) external onlyOwner {
require(_treasury != address(0) || _fee == 0, "FlashMinter: invalid fee config");
limit = _limit;
treasury = _treasury;
_setFees(_fee, _maxFee);
}

function _setFees(uint64 _fee, uint96 _maxFee) internal {
require(_fee <= 0.01 ether, "FlashMinter: fee too large");
(fee, maxFee) = (_fee, _maxFee);
}

/**
* @dev Returns the maximum amount of tokens available for loan.
* @param _token The address of the token that is requested.
* @return The amount of token that can be loaned.
*/
function maxFlashLoan(address _token) public view virtual override returns (uint256) {
return token == _token ? limit : 0;
}

/**
* @dev Returns the fee applied when doing flash loans.
* @param _token The token to be flash loaned.
* @param _amount The amount of tokens to be loaned.
* @return The fees applied to the corresponding flash loan.
*/
function flashFee(address _token, uint256 _amount) public view virtual override returns (uint256) {
require(token == _token, "FlashMinter: wrong token");
return _flashFee(_amount);
}

/**
* @dev Returns the fee applied when doing flash loans.
* @param _amount The amount of tokens to be loaned.
* @return The fees applied to the corresponding flash loan.
*/
function _flashFee(uint256 _amount) internal view virtual returns (uint256) {
(uint64 _fee, uint96 _maxFee) = (fee, maxFee);
uint256 flashFee = _amount * _fee / 1 ether;
return flashFee > _maxFee ? _maxFee : flashFee;
}

/**
* @dev Performs a flash loan. New tokens are minted and sent to the
* `receiver`, who is required to implement the IERC3156FlashBorrower
* interface. By the end of the flash loan, the receiver is expected to own
* amount + fee tokens and have them approved back to the token contract itself so
* they can be burned.
* @param _receiver The receiver of the flash loan. Should implement the
* IERC3156FlashBorrower.onFlashLoan interface.
* @param _token The token to be flash loaned. Only configured token is
* supported.
* @param _amount The amount of tokens to be loaned.
* @param _data An arbitrary data that is passed to the receiver.
* @return `true` if the flash loan was successful.
*/
function flashLoan(
IERC3156FlashBorrower _receiver,
address _token,
uint256 _amount,
bytes calldata _data
)
public
override
nonReentrant
returns (bool)
{
require(token == _token, "FlashMinter: wrong token");
require(_amount <= limit, "FlashMinter: amount exceeds maxFlashLoan");
uint256 fee = _flashFee(_amount);
IMintableERC20(_token).mint(address(_receiver), _amount);
require(
_receiver.onFlashLoan(msg.sender, _token, _amount, fee, _data) == _RETURN_VALUE,
"FlashMinter: invalid return value"
);
IBurnableERC20(_token).burnFrom(address(_receiver), _amount);
if (fee > 0) {
IERC20(_token).transferFrom(address(_receiver), treasury, fee);
}

emit FlashMint(address(_receiver), _amount, fee);

return true;
}
}
126 changes: 126 additions & 0 deletions src/minters/SurplusMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../utils/Ownable.sol";
import "../interfaces/IMintableERC20.sol";
import "../interfaces/IBurnableERC20.sol";
import "../interfaces/IERC677Receiver.sol";

/**
* @title SurplusMinter
* Managing realized and unrealized BOB surplus from debt-minting use-cases.
*/
contract SurplusMinter is IERC677Receiver, Ownable {
address public immutable token;

mapping(address => bool) public isMinter;

uint256 public surplus; // unrealized surplus

event WithdrawSurplus(address indexed to, uint256 realized, uint256 unrealized);
event AddSurplus(address indexed from, uint256 unrealized);

constructor(address _token) {
token = _token;
}

/**
* @dev Updates surplus mint permissions for the given address.
* Callable only by the contract owner.
* @param _account managed minter account address.
* @param _enabled true, if enabling surplus minting, false otherwise.
*/
function setMinter(address _account, bool _enabled) external onlyOwner {
isMinter[_account] = _enabled;
}

/**
* @dev Records potential unrealized surplus.
* Callable only by the pre-approved surplus minter.
* Once unrealized surplus is realized, it should be transferred to this contract via transferAndCall.
* @param _surplus unrealized surplus to add.
*/
function add(uint256 _surplus) external {
require(isMinter[msg.sender], "SurplusMinter: not a minter");

surplus += _surplus;

emit AddSurplus(msg.sender, _surplus);
}

/**
* @dev ERC677 callback. Converts previously recorded unrealized surplus into the realized one.
* If converted amount exceeds unrealized surplus, remainder is burnt to account for unrealized interest withdrawn in advance.
* Callable by anyone.
* @param _from tokens sender.
* @param _amount amount of tokens corresponding to realized interest.
* @param _data optional extra data, encoded uint256 amount of unrealized surplus to convert. Defaults to _amount.
*/
function onTokenTransfer(address _from, uint256 _amount, bytes calldata _data) external override returns (bool) {
require(msg.sender == token, "SurplusMinter: invalid caller");

uint256 unrealized = _amount;
if (_data.length == 32) {
unrealized = abi.decode(_data, (uint256));
require(unrealized <= _amount, "SurplusMinter: invalid value");
}

uint256 currentSurplus = surplus;
if (currentSurplus >= unrealized) {
unchecked {
surplus = currentSurplus - unrealized;
}
} else {
IBurnableERC20(token).burn(unrealized - currentSurplus);
unrealized = currentSurplus;
surplus = 0;
}
emit WithdrawSurplus(address(this), 0, unrealized);

return true;
}

/**
* @dev Burns potential unrealized surplus.
* Callable only by the contract owner.
* Intended to be used for cancelling mistakenly accounted surplus.
* @param _surplus unrealized surplus to cancel.
*/
function burn(uint256 _surplus) external onlyOwner {
require(_surplus <= surplus, "SurplusMinter: exceeds surplus");
unchecked {
surplus -= _surplus;
}
emit WithdrawSurplus(address(0), 0, _surplus);
}

/**
* @dev Withdraws surplus.
* Callable only by the contract owner.
* Withdrawing realized surplus is prioritised, unrealized surplus is minted only
* if realized surplus is not enough to cover the requested amount.
* @param _surplus surplus amount to withdraw/mint.
*/
function withdraw(address _to, uint256 _surplus) external onlyOwner {
uint256 realized = IERC20(token).balanceOf(address(this));

if (_surplus > realized) {
uint256 unrealized = _surplus - realized;
require(unrealized <= surplus, "SurplusMinter: exceeds surplus");
unchecked {
surplus -= unrealized;
}

IERC20(token).transfer(_to, realized);
IMintableERC20(token).mint(_to, unrealized);

emit WithdrawSurplus(_to, realized, unrealized);
} else {
IERC20(token).transfer(_to, _surplus);

emit WithdrawSurplus(_to, _surplus, 0);
}
}
}
15 changes: 14 additions & 1 deletion src/token/ERC20MintBurn.sol
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
pragma solidity 0.8.15;

import "../utils/Ownable.sol";
import "../interfaces/IMintableERC20.sol";
import "./BaseERC20.sol";
import "../interfaces/IMintableERC20.sol";
import "../interfaces/IBurnableERC20.sol";
@@ -58,4 +57,18 @@ abstract contract ERC20MintBurn is IMintableERC20, IBurnableERC20, Ownable, Base

_burn(msg.sender, _value);
}

/**
* @dev Burns pre-approved tokens from the other address.
* Callable only by one of the burner addresses.
* @param _from account to burn tokens from.
* @param _value amount of tokens to burn. Should be less than or equal to account balance.
*/
function burnFrom(address _from, uint256 _value) external virtual {
require(isBurner(msg.sender), "ERC20MintBurn: not a burner");

_spendAllowance(_from, msg.sender, _value);

_burn(_from, _value);
}
}
24 changes: 24 additions & 0 deletions test/BobToken.t.sol
Original file line number Diff line number Diff line change
@@ -94,6 +94,30 @@ contract BobTokenTest is Test, EIP2470Test {
assertEq(bob.balanceOf(user2), 0 ether);
}

function testBurnFrom() public {
vm.prank(user1);
bob.mint(user1, 1 ether);

vm.expectRevert("ERC20MintBurn: not a burner");
bob.burnFrom(user1, 1 ether);

vm.prank(user2);
vm.expectRevert("ERC20: insufficient allowance");
bob.burnFrom(user1, 1 ether);

vm.prank(user1);
bob.approve(user2, 10 ether);

vm.prank(user2);
vm.expectEmit(true, true, false, true);
emit Transfer(user1, address(0), 1 ether);
bob.burnFrom(user1, 1 ether);

assertEq(bob.totalSupply(), 0);
assertEq(bob.balanceOf(user1), 0);
assertEq(bob.allowance(user1, user2), 9 ether);
}

function testMinterChange() public {
vm.expectRevert("Ownable: caller is not the owner");
bob.updateMinter(user3, true, true);
210 changes: 210 additions & 0 deletions test/minters/BalancedMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "forge-std/Test.sol";
import "../shared/Env.t.sol";
import "../../src/proxy/EIP1967Proxy.sol";
import "../../src/BobToken.sol";
import "../../src/minters/BalancedMinter.sol";

contract BalancedMinterTest is Test {
BobToken bob;
BalancedMinter minter;

function setUp() public {
EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
BobToken bobImpl = new BobToken(address(bobProxy));
bobProxy.upgradeTo(address(bobImpl));
bob = BobToken(address(bobProxy));

minter = new BalancedMinter(address(bob), 200 ether, 100 ether);

bob.updateMinter(address(minter), true, true);
bob.updateMinter(address(this), true, true);
}

function testMintPermissions() public {
vm.expectRevert("BaseMinter: not a minter");
minter.mint(user3, 1 ether);
vm.expectRevert("BaseMinter: not a burner");
minter.burn(1 ether);

vm.startPrank(deployer);
vm.expectRevert("Ownable: caller is not the owner");
minter.setMinter(deployer, true);
vm.expectRevert("Ownable: caller is not the owner");
minter.adjustQuotas(200 ether, 100 ether);
vm.stopPrank();

minter.setMinter(deployer, true);
minter.adjustQuotas(200 ether, 100 ether);

vm.expectRevert("BaseMinter: not a minter");
minter.mint(user1, 1 ether);

vm.prank(deployer);
minter.mint(user1, 1 ether);
}

function testQuotas() public {
minter.setMinter(address(this), true);

assertEq(minter.mintQuota(), 200 ether);
assertEq(minter.burnQuota(), 100 ether);

minter.mint(user1, 10 ether);

assertEq(minter.mintQuota(), 190 ether);
assertEq(minter.burnQuota(), 110 ether);

vm.prank(user1);
bob.transfer(address(minter), 5 ether);
minter.burn(5 ether);

assertEq(minter.mintQuota(), 195 ether);
assertEq(minter.burnQuota(), 105 ether);
}

function testExceedingQuotas() public {
bob.mint(address(this), 200 ether);
minter.setMinter(address(this), true);

vm.expectRevert("BalancedMinter: exceeds minting quota");
minter.mint(address(this), 300 ether);
minter.mint(address(this), 200 ether);

bob.transfer(address(minter), 200 ether);
minter.burn(200 ether);

assertEq(minter.mintQuota(), 200 ether);
assertEq(minter.burnQuota(), 100 ether);

bob.transfer(address(minter), 200 ether);
vm.expectRevert("BalancedMinter: exceeds burning quota");
minter.burn(200 ether);
minter.burn(100 ether);
}

function testBurnWithTransferAndCall() public {
bob.mint(address(this), 200 ether);
bob.mint(user1, 200 ether);
minter.setMinter(address(this), true);

vm.prank(user1);
vm.expectRevert("BaseMinter: not a burner");
bob.transferAndCall(address(minter), 10 ether, "");
vm.expectRevert("BalancedMinter: exceeds burning quota");
bob.transferAndCall(address(minter), 110 ether, "");
bob.transferAndCall(address(minter), 10 ether, "");

assertEq(minter.mintQuota(), 210 ether);
assertEq(minter.burnQuota(), 90 ether);
}

function testBurnFrom() public {
bob.mint(address(this), 200 ether);
bob.mint(user1, 200 ether);
minter.setMinter(address(this), true);

vm.expectRevert("ERC20: insufficient allowance");
minter.burnFrom(user1, 10 ether);

vm.prank(user1);
bob.approve(address(minter), 110 ether);

vm.expectRevert("BalancedMinter: exceeds burning quota");
minter.burnFrom(user1, 110 ether);
minter.burnFrom(user1, 10 ether);

assertEq(minter.mintQuota(), 210 ether);
assertEq(minter.burnQuota(), 90 ether);
}

function testAdjustQuotas() public {
assertEq(minter.mintQuota(), 200 ether);
assertEq(minter.burnQuota(), 100 ether);

minter.adjustQuotas(10 ether, -20 ether);

assertEq(minter.mintQuota(), 210 ether);
assertEq(minter.burnQuota(), 80 ether);

minter.adjustQuotas(-20 ether, 10 ether);

assertEq(minter.mintQuota(), 190 ether);
assertEq(minter.burnQuota(), 90 ether);

minter.adjustQuotas(-200 ether, -200 ether);

assertEq(minter.mintQuota(), -10 ether);
assertEq(minter.burnQuota(), -110 ether);

minter.adjustQuotas(200 ether, 100 ether);

assertEq(minter.mintQuota(), 190 ether);
assertEq(minter.burnQuota(), -10 ether);
}

function _setupDualMinter() internal returns (BalancedMinter) {
BalancedMinter minter2 = new BalancedMinter(address(bob), 100 ether, 200 ether);
minter.setMinter(address(this), true);
minter2.setMinter(address(this), true);
bob.updateMinter(address(minter2), true, true);
bob.mint(address(minter), 1000 ether);
bob.mint(address(minter2), 1000 ether);
return minter2;
}

function testMultiChain() public {
BalancedMinter minter2 = _setupDualMinter();

minter.burn(60 ether); // 200/100 -> 260/40
minter2.mint(user1, 60 ether); // 100/200 -> 40/260

vm.expectRevert("BalancedMinter: exceeds burning quota");
minter.burn(60 ether);

minter2.adjustQuotas(50 ether, -50 ether); // 40/260 -> 90/210
minter.adjustQuotas(-50 ether, 50 ether); // 260/40 -> 210/90

minter.burn(60 ether); // 210/90 -> 270/30
minter2.mint(user1, 60 ether); // 90/210 -> 30/270

minter2.burn(250 ether); // 30/270 -> 280/20
minter2.adjustQuotas(50 ether, -50 ether); // 280/20 -> 330/-30
minter.mint(user1, 250 ether); // 270/30 -> 20/280
minter.adjustQuotas(-50 ether, 50 ether); // -30/330

minter.burn(100 ether); // -30/330 -> 70/230
minter2.mint(user1, 100 ether); // 330/-30 -> 230/70

assertEq(minter.mintQuota(), 70 ether);
assertEq(minter.burnQuota(), 230 ether);
assertEq(minter2.mintQuota(), 230 ether);
assertEq(minter2.burnQuota(), 70 ether);
}

function testStuckFailedMint() public {
BalancedMinter minter2 = _setupDualMinter();

// burn is executed before first gov limits adjustment is made
minter2.burn(160 ether); // 100/200 -> 260/40
// this leads to a negative burn quota
minter2.adjustQuotas(50 ether, -50 ether); // 260/40 -> 310/-10
// second gov limits adjustment is executed before mint
minter.adjustQuotas(-50 ether, 50 ether); // 200/100 -> 150/150
// mint fails, as the quota was already adjusted
vm.expectRevert("BalancedMinter: exceeds minting quota");
minter.mint(user1, 160 ether);

// resolve failed mint manually
minter.adjustQuotas(-160 ether, 160 ether); // 150/150 -> -10/310
bob.mint(user1, 160 ether);

assertEq(minter.mintQuota(), -10 ether);
assertEq(minter.burnQuota(), 310 ether);
assertEq(minter2.mintQuota(), 310 ether);
assertEq(minter2.burnQuota(), -10 ether);
}
}
337 changes: 337 additions & 0 deletions test/minters/DebtMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "forge-std/Test.sol";
import "../shared/Env.t.sol";
import "../../src/proxy/EIP1967Proxy.sol";
import "../../src/BobToken.sol";
import "../../src/minters/DebtMinter.sol";

contract DebtMinterTest is Test {
BobToken bob;
DebtMinter minter;

event UpdateDebt(uint104 debt, uint104 debtLimit);

function setUp() public {
EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
BobToken bobImpl = new BobToken(address(bobProxy));
bobProxy.upgradeTo(address(bobImpl));
bob = BobToken(address(bobProxy));

minter = new DebtMinter(address(bob), 800 ether, 400 ether, 12 hours, 200 ether, user1);

bob.updateMinter(address(minter), true, true);
bob.updateMinter(address(this), true, true);

minter.setMinter(address(this), true);

vm.warp(block.timestamp + 1 days);

vm.prank(user1);
bob.approve(address(minter), 10000 ether);
vm.prank(user2);
bob.approve(address(minter), 10000 ether);
}

function testGetters() public {
minter.mint(user1, 150 ether);

assertEq(minter.getState().debt, 150 ether);
assertEq(minter.getState().debtLimit, 400 ether);
assertEq(minter.getState().lastRaise, 1);

assertEq(minter.getParameters().maxDebtLimit, 800 ether);
assertEq(minter.getParameters().minDebtLimit, 400 ether);
assertEq(minter.getParameters().raiseDelay, 12 hours);
assertEq(minter.getParameters().raise, 200 ether);
assertEq(minter.getParameters().treasury, user1);
}

function testMintBurnBalanceChange() public {
minter.mint(user1, 150 ether);
assertEq(bob.balanceOf(user1), 150 ether);

minter.burnFrom(user1, 50 ether);
assertEq(bob.balanceOf(user1), 100 ether);

vm.prank(user1);
bob.transfer(address(minter), 50 ether);
minter.burn(50 ether);
assertEq(bob.balanceOf(user1), 50 ether);
assertEq(bob.balanceOf(address(minter)), 0 ether);

vm.prank(user1);
bob.transfer(address(this), 50 ether);
bob.transferAndCall(address(minter), 50 ether, "");
assertEq(bob.balanceOf(user1), 0 ether);
assertEq(bob.balanceOf(address(minter)), 0 ether);
}

function testSimpleDebtLimitIncrease() public {
assertEq(minter.getState().debt, 0);
assertEq(minter.getState().debtLimit, 400 ether);
assertEq(minter.maxDebtIncrease(), 400 ether);

minter.mint(user1, 150 ether);

assertEq(minter.getState().debt, 150 ether);
assertEq(minter.getState().debtLimit, 400 ether);
assertEq(minter.maxDebtIncrease(), 250 ether);

minter.mint(user1, 150 ether);

assertEq(minter.getState().debt, 300 ether);
assertEq(minter.getState().debtLimit, 400 ether);
assertEq(minter.maxDebtIncrease(), 200 ether);

minter.mint(user1, 150 ether);

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 500 ether);
assertEq(minter.maxDebtIncrease(), 50 ether);

vm.expectRevert("DebtMinter: exceeds debt limit");
minter.mint(user1, 150 ether);

vm.warp(block.timestamp + 1 days);

minter.mint(user1, 150 ether);
assertEq(minter.getState().debt, 600 ether);
assertEq(minter.getState().debtLimit, 650 ether);
assertEq(minter.maxDebtIncrease(), 50 ether);

vm.expectRevert("DebtMinter: exceeds debt limit");
minter.mint(user1, 150 ether);

vm.warp(block.timestamp + 1 days);

minter.mint(user1, 150 ether);
assertEq(minter.getState().debt, 750 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 50 ether);

vm.warp(block.timestamp + 1 days);

assertEq(minter.getState().debt, 750 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 50 ether);

vm.expectRevert("DebtMinter: exceeds debt limit");
minter.mint(user1, 51 ether);

minter.mint(user1, 30 ether);

assertEq(minter.getState().debt, 780 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 20 ether);

vm.expectRevert("DebtMinter: exceeds debt limit");
minter.mint(user1, 21 ether);

minter.mint(user1, 20 ether);

assertEq(minter.getState().debt, 800 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 0);
}

function testSimpleDebtLimitDecrease() public {
minter.mint(user1, 400 ether);
minter.mint(user1, 200 ether);
vm.expectRevert("DebtMinter: exceeds debt limit");
minter.mint(user1, 200 ether);
vm.warp(block.timestamp + 1 days);
minter.mint(user1, 200 ether);

assertEq(minter.getState().debt, 800 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 0);

minter.burnFrom(user1, 150 ether);

assertEq(minter.getState().debt, 650 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 150 ether);

minter.burnFrom(user1, 150 ether);

assertEq(minter.getState().debt, 500 ether);
assertEq(minter.getState().debtLimit, 700 ether);
assertEq(minter.maxDebtIncrease(), 200 ether);
}

function testParamsIncrease() public {
minter.mint(user1, 350 ether);
minter.mint(user1, 100 ether);
vm.warp(block.timestamp + 1 days);

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 550 ether);
assertEq(minter.maxDebtIncrease(), 200 ether);

minter.updateParameters(DebtMinter.Parameters(1000 ether, 400 ether, 12 hours, 250 ether, user1));

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 700 ether);
assertEq(minter.maxDebtIncrease(), 250 ether);

minter.updateParameters(DebtMinter.Parameters(2000 ether, 800 ether, 12 hours, 300 ether, user1));

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 350 ether);

minter.mint(user1, 290 ether);

assertEq(minter.getState().debt, 740 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 60 ether);

vm.warp(block.timestamp + 1 days);

assertEq(minter.getState().debt, 740 ether);
assertEq(minter.getState().debtLimit, 800 ether);
assertEq(minter.maxDebtIncrease(), 300 ether);

minter.mint(user1, 10 ether);

assertEq(minter.getState().debt, 750 ether);
assertEq(minter.getState().debtLimit, 1040 ether);
assertEq(minter.maxDebtIncrease(), 290 ether);

minter.mint(user1, 250 ether);

assertEq(minter.getState().debt, 1000 ether);
assertEq(minter.getState().debtLimit, 1040 ether);
assertEq(minter.maxDebtIncrease(), 40 ether);
}

function testParamsDecrease() public {
minter.mint(user1, 350 ether);
minter.mint(user1, 100 ether);
vm.warp(block.timestamp + 1 days);

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 550 ether);
assertEq(minter.maxDebtIncrease(), 200 ether);

minter.updateParameters(DebtMinter.Parameters(600 ether, 400 ether, 12 hours, 200 ether, user1));

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 600 ether);
assertEq(minter.maxDebtIncrease(), 150 ether);

minter.updateParameters(DebtMinter.Parameters(500 ether, 400 ether, 12 hours, 100 ether, user1));

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 500 ether);
assertEq(minter.maxDebtIncrease(), 50 ether);

vm.expectRevert("DebtMinter: exceeds debt limit");
minter.mint(user1, 60 ether);
minter.mint(user1, 50 ether);
vm.warp(block.timestamp + 1 days);

assertEq(minter.getState().debt, 500 ether);
assertEq(minter.getState().debtLimit, 500 ether);
assertEq(minter.maxDebtIncrease(), 0 ether);

minter.burnFrom(user1, 50 ether);

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 500 ether);
assertEq(minter.maxDebtIncrease(), 50 ether);

minter.updateParameters(DebtMinter.Parameters(300 ether, 200 ether, 12 hours, 100 ether, user1));

assertEq(minter.getState().debt, 450 ether);
assertEq(minter.getState().debtLimit, 450 ether);
assertEq(minter.maxDebtIncrease(), 0 ether);

minter.burnFrom(user1, 100 ether);

assertEq(minter.getState().debt, 350 ether);
assertEq(minter.getState().debtLimit, 350 ether);
assertEq(minter.maxDebtIncrease(), 0 ether);

minter.burnFrom(user1, 60 ether);

assertEq(minter.getState().debt, 290 ether);
assertEq(minter.getState().debtLimit, 300 ether);
assertEq(minter.maxDebtIncrease(), 10 ether);

minter.burnFrom(user1, 100 ether);

assertEq(minter.getState().debt, 190 ether);
assertEq(minter.getState().debtLimit, 290 ether);
assertEq(minter.maxDebtIncrease(), 100 ether);

minter.burnFrom(user1, 100 ether);

assertEq(minter.getState().debt, 90 ether);
assertEq(minter.getState().debtLimit, 200 ether);
assertEq(minter.maxDebtIncrease(), 110 ether);

minter.updateParameters(DebtMinter.Parameters(300 ether, 100 ether, 12 hours, 100 ether, user1));

assertEq(minter.getState().debt, 90 ether);
assertEq(minter.getState().debtLimit, 190 ether);
assertEq(minter.maxDebtIncrease(), 100 ether);

minter.updateParameters(DebtMinter.Parameters(0 ether, 0 ether, 12 hours, 0 ether, user1));

assertEq(minter.getState().debt, 90 ether);
assertEq(minter.getState().debtLimit, 90 ether);
assertEq(minter.maxDebtIncrease(), 0 ether);

minter.burnFrom(user1, 70 ether);

assertEq(minter.getState().debt, 20 ether);
assertEq(minter.getState().debtLimit, 20 ether);
assertEq(minter.maxDebtIncrease(), 0 ether);

minter.burnFrom(user1, 20 ether);

assertEq(minter.getState().debt, 0 ether);
assertEq(minter.getState().debtLimit, 0 ether);
assertEq(minter.maxDebtIncrease(), 0 ether);
}

function testBurnExcess() public {
minter.mint(user2, 350 ether);
bob.mint(user2, 100 ether);

assertEq(bob.totalSupply(), 450 ether);
assertEq(bob.balanceOf(user2), 450 ether);
assertEq(minter.getState().debt, 350 ether);
assertEq(minter.getState().debtLimit, 400 ether);
assertEq(minter.maxDebtIncrease(), 200 ether);

minter.burnFrom(user2, 410 ether);

assertEq(bob.totalSupply(), 100 ether);
assertEq(bob.balanceOf(user2), 40 ether);
assertEq(bob.balanceOf(user1), 60 ether);
assertEq(minter.getState().debt, 0 ether);
assertEq(minter.getState().debtLimit, 400 ether);
assertEq(minter.maxDebtIncrease(), 400 ether);
}

function testDebtEmit() public {
vm.expectEmit(true, false, false, true);
emit UpdateDebt(350 ether, 400 ether);
minter.mint(user1, 350 ether);

vm.expectEmit(true, false, false, true);
emit UpdateDebt(450 ether, 550 ether);
minter.mint(user1, 100 ether);

vm.warp(block.timestamp + 1 days);

vm.expectEmit(true, false, false, true);
emit UpdateDebt(420 ether, 620 ether);
minter.burnFrom(user1, 30 ether);
}
}
98 changes: 98 additions & 0 deletions test/minters/FlashMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "forge-std/Test.sol";
import "../shared/Env.t.sol";
import "../../src/proxy/EIP1967Proxy.sol";
import "../../src/BobToken.sol";
import "../../src/minters/FlashMinter.sol";
import "../mocks/ERC3156FlashBorrowerMock.sol";

contract FlashMinterTest is Test {
BobToken bob;
FlashMinter minter;

function setUp() public {
EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
BobToken bobImpl = new BobToken(address(bobProxy));
bobProxy.upgradeTo(address(bobImpl));
bob = BobToken(address(bobProxy));

minter = new FlashMinter(address(bob), 1000 ether, user1, 0.001 ether, 0.1 ether);

bob.updateMinter(address(minter), true, true);
bob.updateMinter(address(this), true, true);

vm.warp(block.timestamp + 1 days);
}

function testGetters() public {
assertEq(minter.flashFee(address(bob), 50 ether), 0.05 ether);
assertEq(minter.flashFee(address(bob), 100 ether), 0.1 ether);
assertEq(minter.flashFee(address(bob), 500 ether), 0.1 ether);
assertEq(minter.maxFlashLoan(address(bob)), 1000 ether);
}

function testFlashLoan() public {
ERC3156FlashBorrowerMock mock = new ERC3156FlashBorrowerMock(address(bob), false, false);
vm.expectRevert(bytes("E1"));
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 100 ether, "");

mock = new ERC3156FlashBorrowerMock(address(minter), false, false);
vm.expectRevert("FlashMinter: invalid return value");
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 100 ether, "");

mock = new ERC3156FlashBorrowerMock(address(minter), true, false);
vm.expectRevert("ERC20: insufficient allowance");
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 100 ether, "");

mock = new ERC3156FlashBorrowerMock(address(minter), true, true);
vm.expectRevert("ERC20: amount exceeds balance");
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 100 ether, "");

mock = new ERC3156FlashBorrowerMock(address(minter), true, true);
vm.expectRevert("FlashMinter: amount exceeds maxFlashLoan");
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 2000 ether, "");

mock = new ERC3156FlashBorrowerMock(address(minter), true, true);
bob.mint(address(mock), minter.flashFee(address(bob), 100 ether));
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 100 ether, "");

assertEq(bob.totalSupply(), 0.1 ether);
assertEq(bob.balanceOf(address(minter)), 0);
assertEq(bob.balanceOf(address(mock)), 0);
assertEq(bob.balanceOf(address(user1)), 0.1 ether);
}

function testUpdateConfig() external {
ERC3156FlashBorrowerMock mock = new ERC3156FlashBorrowerMock(address(minter), true, true);
bob.mint(address(mock), minter.flashFee(address(bob), 100 ether));
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 100 ether, "");

assertEq(bob.totalSupply(), 0.1 ether);
assertEq(bob.balanceOf(address(minter)), 0);
assertEq(bob.balanceOf(address(mock)), 0);
assertEq(bob.balanceOf(address(user1)), 0.1 ether);

vm.prank(user1);
vm.expectRevert("Ownable: caller is not the owner");
minter.updateConfig(10000 ether, user2, 0.01 ether, 1 ether);
minter.updateConfig(10000 ether, user2, 0.01 ether, 1 ether);

assertEq(minter.maxFlashLoan(address(bob)), 10000 ether);
assertEq(minter.flashFee(address(bob), 10 ether), 0.1 ether);
assertEq(minter.flashFee(address(bob), 100 ether), 1 ether);
assertEq(minter.flashFee(address(bob), 1000 ether), 1 ether);

mock = new ERC3156FlashBorrowerMock(address(minter), true, true);
bob.mint(address(mock), minter.flashFee(address(bob), 2000 ether));
minter.flashLoan(IERC3156FlashBorrower(mock), address(bob), 2000 ether, "");

assertEq(bob.totalSupply(), 1.1 ether);
assertEq(bob.balanceOf(address(minter)), 0);
assertEq(bob.balanceOf(address(mock)), 0);
assertEq(bob.balanceOf(address(user1)), 0.1 ether);
assertEq(bob.balanceOf(address(user2)), 1 ether);
}
}
166 changes: 166 additions & 0 deletions test/minters/SurplusMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "forge-std/Test.sol";
import "../shared/Env.t.sol";
import "../../src/proxy/EIP1967Proxy.sol";
import "../../src/BobToken.sol";
import "../../src/minters/SurplusMinter.sol";

contract SurplusMinterTest is Test {
BobToken bob;
SurplusMinter minter;

event WithdrawSurplus(address indexed to, uint256 realized, uint256 unrealized);
event AddSurplus(address indexed from, uint256 surplus);

function setUp() public {
EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
BobToken bobImpl = new BobToken(address(bobProxy));
bobProxy.upgradeTo(address(bobImpl));
bob = BobToken(address(bobProxy));

minter = new SurplusMinter(address(bob));

bob.updateMinter(address(minter), true, true);
bob.updateMinter(address(this), true, true);

minter.setMinter(address(this), true);
}

function testSurplusAdd() public {
vm.prank(user1);
vm.expectRevert("SurplusMinter: not a minter");
minter.add(100 ether);

vm.expectEmit(true, false, false, true);
emit AddSurplus(address(this), 100 ether);
minter.add(100 ether);
assertEq(minter.surplus(), 100 ether);
}

function testRealizedSurplusAdd() public {
bob.mint(address(this), 1000 ether);

minter.add(100 ether);

assertEq(minter.surplus(), 100 ether);
assertEq(bob.balanceOf(address(minter)), 0 ether);

bob.transferAndCall(address(minter), 10 ether, "");

assertEq(minter.surplus(), 90 ether);
assertEq(bob.balanceOf(address(minter)), 10 ether);

vm.expectRevert("SurplusMinter: invalid value");
bob.transferAndCall(address(minter), 20 ether, abi.encode(30 ether));

bob.transferAndCall(address(minter), 20 ether, abi.encode(20 ether));
assertEq(minter.surplus(), 70 ether);
assertEq(bob.balanceOf(address(minter)), 30 ether);

bob.transferAndCall(address(minter), 20 ether, abi.encode(10 ether));
assertEq(minter.surplus(), 60 ether);
assertEq(bob.balanceOf(address(minter)), 50 ether);

// convert 60 BOB unrealized surplus, burn 10 BOB
bob.transferAndCall(address(minter), 70 ether, "");
assertEq(minter.surplus(), 0 ether);
assertEq(bob.balanceOf(address(minter)), 110 ether);

minter.add(10 ether);
assertEq(minter.surplus(), 10 ether);
assertEq(bob.balanceOf(address(minter)), 110 ether);

// convert 10 BOB of unrealized surplus, burn 15 BOB, record remaining 5 BOB of realized surplus
bob.transferAndCall(address(minter), 30 ether, abi.encode(25 ether));
assertEq(minter.surplus(), 0 ether);
assertEq(bob.balanceOf(address(minter)), 125 ether);
}

function testSurplusBurn() public {
minter.add(100 ether);

vm.prank(user1);
vm.expectRevert("Ownable: caller is not the owner");
minter.burn(100 ether);

vm.expectRevert("SurplusMinter: exceeds surplus");
minter.burn(2000 ether);

vm.expectEmit(true, false, false, true);
emit WithdrawSurplus(address(0), 0, 60 ether);
minter.burn(60 ether);
assertEq(minter.surplus(), 40 ether);
minter.burn(40 ether);
assertEq(minter.surplus(), 0 ether);
}

function testSurplusWithdraw() public {
minter.add(100 ether);
bob.mint(address(minter), 50 ether);

vm.prank(user1);
vm.expectRevert("Ownable: caller is not the owner");
minter.withdraw(user1, 100 ether);

vm.expectRevert("SurplusMinter: exceeds surplus");
minter.withdraw(user1, 200 ether);

vm.expectEmit(true, false, false, true);
emit WithdrawSurplus(user1, 30 ether, 0 ether);
minter.withdraw(user1, 30 ether);
assertEq(minter.surplus(), 100 ether);
assertEq(bob.balanceOf(user1), 30 ether);
assertEq(bob.balanceOf(address(minter)), 20 ether);

vm.expectEmit(true, false, false, true);
emit WithdrawSurplus(user1, 20 ether, 10 ether);
minter.withdraw(user1, 30 ether);
assertEq(minter.surplus(), 90 ether);
assertEq(bob.balanceOf(user1), 60 ether);
assertEq(bob.balanceOf(address(minter)), 0 ether);

vm.expectRevert("SurplusMinter: exceeds surplus");
minter.withdraw(user1, 100 ether);

vm.expectEmit(true, false, false, true);
emit WithdrawSurplus(user1, 0 ether, 90 ether);
minter.withdraw(user1, 90 ether);
assertEq(minter.surplus(), 0 ether);
assertEq(bob.balanceOf(user1), 150 ether);
assertEq(bob.balanceOf(address(minter)), 0 ether);

vm.expectRevert("SurplusMinter: exceeds surplus");
minter.withdraw(user1, 1 ether);
}

function testWithdrawConvertUnrealized() public {
// 0 withdrawn, 50 realized, 50 unrealized
minter.add(50 ether);
bob.mint(address(minter), 50 ether);

// 80 withdrawn, 0 realized, 20 unrealized
minter.withdraw(user1, 80 ether);
assertEq(minter.surplus(), 20 ether);
assertEq(bob.balanceOf(user1), 80 ether);
assertEq(bob.balanceOf(address(minter)), 0 ether);

bob.mint(address(this), 1000 ether);

// convert 20 unrealized into realized surplus, burn 30 previously withdrawn unrealized surplus
bob.transferAndCall(address(minter), 50 ether, "");
// 80 withdrawn, 20 realized, 0 unrealized
assertEq(minter.surplus(), 0 ether);
assertEq(bob.balanceOf(user1), 80 ether);
assertEq(bob.balanceOf(address(minter)), 20 ether);

// add 50 realized surplus
bob.transferAndCall(address(minter), 50 ether, abi.encode(0));
// 80 withdrawn, 70 realized, 0 unrealized
assertEq(minter.surplus(), 0 ether);
assertEq(bob.balanceOf(user1), 80 ether);
assertEq(bob.balanceOf(address(minter)), 70 ether);
}
}
52 changes: 52 additions & 0 deletions test/mocks/ERC3156FlashBorrowerMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "@openzeppelin/contracts/interfaces/IERC3156.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";

contract ERC3156FlashBorrowerMock is IERC3156FlashBorrower {
bytes32 internal constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");

address immutable _expectedCaller;
bool immutable _enableApprove;
bool immutable _enableReturn;

event BalanceOf(address token, address account, uint256 value);
event TotalSupply(address token, uint256 value);

constructor(address caller, bool enableReturn, bool enableApprove) {
_expectedCaller = caller;
_enableApprove = enableApprove;
_enableReturn = enableReturn;
}

function onFlashLoan(
address, /*initiator*/
address token,
uint256 amount,
uint256 fee,
bytes calldata data
)
public
override
returns (bytes32)
{
require(msg.sender == _expectedCaller, "E1");

emit BalanceOf(token, address(this), IERC20(token).balanceOf(address(this)));
emit TotalSupply(token, IERC20(token).totalSupply());

if (data.length > 0) {
// WARNING: This code is for testing purposes only! Do not use.
Address.functionCall(token, data);
}

if (_enableApprove) {
IERC20(token).approve(msg.sender, amount + fee);
}

return _enableReturn ? _RETURN_VALUE : bytes32(0);
}
}

0 comments on commit 0072688

Please sign in to comment.