Last active
January 10, 2024 03:26
-
-
Save z0r0z/edfd3558913eaf3922a4874607121542 to your computer and use it in GitHub Desktop.
flash-lendable ETH wrapper with elastic shares
This file contains hidden or 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
// SPDX-License-Identifier: GPL-3.0-or-later | |
pragma solidity >=0.8.4; | |
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. | |
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) | |
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) | |
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. | |
abstract contract ERC20 { | |
/*/////////////////////////////////////////////////////////////// | |
EVENTS | |
//////////////////////////////////////////////////////////////*/ | |
event Transfer(address indexed from, address indexed to, uint256 amount); | |
event Approval(address indexed owner, address indexed spender, uint256 amount); | |
/*/////////////////////////////////////////////////////////////// | |
METADATA STORAGE | |
//////////////////////////////////////////////////////////////*/ | |
string public name; | |
string public symbol; | |
uint8 public immutable decimals; | |
/*/////////////////////////////////////////////////////////////// | |
ERC20 STORAGE | |
//////////////////////////////////////////////////////////////*/ | |
uint256 public totalSupply; | |
mapping(address => uint256) public balanceOf; | |
mapping(address => mapping(address => uint256)) public allowance; | |
/*/////////////////////////////////////////////////////////////// | |
EIP-2612 STORAGE | |
//////////////////////////////////////////////////////////////*/ | |
bytes32 public constant PERMIT_TYPEHASH = | |
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); | |
uint256 internal immutable INITIAL_CHAIN_ID; | |
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; | |
mapping(address => uint256) public nonces; | |
/*/////////////////////////////////////////////////////////////// | |
CONSTRUCTOR | |
//////////////////////////////////////////////////////////////*/ | |
constructor( | |
string memory _name, | |
string memory _symbol, | |
uint8 _decimals | |
) { | |
name = _name; | |
symbol = _symbol; | |
decimals = _decimals; | |
INITIAL_CHAIN_ID = block.chainid; | |
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
ERC20 LOGIC | |
//////////////////////////////////////////////////////////////*/ | |
function approve(address spender, uint256 amount) public virtual returns (bool) { | |
allowance[msg.sender][spender] = amount; | |
emit Approval(msg.sender, spender, amount); | |
return true; | |
} | |
function transfer(address to, uint256 amount) public virtual returns (bool) { | |
balanceOf[msg.sender] -= amount; | |
// Cannot overflow because the sum of all user | |
// balances can't exceed the max uint256 value. | |
unchecked { | |
balanceOf[to] += amount; | |
} | |
emit Transfer(msg.sender, to, amount); | |
return true; | |
} | |
function transferFrom( | |
address from, | |
address to, | |
uint256 amount | |
) public virtual returns (bool) { | |
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. | |
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; | |
balanceOf[from] -= amount; | |
// Cannot overflow because the sum of all user | |
// balances can't exceed the max uint256 value. | |
unchecked { | |
balanceOf[to] += amount; | |
} | |
emit Transfer(from, to, amount); | |
return true; | |
} | |
/*/////////////////////////////////////////////////////////////// | |
EIP-2612 LOGIC | |
//////////////////////////////////////////////////////////////*/ | |
function permit( | |
address owner, | |
address spender, | |
uint256 value, | |
uint256 deadline, | |
uint8 v, | |
bytes32 r, | |
bytes32 s | |
) public virtual { | |
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); | |
// Unchecked because the only math done is incrementing | |
// the owner's nonce which cannot realistically overflow. | |
unchecked { | |
bytes32 digest = keccak256( | |
abi.encodePacked( | |
"\x19\x01", | |
DOMAIN_SEPARATOR(), | |
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) | |
) | |
); | |
address recoveredAddress = ecrecover(digest, v, r, s); | |
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); | |
allowance[recoveredAddress][spender] = value; | |
} | |
emit Approval(owner, spender, value); | |
} | |
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { | |
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); | |
} | |
function computeDomainSeparator() internal view virtual returns (bytes32) { | |
return | |
keccak256( | |
abi.encode( | |
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), | |
keccak256(bytes(name)), | |
keccak256(bytes("1")), | |
block.chainid, | |
address(this) | |
) | |
); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
INTERNAL MINT/BURN LOGIC | |
//////////////////////////////////////////////////////////////*/ | |
function _mint(address to, uint256 amount) internal virtual { | |
totalSupply += amount; | |
// Cannot overflow because the sum of all user | |
// balances can't exceed the max uint256 value. | |
unchecked { | |
balanceOf[to] += amount; | |
} | |
emit Transfer(address(0), to, amount); | |
} | |
function _burn(address from, uint256 amount) internal virtual { | |
balanceOf[from] -= amount; | |
// Cannot underflow because a user's balance | |
// will never be larger than the total supply. | |
unchecked { | |
totalSupply -= amount; | |
} | |
emit Transfer(from, address(0), amount); | |
} | |
} | |
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. | |
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol) | |
/// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol) | |
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. | |
library SafeTransferLib { | |
/*/////////////////////////////////////////////////////////////// | |
ETH OPERATIONS | |
//////////////////////////////////////////////////////////////*/ | |
function safeTransferETH(address to, uint256 amount) internal { | |
bool callStatus; | |
assembly { | |
// Transfer the ETH and store if it succeeded or not. | |
callStatus := call(gas(), to, amount, 0, 0, 0, 0) | |
} | |
require(callStatus, "ETH_TRANSFER_FAILED"); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
ERC20 OPERATIONS | |
//////////////////////////////////////////////////////////////*/ | |
function safeTransferFrom( | |
ERC20 token, | |
address from, | |
address to, | |
uint256 amount | |
) internal { | |
bool callStatus; | |
assembly { | |
// Get a pointer to some free memory. | |
let freeMemoryPointer := mload(0x40) | |
// Write the abi-encoded calldata to memory piece by piece: | |
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) // Begin with the function selector. | |
mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "from" argument. | |
mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. | |
mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. | |
// Call the token and store if it succeeded or not. | |
// We use 100 because the calldata length is 4 + 32 * 3. | |
callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0) | |
} | |
require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FROM_FAILED"); | |
} | |
function safeTransfer( | |
ERC20 token, | |
address to, | |
uint256 amount | |
) internal { | |
bool callStatus; | |
assembly { | |
// Get a pointer to some free memory. | |
let freeMemoryPointer := mload(0x40) | |
// Write the abi-encoded calldata to memory piece by piece: | |
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) // Begin with the function selector. | |
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. | |
mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. | |
// Call the token and store if it succeeded or not. | |
// We use 68 because the calldata length is 4 + 32 * 2. | |
callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) | |
} | |
require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FAILED"); | |
} | |
function safeApprove( | |
ERC20 token, | |
address to, | |
uint256 amount | |
) internal { | |
bool callStatus; | |
assembly { | |
// Get a pointer to some free memory. | |
let freeMemoryPointer := mload(0x40) | |
// Write the abi-encoded calldata to memory piece by piece: | |
mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000) // Begin with the function selector. | |
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. | |
mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. | |
// Call the token and store if it succeeded or not. | |
// We use 68 because the calldata length is 4 + 32 * 2. | |
callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) | |
} | |
require(didLastOptionalReturnCallSucceed(callStatus), "APPROVE_FAILED"); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
INTERNAL HELPER LOGIC | |
//////////////////////////////////////////////////////////////*/ | |
function didLastOptionalReturnCallSucceed(bool callStatus) private pure returns (bool success) { | |
assembly { | |
// Get how many bytes the call returned. | |
let returnDataSize := returndatasize() | |
// If the call reverted: | |
if iszero(callStatus) { | |
// Copy the revert message into memory. | |
returndatacopy(0, 0, returnDataSize) | |
// Revert with the same message. | |
revert(0, returnDataSize) | |
} | |
switch returnDataSize | |
case 32 { | |
// Copy the return data into memory. | |
returndatacopy(0, 0, returnDataSize) | |
// Set success to whether it returned true. | |
success := iszero(iszero(mload(0))) | |
} | |
case 0 { | |
// There was no return data. | |
success := 1 | |
} | |
default { | |
// It returned some malformed input. | |
success := 0 | |
} | |
} | |
} | |
} | |
/** | |
* @dev Interface of the ERC3156 FlashBorrower, as defined in | |
* https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. | |
* | |
* _Available since v4.1._ | |
*/ | |
interface IERC3156FlashBorrower { | |
/** | |
* @dev Receive a flash loan. | |
* @param initiator The initiator of the loan. | |
* @param token The loan currency. | |
* @param amount The amount of tokens lent. | |
* @param fee The additional amount of tokens to repay. | |
* @param data Arbitrary data structure, intended to contain user-defined parameters. | |
* @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" | |
*/ | |
function onFlashLoan( | |
address initiator, | |
address token, | |
uint256 amount, | |
uint256 fee, | |
bytes calldata data | |
) external returns (bytes32); | |
} | |
/** | |
* @dev Interface of the ERC3156 FlashLender, as defined in | |
* https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. | |
* | |
* _Available since v4.1._ | |
*/ | |
interface IERC3156FlashLender { | |
/** | |
* @dev The amount of currency available to be lended. | |
* @param token The loan currency. | |
* @return The amount of `token` that can be borrowed. | |
*/ | |
function maxFlashLoan(address token) external view returns (uint256); | |
/** | |
* @dev The fee to be charged for a given loan. | |
* @param token The loan currency. | |
* @param amount The amount of tokens lent. | |
* @return The amount of `token` to be charged for the loan, on top of the returned principal. | |
*/ | |
function flashFee(address token, uint256 amount) external view returns (uint256); | |
/** | |
* @dev Initiate a flash loan. | |
* @param receiver The receiver of the tokens in the loan, and the receiver of the callback. | |
* @param token The loan currency. | |
* @param amount The amount of tokens lent. | |
* @param data Arbitrary data structure, intended to contain user-defined parameters. | |
*/ | |
function flashLoan( | |
IERC3156FlashBorrower receiver, | |
address token, | |
uint256 amount, | |
bytes calldata data | |
) external returns (bool); | |
} | |
/// @notice Gas optimized reentrancy protection for smart contracts. | |
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) | |
abstract contract ReentrancyGuard { | |
uint256 private reentrancyStatus = 1; | |
modifier nonReentrant() { | |
require(reentrancyStatus == 1, "REENTRANCY"); | |
reentrancyStatus = 2; | |
_; | |
reentrancyStatus = 1; | |
} | |
} | |
/// @notice Trident access control contract. | |
/// @author Adapted from https://github.com/boringcrypto/BoringSolidity/blob/master/contracts/BoringOwnable.sol, License-Identifier: MIT. | |
contract TridentOwnable { | |
address public owner; | |
address public pendingOwner; | |
event TransferOwner(address indexed sender, address indexed recipient); | |
event TransferOwnerClaim(address indexed sender, address indexed recipient); | |
/// @notice Initialize and grant deployer account (`msg.sender`) `owner` access role. | |
constructor() { | |
owner = msg.sender; | |
emit TransferOwner(address(0), msg.sender); | |
} | |
/// @notice Access control modifier that requires modified function to be called by `owner` account. | |
modifier onlyOwner() { | |
require(msg.sender == owner, "NOT_OWNER"); | |
_; | |
} | |
/// @notice `pendingOwner` can claim `owner` account. | |
function claimOwner() external { | |
require(msg.sender == pendingOwner, "NOT_PENDING_OWNER"); | |
emit TransferOwner(owner, msg.sender); | |
owner = msg.sender; | |
pendingOwner = address(0); | |
} | |
/// @notice Transfer `owner` account. | |
/// @param recipient Account granted `owner` access control. | |
/// @param direct If 'true', ownership is directly transferred. | |
function transferOwner(address recipient, bool direct) external onlyOwner { | |
require(recipient != address(0), "ZERO_ADDRESS"); | |
if (direct) { | |
owner = recipient; | |
emit TransferOwner(msg.sender, recipient); | |
} else { | |
pendingOwner = recipient; | |
emit TransferOwnerClaim(msg.sender, recipient); | |
} | |
} | |
} | |
/// @notice Flash-lendable ETH wrapper with elastic shares. | |
contract FlashPot is ERC20("FlashPot Wrapped Ether", "FWETH", 18), IERC3156FlashLender, ReentrancyGuard, TridentOwnable { | |
using SafeTransferLib for address; | |
/*/////////////////////////////////////////////////////////////// | |
EVENTS | |
//////////////////////////////////////////////////////////////*/ | |
event Deposit(address indexed from, uint256 amount); | |
event Withdrawal(address indexed to, uint256 amount); | |
/*/////////////////////////////////////////////////////////////// | |
ERRORS | |
//////////////////////////////////////////////////////////////*/ | |
error CallbackFailed(); | |
error RepayFailed(); | |
/*/////////////////////////////////////////////////////////////// | |
FLASH STORAGE | |
//////////////////////////////////////////////////////////////*/ | |
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); | |
uint256 public fee; | |
/*/////////////////////////////////////////////////////////////// | |
CONSTRUCTOR | |
//////////////////////////////////////////////////////////////*/ | |
constructor(uint256 fee_) { | |
fee = fee_; | |
} | |
/*/////////////////////////////////////////////////////////////// | |
FLASH OPERATIONS | |
//////////////////////////////////////////////////////////////*/ | |
function flashLoan( | |
IERC3156FlashBorrower receiver, | |
address, | |
uint256 amount, | |
bytes calldata data | |
) public override nonReentrant returns (bool) { | |
uint256 flee = flashFee(address(0), amount); | |
uint256 startingBalance = address(this).balance; | |
address(receiver).safeTransferETH(amount); | |
if (receiver.onFlashLoan(msg.sender, address(0), amount, flee, data) != CALLBACK_SUCCESS) revert CallbackFailed(); | |
if (address(this).balance != startingBalance + flee) revert RepayFailed(); | |
return true; | |
} | |
function flashFee( | |
address, | |
uint256 amount | |
) public view override returns (uint256) { | |
return amount * fee / 10000; | |
} | |
function maxFlashLoan(address) public view override returns (uint256) { | |
return address(this).balance; | |
} | |
/*/////////////////////////////////////////////////////////////// | |
WRAP OPERATIONS | |
//////////////////////////////////////////////////////////////*/ | |
function deposit() public payable nonReentrant { | |
uint256 totalShares = totalSupply; | |
uint256 totalETH = address(this).balance - msg.value; | |
if (totalShares == 0 || totalETH == 0) { | |
_mint(msg.sender, msg.value); | |
} else { | |
uint256 what = (msg.value * totalShares) / totalETH; | |
_mint(msg.sender, what); | |
} | |
emit Deposit(msg.sender, msg.value); | |
} | |
function withdraw(uint256 share) public nonReentrant { | |
uint256 what = (share * address(this).balance) / totalSupply; | |
_burn(msg.sender, share); | |
msg.sender.safeTransferETH(what); | |
emit Withdrawal(msg.sender, share); | |
} | |
receive() external payable {} | |
/*/////////////////////////////////////////////////////////////// | |
GOVERNANCE | |
//////////////////////////////////////////////////////////////*/ | |
function updateFlashFee(uint256 fee_) public onlyOwner { | |
fee = fee_; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment