-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge the develop branch into the master branch, v1.1.0-rc1
Showing
14 changed files
with
1,492 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |