// SPDX-License-Identifier: MIT pragma solidity 0.8.28; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Timestamps.sol"; using SafeERC20 for IERC20; using Timestamps for Timestamp; contract Vault { IERC20 private immutable _token; type Controller is address; type Context is bytes32; type Recipient is address; struct Lock { Timestamp expiry; Timestamp maximum; } mapping(Controller => mapping(Context => Lock)) private _locks; mapping(Controller => mapping(Context => mapping(Recipient => uint256))) private _available; mapping(Controller => mapping(Context => mapping(Recipient => uint256))) private _designated; constructor(IERC20 token) { _token = token; } function balance( Context context, Recipient recipient ) public view returns (uint256) { Controller controller = Controller.wrap(msg.sender); return _available[controller][context][recipient] + _designated[controller][context][recipient]; } function designated( Context context, Recipient recipient ) public view returns (uint256) { Controller controller = Controller.wrap(msg.sender); return _designated[controller][context][recipient]; } function lock(Context context) public view returns (Lock memory) { Controller controller = Controller.wrap(msg.sender); return _locks[controller][context]; } function deposit(Context context, address from, uint256 amount) public { Controller controller = Controller.wrap(msg.sender); Recipient recipient = Recipient.wrap(from); _available[controller][context][recipient] += amount; _token.safeTransferFrom(from, address(this), amount); } function _delete(Context context, Recipient recipient) private { Controller controller = Controller.wrap(msg.sender); delete _available[controller][context][recipient]; delete _designated[controller][context][recipient]; } function withdraw(Context context, Recipient recipient) public { Controller controller = Controller.wrap(msg.sender); require(!lock(context).expiry.isFuture(), Locked()); delete _locks[controller][context]; uint256 amount = balance(context, recipient); _delete(context, recipient); _token.safeTransfer(Recipient.unwrap(recipient), amount); } function burn(Context context, Recipient recipient) public { uint256 amount = balance(context, recipient); _delete(context, recipient); _token.safeTransfer(address(0xdead), amount); } function transfer( Context context, Recipient from, Recipient to, uint256 amount ) public { Controller controller = Controller.wrap(msg.sender); require( amount <= _available[controller][context][from], InsufficientBalance() ); _available[controller][context][from] -= amount; _available[controller][context][to] += amount; } function designate( Context context, Recipient recipient, uint256 amount ) public { Controller controller = Controller.wrap(msg.sender); require( amount <= _available[controller][context][recipient], InsufficientBalance() ); _available[controller][context][recipient] -= amount; _designated[controller][context][recipient] += amount; } function lockup(Context context, Timestamp expiry, Timestamp maximum) public { require(Timestamp.unwrap(lock(context).maximum) == 0, AlreadyLocked()); require(!expiry.isAfter(maximum), ExpiryPastMaximum()); Controller controller = Controller.wrap(msg.sender); _locks[controller][context] = Lock({expiry: expiry, maximum: maximum}); } function extend(Context context, Timestamp expiry) public { Lock memory previous = lock(context); require(previous.expiry.isFuture(), LockExpired()); require(!previous.expiry.isAfter(expiry), InvalidExpiry()); require(!expiry.isAfter(previous.maximum), ExpiryPastMaximum()); Controller controller = Controller.wrap(msg.sender); _locks[controller][context].expiry = expiry; } error InsufficientBalance(); error Locked(); error AlreadyLocked(); error ExpiryPastMaximum(); error InvalidExpiry(); error LockExpired(); }