|
// SPDX-License-Identifier: MIT |
|
pragma solidity ^0.8.19; |
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
|
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; |
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; |
|
|
|
// Permit2 interface |
|
interface IPermit2 { |
|
struct TokenPermissions { |
|
address token; |
|
uint256 amount; |
|
} |
|
|
|
struct PermitTransferFrom { |
|
TokenPermissions permitted; |
|
uint256 nonce; |
|
uint256 deadline; |
|
} |
|
|
|
struct SignatureTransferDetails { |
|
address to; |
|
uint256 requestedAmount; |
|
} |
|
|
|
function permitTransferFrom( |
|
PermitTransferFrom calldata permit, |
|
SignatureTransferDetails calldata transferDetails, |
|
address owner, |
|
bytes calldata signature |
|
) external; |
|
} |
|
|
|
/** |
|
* @title TokenVault |
|
* @dev A vault contract that supports both ERC20Permit and Permit2 for gasless approvals |
|
*/ |
|
contract TokenVault is ReentrancyGuard { |
|
// Canonical Permit2 contract address (same across all networks) |
|
address public constant PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; |
|
|
|
// Events |
|
event Deposit(address indexed user, address indexed token, uint256 amount); |
|
event Withdrawal(address indexed user, address indexed token, uint256 amount); |
|
|
|
// User balances: user => token => amount |
|
mapping(address => mapping(address => uint256)) public balances; |
|
|
|
// Total deposits per token |
|
mapping(address => uint256) public totalDeposits; |
|
|
|
/** |
|
* @dev Standard deposit function (requires prior approval) |
|
* @param token The ERC20 token address |
|
* @param amount The amount to deposit |
|
*/ |
|
function deposit(address token, uint256 amount) external nonReentrant { |
|
require(amount > 0, "Amount must be greater than 0"); |
|
|
|
// Transfer tokens from user to vault |
|
IERC20(token).transferFrom(msg.sender, address(this), amount); |
|
|
|
// Update balances |
|
balances[msg.sender][token] += amount; |
|
totalDeposits[token] += amount; |
|
|
|
emit Deposit(msg.sender, token, amount); |
|
} |
|
|
|
/** |
|
* @dev Deposit using ERC20Permit (EIP-2612) - Single transaction |
|
* @param token The ERC20Permit token address |
|
* @param amount The amount to deposit |
|
* @param deadline The permit deadline |
|
* @param v The permit signature parameter |
|
* @param r The permit signature parameter |
|
* @param s The permit signature parameter |
|
*/ |
|
function depositWithPermit( |
|
address token, |
|
uint256 amount, |
|
uint256 deadline, |
|
uint8 v, |
|
bytes32 r, |
|
bytes32 s |
|
) external nonReentrant { |
|
require(amount > 0, "Amount must be greater than 0"); |
|
|
|
// Execute permit to grant allowance |
|
IERC20Permit(token).permit( |
|
msg.sender, |
|
address(this), |
|
amount, |
|
deadline, |
|
v, |
|
r, |
|
s |
|
); |
|
|
|
// Transfer tokens from user to vault |
|
IERC20(token).transferFrom(msg.sender, address(this), amount); |
|
|
|
// Update balances |
|
balances[msg.sender][token] += amount; |
|
totalDeposits[token] += amount; |
|
|
|
emit Deposit(msg.sender, token, amount); |
|
} |
|
|
|
/** |
|
* @dev Deposit using Permit2 - Works with any ERC20 token |
|
* @param token The ERC20 token address |
|
* @param amount The amount to deposit |
|
* @param nonce The permit nonce |
|
* @param deadline The permit deadline |
|
* @param signature The permit signature |
|
*/ |
|
function depositWithPermit2( |
|
address token, |
|
uint256 amount, |
|
uint256 nonce, |
|
uint256 deadline, |
|
bytes calldata signature |
|
) external nonReentrant { |
|
require(amount > 0, "Amount must be greater than 0"); |
|
|
|
// Prepare Permit2 structs |
|
IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ |
|
permitted: IPermit2.TokenPermissions({ |
|
token: token, |
|
amount: amount |
|
}), |
|
nonce: nonce, |
|
deadline: deadline |
|
}); |
|
|
|
IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({ |
|
to: address(this), |
|
requestedAmount: amount |
|
}); |
|
|
|
// Execute permit transfer through Permit2 |
|
IPermit2(PERMIT2_ADDRESS).permitTransferFrom( |
|
permit, |
|
transferDetails, |
|
msg.sender, |
|
signature |
|
); |
|
|
|
// Update balances |
|
balances[msg.sender][token] += amount; |
|
totalDeposits[token] += amount; |
|
|
|
emit Deposit(msg.sender, token, amount); |
|
} |
|
|
|
/** |
|
* @dev Batch deposit using Permit2 - Deposit multiple tokens in one transaction |
|
* @param tokens Array of ERC20 token addresses |
|
* @param amounts Array of amounts to deposit |
|
* @param nonce The permit nonce |
|
* @param deadline The permit deadline |
|
* @param signature The permit signature |
|
*/ |
|
function batchDepositWithPermit2( |
|
address[] calldata tokens, |
|
uint256[] calldata amounts, |
|
uint256 nonce, |
|
uint256 deadline, |
|
bytes calldata signature |
|
) external nonReentrant { |
|
require(tokens.length == amounts.length, "Arrays length mismatch"); |
|
require(tokens.length > 0, "Empty arrays"); |
|
|
|
// Note: This is a simplified example. For actual batch transfers, |
|
// you'd need to use the PermitBatchTransferFrom struct and |
|
// the corresponding permitTransferFrom overload in Permit2 |
|
|
|
for (uint256 i = 0; i < tokens.length; i++) { |
|
require(amounts[i] > 0, "Amount must be greater than 0"); |
|
|
|
// For each token, create individual permit and transfer |
|
IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ |
|
permitted: IPermit2.TokenPermissions({ |
|
token: tokens[i], |
|
amount: amounts[i] |
|
}), |
|
nonce: nonce + i, // Different nonce for each permit |
|
deadline: deadline |
|
}); |
|
|
|
IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({ |
|
to: address(this), |
|
requestedAmount: amounts[i] |
|
}); |
|
|
|
// This is simplified - in practice you'd want to batch the permits |
|
IPermit2(PERMIT2_ADDRESS).permitTransferFrom( |
|
permit, |
|
transferDetails, |
|
msg.sender, |
|
signature // In practice, you'd have separate signatures for each permit |
|
); |
|
|
|
// Update balances |
|
balances[msg.sender][tokens[i]] += amounts[i]; |
|
totalDeposits[tokens[i]] += amounts[i]; |
|
|
|
emit Deposit(msg.sender, tokens[i], amounts[i]); |
|
} |
|
} |
|
|
|
/** |
|
* @dev Withdraw tokens from the vault |
|
* @param token The ERC20 token address |
|
* @param amount The amount to withdraw |
|
*/ |
|
function withdraw(address token, uint256 amount) external nonReentrant { |
|
require(amount > 0, "Amount must be greater than 0"); |
|
require(balances[msg.sender][token] >= amount, "Insufficient balance"); |
|
|
|
// Update balances |
|
balances[msg.sender][token] -= amount; |
|
totalDeposits[token] -= amount; |
|
|
|
// Transfer tokens back to user |
|
IERC20(token).transfer(msg.sender, amount); |
|
|
|
emit Withdrawal(msg.sender, token, amount); |
|
} |
|
|
|
/** |
|
* @dev Get user's balance for a specific token |
|
* @param user The user address |
|
* @param token The token address |
|
* @return The user's balance |
|
*/ |
|
function getBalance(address user, address token) external view returns (uint256) { |
|
return balances[user][token]; |
|
} |
|
|
|
/** |
|
* @dev Get total deposits for a specific token |
|
* @param token The token address |
|
* @return The total deposits |
|
*/ |
|
function getTotalDeposits(address token) external view returns (uint256) { |
|
return totalDeposits[token]; |
|
} |
|
|
|
/** |
|
* @dev Emergency function to recover tokens (only for owner) |
|
* This is a simplified example - in production, you'd want proper access control |
|
*/ |
|
function emergencyWithdraw(address token) external { |
|
// Add proper access control here (e.g., onlyOwner modifier) |
|
uint256 balance = IERC20(token).balanceOf(address(this)); |
|
IERC20(token).transfer(msg.sender, balance); |
|
} |
|
} |
|
|
|
/** |
|
* @title AdvancedTokenVault |
|
* @dev Enhanced version with additional features like time locks and yield farming |
|
*/ |
|
contract AdvancedTokenVault is TokenVault { |
|
// Time lock for deposits |
|
mapping(address => mapping(address => uint256)) public lockUntil; |
|
|
|
// Minimum lock duration (e.g., 1 day) |
|
uint256 public constant MIN_LOCK_DURATION = 1 days; |
|
|
|
event DepositLocked(address indexed user, address indexed token, uint256 amount, uint256 lockUntil); |
|
|
|
/** |
|
* @dev Deposit with time lock using ERC20Permit |
|
*/ |
|
function depositWithPermitAndLock( |
|
address token, |
|
uint256 amount, |
|
uint256 lockDuration, |
|
uint256 deadline, |
|
uint8 v, |
|
bytes32 r, |
|
bytes32 s |
|
) external nonReentrant { |
|
require(amount > 0, "Amount must be greater than 0"); |
|
require(lockDuration >= MIN_LOCK_DURATION, "Lock duration too short"); |
|
|
|
// Execute permit |
|
IERC20Permit(token).permit( |
|
msg.sender, |
|
address(this), |
|
amount, |
|
deadline, |
|
v, |
|
r, |
|
s |
|
); |
|
|
|
// Transfer tokens |
|
IERC20(token).transferFrom(msg.sender, address(this), amount); |
|
|
|
// Update balances and lock time |
|
balances[msg.sender][token] += amount; |
|
totalDeposits[token] += amount; |
|
lockUntil[msg.sender][token] = block.timestamp + lockDuration; |
|
|
|
emit Deposit(msg.sender, token, amount); |
|
emit DepositLocked(msg.sender, token, amount, lockUntil[msg.sender][token]); |
|
} |
|
|
|
/** |
|
* @dev Deposit with time lock using Permit2 |
|
*/ |
|
function depositWithPermit2AndLock( |
|
address token, |
|
uint256 amount, |
|
uint256 lockDuration, |
|
uint256 nonce, |
|
uint256 deadline, |
|
bytes calldata signature |
|
) external nonReentrant { |
|
require(amount > 0, "Amount must be greater than 0"); |
|
require(lockDuration >= MIN_LOCK_DURATION, "Lock duration too short"); |
|
|
|
// Prepare Permit2 structs |
|
IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ |
|
permitted: IPermit2.TokenPermissions({ |
|
token: token, |
|
amount: amount |
|
}), |
|
nonce: nonce, |
|
deadline: deadline |
|
}); |
|
|
|
IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({ |
|
to: address(this), |
|
requestedAmount: amount |
|
}); |
|
|
|
// Execute permit transfer |
|
IPermit2(PERMIT2_ADDRESS).permitTransferFrom( |
|
permit, |
|
transferDetails, |
|
msg.sender, |
|
signature |
|
); |
|
|
|
// Update balances and lock time |
|
balances[msg.sender][token] += amount; |
|
totalDeposits[token] += amount; |
|
lockUntil[msg.sender][token] = block.timestamp + lockDuration; |
|
|
|
emit Deposit(msg.sender, token, amount); |
|
emit DepositLocked(msg.sender, token, amount, lockUntil[msg.sender][token]); |
|
} |
|
|
|
/** |
|
* @dev Override withdraw to check lock time |
|
*/ |
|
function withdraw(address token, uint256 amount) external override nonReentrant { |
|
require(amount > 0, "Amount must be greater than 0"); |
|
require(balances[msg.sender][token] >= amount, "Insufficient balance"); |
|
require(block.timestamp >= lockUntil[msg.sender][token], "Tokens are still locked"); |
|
|
|
// Update balances |
|
balances[msg.sender][token] -= amount; |
|
totalDeposits[token] -= amount; |
|
|
|
// Transfer tokens back to user |
|
IERC20(token).transfer(msg.sender, amount); |
|
|
|
emit Withdrawal(msg.sender, token, amount); |
|
} |
|
|
|
/** |
|
* @dev Get lock expiration time for user's tokens |
|
*/ |
|
function getLockExpiration(address user, address token) external view returns (uint256) { |
|
return lockUntil[user][token]; |
|
} |
|
|
|
/** |
|
* @dev Check if user's tokens are currently locked |
|
*/ |
|
function isLocked(address user, address token) external view returns (bool) { |
|
return block.timestamp < lockUntil[user][token]; |
|
} |
|
} |