- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
141 lines
5.5 KiB
Solidity
141 lines
5.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
|
|
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
|
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
|
|
/**
|
|
* @title SimpleERC3156FlashVault
|
|
* @notice Minimal ERC-3156 flash lender: whitelist per token, flat bps fee, same-block repayment invariant.
|
|
* @dev Intended for Chain 138 / internal atomic workflows when no external flash source exists.
|
|
*/
|
|
contract SimpleERC3156FlashVault is IERC3156FlashLender, Ownable, ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
|
|
|
/// @notice Maximum owner-configurable fee (10%). Prevents griefing at 100% fee.
|
|
uint256 public constant MAX_FEE_BPS = 1000;
|
|
|
|
/// @notice Fee on borrowed amount, basis points (10_000 = 100%).
|
|
uint256 public feeBps;
|
|
|
|
mapping(address token => bool) public supportedToken;
|
|
|
|
/// @notice When true, only `approvedBorrower[receiver]` may be used as the flash callback contract.
|
|
bool public borrowerAllowlistEnabled;
|
|
|
|
mapping(address borrower => bool) public approvedBorrower;
|
|
|
|
/// @notice Cumulative fees retained by the vault per token (for ops / accounting).
|
|
mapping(address token => uint256) public totalFeesCollected;
|
|
|
|
/// @notice initiator = flashLoan caller; receiver = IERC3156FlashBorrower callback target.
|
|
event FlashLoan(address indexed initiator, IERC3156FlashBorrower indexed receiver, address indexed token, uint256 amount, uint256 fee);
|
|
event FeeBpsUpdated(uint256 feeBps);
|
|
event TokenSupportUpdated(address indexed token, bool supported);
|
|
event TokensRescued(address indexed token, address indexed to, uint256 amount);
|
|
event BorrowerAllowlistEnabledUpdated(bool enabled);
|
|
event BorrowerApprovalUpdated(address indexed borrower, bool approved);
|
|
|
|
error UnsupportedToken();
|
|
error ZeroAmount();
|
|
error FeeTooHigh();
|
|
error InvalidCallback();
|
|
error RepaymentFailed();
|
|
error ZeroRescue();
|
|
error ZeroRecipient();
|
|
error BorrowerNotApproved();
|
|
|
|
constructor(address initialOwner, uint256 initialFeeBps) Ownable(initialOwner) {
|
|
_setFeeBps(initialFeeBps);
|
|
}
|
|
|
|
function setFeeBps(uint256 newFeeBps) external onlyOwner {
|
|
_setFeeBps(newFeeBps);
|
|
}
|
|
|
|
function setTokenSupported(address token, bool supported) external onlyOwner {
|
|
supportedToken[token] = supported;
|
|
emit TokenSupportUpdated(token, supported);
|
|
}
|
|
|
|
function setBorrowerAllowlistEnabled(bool enabled) external onlyOwner {
|
|
borrowerAllowlistEnabled = enabled;
|
|
emit BorrowerAllowlistEnabledUpdated(enabled);
|
|
}
|
|
|
|
function setBorrowerApproved(address borrower, bool approved) external onlyOwner {
|
|
approvedBorrower[borrower] = approved;
|
|
emit BorrowerApprovalUpdated(borrower, approved);
|
|
}
|
|
|
|
/// @notice Operator alias for `flashFee` (same revert rules).
|
|
function previewFlashFee(address token, uint256 amount) external view returns (uint256) {
|
|
return flashFee(token, amount);
|
|
}
|
|
|
|
/// @notice Owner-only recovery (mis-seeded asset, deprecated token, migration). Does not bypass flash invariants on active loans in the same tx.
|
|
function rescueTokens(address token, uint256 amount, address to) external onlyOwner {
|
|
if (amount == 0) revert ZeroRescue();
|
|
if (to == address(0)) revert ZeroRecipient();
|
|
IERC20(token).safeTransfer(to, amount);
|
|
emit TokensRescued(token, to, amount);
|
|
}
|
|
|
|
function maxFlashLoan(address token) external view override returns (uint256) {
|
|
if (!supportedToken[token]) {
|
|
return 0;
|
|
}
|
|
return IERC20(token).balanceOf(address(this));
|
|
}
|
|
|
|
function flashFee(address token, uint256 amount) public view override returns (uint256) {
|
|
if (!supportedToken[token]) {
|
|
revert UnsupportedToken();
|
|
}
|
|
return (amount * feeBps) / 10_000;
|
|
}
|
|
|
|
function flashLoan(
|
|
IERC3156FlashBorrower receiver,
|
|
address token,
|
|
uint256 amount,
|
|
bytes calldata data
|
|
) external override nonReentrant returns (bool) {
|
|
if (!supportedToken[token]) revert UnsupportedToken();
|
|
if (amount == 0) revert ZeroAmount();
|
|
if (borrowerAllowlistEnabled && !approvedBorrower[address(receiver)]) {
|
|
revert BorrowerNotApproved();
|
|
}
|
|
|
|
uint256 fee = flashFee(token, amount);
|
|
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
|
|
if (balanceBefore < amount) revert RepaymentFailed();
|
|
|
|
IERC20(token).safeTransfer(address(receiver), amount);
|
|
|
|
bytes32 retval = receiver.onFlashLoan(msg.sender, token, amount, fee, data);
|
|
if (retval != _RETURN_VALUE) revert InvalidCallback();
|
|
|
|
if (IERC20(token).balanceOf(address(this)) < balanceBefore + fee) {
|
|
revert RepaymentFailed();
|
|
}
|
|
|
|
totalFeesCollected[token] += fee;
|
|
|
|
emit FlashLoan(msg.sender, receiver, token, amount, fee);
|
|
return true;
|
|
}
|
|
|
|
function _setFeeBps(uint256 newFeeBps) internal {
|
|
if (newFeeBps > MAX_FEE_BPS) revert FeeTooHigh();
|
|
feeBps = newFeeBps;
|
|
emit FeeBpsUpdated(newFeeBps);
|
|
}
|
|
}
|