// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; interface IPactSwap { function swap(uint256 amount, address recipient) external returns (uint256); } /// @notice Experimental, unaudited permission vault. The shipped application uses test assets only. /// @dev No arbitrary calls, delegatecall or router allowances. Two typed actions, fixed asset and adapter. contract PactVault is ReentrancyGuard { using SafeERC20 for IERC20; enum Action { Transfer, Swap } struct Grant { address owner; address agent; address recipient; uint256 remaining; uint256 perCall; uint256 minRate; // output base units per one input base unit; demo USDC 6 / demo WETH 18 uint64 expires; uint16 maxCalls; uint16 calls; Action action; bool closed; } IERC20 public immutable asset; IERC20 public immutable output; address public immutable adapter; uint256 public nextId=1; mapping(uint256=>Grant) public grants; event Created(uint256 indexed id,address indexed owner,address indexed agent,address recipient,uint256 budget,Action action,uint64 expires); event Executed(uint256 indexed id,uint256 amount,uint256 outputAmount,address recipient,uint256 remaining); event Closed(uint256 indexed id,uint256 refunded,uint8 reason); // 0 completed; 1 revoked; 2 expired error InvalidPolicy(); error NotOwner(); error NotAgent(); error Inactive(); error Expired(); error OverBudget(); error PerCallLimit(); error TooManyCalls(); error InsufficientOutput(); error UnsupportedToken(); constructor(address asset_,address output_,address adapter_) { require(asset_.code.length>0 && output_.code.length>0 && adapter_.code.length>0,'contracts required'); require(asset_!=output_,'distinct assets');asset=IERC20(asset_);output=IERC20(output_);adapter=adapter_; } function create(address agent,address recipient,uint256 budget,uint256 perCall,uint64 expires,uint16 maxCalls,Action action,uint256 minRate) external nonReentrant returns(uint256 id) { if(agent==address(0)||recipient==address(0)||recipient==address(this)||budget==0||perCall==0||perCall>budget||expires<=block.timestamp||expires>block.timestamp+7 days||maxCalls==0||maxCalls>100|| (action==Action.Swap && minRate==0)) revert InvalidPolicy(); uint256 beforeBalance=asset.balanceOf(address(this)); asset.safeTransferFrom(msg.sender,address(this),budget); if(asset.balanceOf(address(this))-beforeBalance!=budget) revert UnsupportedToken(); id=nextId++; grants[id]=Grant(msg.sender,agent,recipient,budget,perCall,minRate,expires,maxCalls,0,action,false); emit Created(id,msg.sender,agent,recipient,budget,action,expires); } function execute(uint256 id,uint256 amount) external nonReentrant { Grant storage g=grants[id]; if(g.closed||g.owner==address(0)) revert Inactive(); if(msg.sender!=g.agent) revert NotAgent(); if(block.timestamp>=g.expires) revert Expired(); if(amount==0||amount>g.remaining) revert OverBudget(); if(amount>g.perCall) revert PerCallLimit(); if(g.calls>=g.maxCalls) revert TooManyCalls(); g.remaining-=amount;g.calls++; uint256 out; if(g.action==Action.Transfer){asset.safeTransfer(g.recipient,amount);out=amount;} else { uint256 beforeBalance=output.balanceOf(g.recipient); asset.safeTransfer(adapter,amount); IPactSwap(adapter).swap(amount,g.recipient); out=output.balanceOf(g.recipient)-beforeBalance; if(out0)asset.safeTransfer(g.owner,refund); emit Closed(id,refund,reason); } }