diff --git a/README.md b/README.md index 549d7489..cfb41360 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The `Request` contract is the core contract managing funding requests with dual- - **OfferReceiver**: Validates and processes signed offers using EIP-712 signatures - **VaultController**: Manages PT/YT tokens with ERC4626-style redemptions -- **Ownable**: Restricts admin functions to the contract owner +- **OwnableRoles**: Restricts admin functions to the contract owner and allows for the "puller" role to pull funds from the contract ### Request Lifecycle @@ -78,7 +78,7 @@ The `Request` contract is the core contract managing funding requests with dual- 1. DEPLOYMENT └─> Factory creates Request + PT Vault + YT Vault (beacon proxies) - └─> Owner is set, contract initialized + └─> Owner and puller role are set, contract initialized 2. FUNDING PHASE (canWithdraw = false) ├─> Method A: consume() - Process signed offers from prime brokers @@ -87,10 +87,12 @@ The `Request` contract is the core contract managing funding requests with dual- └─> Owner authorizes → Prime broker calls mint() → PT/YT minted 3. FUND UTILIZATION - └─> Owner calls pullFunds() to transfer assets to borrower + └─> Puller calls pullFunds(amount, data) to transfer assets to themselves + └─> If data is provided, onPullFunds() callback is invoked on the puller 4. REPAYMENT - └─> Borrower transfers assets back to Request contract + └─> Puller (or borrower) transfers assets back to Request contract + └─> Optional: Use repay(amount) helper function 5. REDEMPTION PHASE (canWithdraw = true) └─> Owner calls setRepaid() @@ -110,7 +112,7 @@ The owner broadcasts a signed EIP-712 offer to the contract. This method is idea 2. Owner calls `consume(offer, signature, ptAmount)` 3. Contract validates the signature and offer parameters 4. Contract calls `onRequestConsumed()` callback on the maker's contract -5. Contract pulls `ptAmount` of underlying asset from the owner (msg.sender) +5. Contract pulls `ptAmount` of underlying asset from the maker (offer.maker) 6. PT and YT tokens are minted to the maker 7. The maker's offer nonce is updated @@ -176,20 +178,52 @@ request.mint(); // Receives 1M PT + 100k YT After offers are consumed or minting is complete: -1. **Pull Funds**: Owner calls `pullFunds(receiver, amount)` to transfer collected assets to the borrower -2. **Borrower Utilizes Funds**: The borrower uses the funds for their intended purpose -3. **Repayment**: Borrower transfers assets back to the Request contract -4. **Enable Redemptions**: Owner calls `setRepaid()` to unlock withdrawals +1. **Pull Funds**: Address with puller role calls `pullFunds(amount, data)` to transfer collected assets to themselves +2. **Callback Invocation**: If `data.length > 0`, the contract calls `onPullFunds(amount, data)` on the puller address +3. **Fund Utilization**: The puller uses the funds for their intended purpose +4. **Repayment**: Puller (or borrower) transfers assets back to the Request contract +5. **Enable Redemptions**: Owner calls `setRepaid()` to unlock withdrawals ```solidity -// After funding phase -request.pullFunds(borrowerAddress, totalFunded); +// After funding phase - puller pulls funds +// Funds are transferred to msg.sender (the puller) +request.pullFunds(totalFunded, ""); // No callback -// After borrower repays +// With callback data - useful for automated position management +bytes memory positionData = abi.encode(positionId, strategy); +request.pullFunds(totalFunded, positionData); // Calls onPullFunds() on puller + +// After repayment (optional helper function) +request.repay(repaymentAmount); +// Or direct transfer: +asset.transfer(address(request), repaymentAmount); + +// Enable redemptions request.setRepaid(); // Enables PT/YT holders to redeem ``` -The `pullFunds()` function can be called multiple times by the owner during the funding phase. The `setRepaid()` function toggles the withdrawal lock, enabling PT/YT holders to redeem their tokens for the underlying assets held by the contract. +**Puller Role:** +- The puller role is set during contract initialization via the factory +- Only addresses with the puller role can call `pullFunds()` +- Funds are always transferred to `msg.sender` (the puller), not a separate recipient +- The puller can be a smart contract implementing `IPositionManagerRequestCallback` for automated fund management + +**Callback Interface:** + +Position managers implementing automated strategies can implement `IPositionManagerRequestCallback`: + +```solidity +interface IPositionManagerRequestCallback { + function onPullFunds(uint256 amount, bytes calldata data) external; +} +``` + +The callback is invoked **after** funds are transferred, allowing the position manager to: +- Open positions in DeFi protocols +- Update internal accounting +- Execute automated strategies based on the provided data + +The `pullFunds()` function can be called multiple times by the puller during the funding phase. The `setRepaid()` function toggles the withdrawal lock, enabling PT/YT holders to redeem their tokens for the underlying assets held by the contract. ## RequestFactory @@ -231,6 +265,7 @@ RequestFactory factory = new RequestFactory(beaconOwner); // Create a new request with PT/YT vaults (address request, address ptVault, address ytVault) = factory.createRequest( owner, // Request owner (admin) + puller, // Address with puller role (can call pullFunds) address(usdc), // Underlying asset "USDC Request", // Base name (becomes "PT-USDC Request" / "YT-USDC Request") "USDC-REQ" // Base symbol (becomes "PT-USDC-REQ" / "YT-USDC-REQ") diff --git a/src/interfaces/request/IPositionManagerRequest.sol b/src/interfaces/request/IPositionManagerRequest.sol new file mode 100644 index 00000000..7f7445ad --- /dev/null +++ b/src/interfaces/request/IPositionManagerRequest.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +/// @title IPositionManagerRequest +/// @notice Interface for the PositionManagerRequest contract that pulls funds from the contract. +interface IPositionManagerRequest { + /// @notice Transfers underlying assets from the contract to the puller. + /// @dev This function is used after offers are consumed to transfer the collected funds + /// to the borrower. The borrower then repays by transferring assets back to the + /// contract before `setRepaid()` is called to enable PT/YT holder withdrawals. + /// @dev This function should trigger a callback to the puller if data is provided. + /// This should only be called by the puller role. + /// @param amount The amount of underlying assets to transfer + /// @param data Additional data to be passed to the puller callback + function pullFunds(uint256 amount, bytes calldata data) external; + + /// @notice Repays the request by transferring the underlying assets back to the contract. + /// @dev This function is used to repay the request by transferring the underlying assets back to the contract. + /// It assumes caller has enough allowance to transfer the underlying assets back to the contract. + /// Depending on the request contract implementation, this is optional and may be done via a simple transfer. + /// @param amount The amount of underlying assets to transfer + function repay(uint256 amount) external; +} diff --git a/src/interfaces/request/IPositionManagerRequestCallback.sol b/src/interfaces/request/IPositionManagerRequestCallback.sol new file mode 100644 index 00000000..19715c27 --- /dev/null +++ b/src/interfaces/request/IPositionManagerRequestCallback.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +/// @title IPositionManagerRequestCallback +/// @notice Interface for contracts that want to receive callbacks when their requests are being consumed. +interface IPositionManagerRequestCallback { + /// @notice Called when the PositionManagerRequest contract pulls funds from the contract with non-null data. + /// @param amount The amount of underlying assets to transfer + /// @param data Additional data to be passed to the callback + function onPullFunds(uint256 amount, bytes calldata data) external; +} diff --git a/src/interfaces/request/IRequest.sol b/src/interfaces/request/IRequest.sol index d4818ac6..bbeec4d6 100644 --- a/src/interfaces/request/IRequest.sol +++ b/src/interfaces/request/IRequest.sol @@ -3,10 +3,11 @@ pragma solidity ^0.8.20; import {Offer} from "./IOfferReceiver.sol"; import {IVaultController} from "./IVaultController.sol"; +import {IPositionManagerRequest} from "./IPositionManagerRequest.sol"; /// @title IRequest /// @notice Interface for the Request contract that manages funding requests with dual-token (PT/YT) issuance. -interface IRequest is IVaultController { +interface IRequest is IVaultController, IPositionManagerRequest { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -33,14 +34,6 @@ interface IRequest is IVaultController { /// @param ytAmount The amount of YT tokens the address can mint function authorizeMinting(address to, uint128 ptAmount, uint128 ytAmount) external; - /// @notice Transfers underlying assets from the contract to a specified receiver. - /// @dev This function is used after offers are consumed to transfer the collected funds - /// to the borrower. The borrower then repays by transferring assets back to the - /// contract before `setRepaid()` is called to enable PT/YT holder withdrawals. - /// @param receiver The address to receive the transferred assets - /// @param amount The amount of underlying assets to transfer - function pullFunds(address receiver, uint256 amount) external; - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* MINTING */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/request/Request.sol b/src/request/Request.sol index 4c7ac725..86ea9705 100644 --- a/src/request/Request.sol +++ b/src/request/Request.sol @@ -7,11 +7,13 @@ import {TokenController} from "./abstract/tokens/TokenController.sol"; import {LibMintAuth} from "../libs/request/LibMintAuth.sol"; import {IERC20} from "../interfaces/integrations/IERC20.sol"; import {IRequest} from "../interfaces/request/IRequest.sol"; +import {IPositionManagerRequest} from "../interfaces/request/IPositionManagerRequest.sol"; import {IRequestCallback} from "../interfaces/request/IRequestCallback.sol"; +import {IPositionManagerRequestCallback} from "../interfaces/request/IPositionManagerRequestCallback.sol"; import {ITokenController} from "../interfaces/request/ITokenController.sol"; import {Offer} from "../interfaces/request/IOfferReceiver.sol"; import {Initializable} from "lib/solady/src/utils/Initializable.sol"; -import {Ownable} from "lib/solady/src/auth/Ownable.sol"; +import {OwnableRoles} from "lib/solady/src/auth/OwnableRoles.sol"; import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; import {ReentrancyGuardTransient} from "lib/solady/src/utils/ReentrancyGuardTransient.sol"; import {SafeTransferLib} from "lib/solady/src/utils/SafeTransferLib.sol"; @@ -23,7 +25,7 @@ import {EIP712} from "lib/solady/src/utils/EIP712.sol"; /// - **OfferReceiver**: Validates and processes signed offers using EIP-712 signatures /// - **VaultController**: Manages PT/YT tokens with ERC4626-style redemptions /// - **Initializable**: Supports initialization for proxy deployments -/// - **Ownable**: Restricts admin functions to the contract owner +/// - **OwnableRoles**: Restricts admin functions to the contract owner and allows for the "puller" role to pull funds from the contract. /// - **ReentrancyGuard**: Prevents reentrancy attacks during offer consumption /// /// Deployment Options: @@ -44,16 +46,22 @@ import {EIP712} from "lib/solady/src/utils/EIP712.sol"; /// 2. Owner calls `consume()` with the offer, signature, and PT amount to fulfill /// 3. The maker's `onRequestConsumed` callback is invoked to prepare funds /// 4. Assets are transferred from owner and PT/YT tokens are minted to the maker -contract Request is IRequest, OfferReceiver, VaultController, Initializable, Ownable, ReentrancyGuardTransient { +contract Request is IRequest, OfferReceiver, VaultController, Initializable, OwnableRoles, ReentrancyGuardTransient { using FixedPointMathLib for uint256; using SafeTransferLib for address; using LibMintAuth for address; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + uint256 internal constant _ROLE_PULLER = _ROLE_0; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @dev The request has already been repaid, preventing further calls to `setRepaid()`. + /// @dev The request has already been repaid, preventing further calls to `setRepaid()`, `pullFunds()`, and `repay()`. error AlreadyRepaid(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -64,6 +72,7 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own /// @dev Uses ERC-7201 namespaced storage pattern for proxy compatibility. All fields are grouped /// and accessed via a fixed storage slot to prevent collisions with inherited contracts. /// @param asset The address of the underlying ERC20 asset (e.g., USDC) + /// @param repaymentDeadline The time at which repayments are unlocked regardless of whether repaid is true or false /// @param repaid Whether the request has been repaid, enabling withdrawals /// @param ptToken The address of the Principal Token contract /// @param ytToken The address of the Yield Token contract @@ -71,6 +80,7 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own /// @param symbol The base symbol for the PT/YT tokens (prefixed with "PT-" / "YT-") struct RequestStorage { address asset; + uint64 repaymentDeadline; bool repaid; address ptToken; address ytToken; @@ -101,28 +111,34 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own /// @notice Initializes the Request contract with all required parameters. /// @dev Can only be called once due to the `initializer` modifier. Sets up the contract owner, /// underlying asset, PT/YT token addresses, and metadata. The contract starts in a non-repaid - /// state where withdrawals are disabled. + /// state where withdrawals are disabled until either setRepaid() is called or the repayment deadline passes. /// @param owner_ The address that will own the contract and have admin privileges + /// @param puller_ The address that will have the puller role /// @param asset_ The address of the underlying ERC20 asset (e.g., USDC) /// @param ptToken_ The address of the deployed Principal Token contract /// @param ytToken_ The address of the deployed Yield Token contract /// @param name_ The base name for the tokens (will be prefixed with "PT-" / "YT-") /// @param symbol_ The base symbol for the tokens (will be prefixed with "PT-" / "YT-") + /// @param repaymentDeadline_ The timestamp after which withdrawals are automatically enabled, regardless of repaid status function initialize( address owner_, + address puller_, address asset_, address ptToken_, address ytToken_, string memory name_, - string memory symbol_ + string memory symbol_, + uint64 repaymentDeadline_ ) public initializer { RequestStorage storage req = _requestStorage(); req.asset = asset_; + req.repaymentDeadline = repaymentDeadline_; req.ptToken = ptToken_; req.ytToken = ytToken_; req.name = name_; req.symbol = symbol_; _initializeOwner(owner_); + _setRoles(puller_, _ROLE_PULLER); } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -135,8 +151,11 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own } /// @inheritdoc VaultController + /// @dev Returns true if either the request has been marked as repaid OR the repayment deadline has passed. + /// This allows withdrawals to be enabled automatically after a deadline, even if setRepaid() was never called. function _canWithdraw() internal view override returns (bool) { - return _requestStorage().repaid; + RequestStorage storage req = _requestStorage(); + return req.repaid || block.timestamp >= req.repaymentDeadline; } /// @inheritdoc ITokenController @@ -189,16 +208,29 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own emit AuthorizedMinting(to, ptAmount, ytAmount); } - /// @inheritdoc IRequest - /// @dev Only callable by the owner. This function is used after offers are consumed to - /// transfer the collected funds to the borrower (or any designated receiver). The borrower + /// @inheritdoc IPositionManagerRequest + /// @dev Only callable by the puller role. This function is used after offers are consumed to + /// transfer the collected funds to the puller. The puller /// is then expected to repay by transferring assets back to the contract before /// `setRepaid()` is called to enable PT/YT holder withdrawals. /// Emits a Transfer event from the underlying asset contract. /// @custom:reverts If the request has been repaid - function pullFunds(address receiver, uint256 amount) external onlyOwner { + function pullFunds(uint256 amount, bytes calldata data) external onlyRoles(_ROLE_PULLER) { + if (_canWithdraw()) revert AlreadyRepaid(); + _asset().safeTransfer(msg.sender, amount); + if (data.length > 0) { + IPositionManagerRequestCallback(msg.sender).onPullFunds(amount, data); + } + } + + /// @inheritdoc IPositionManagerRequest + /// @dev Transfers the underlying assets back to the contract. This is purely optional + /// with the given implementation and may be done via a simple transfer. + /// Cannot be called after the request has been repaid (when withdrawals are enabled). + /// @custom:reverts If the request has been repaid (canWithdraw is true) + function repay(uint256 amount) external { if (_canWithdraw()) revert AlreadyRepaid(); - _asset().safeTransfer(receiver, amount); + _asset().safeTransferFrom(msg.sender, address(this), amount); } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ diff --git a/src/request/RequestFactory.sol b/src/request/RequestFactory.sol index 99755cc1..5a578e63 100644 --- a/src/request/RequestFactory.sol +++ b/src/request/RequestFactory.sol @@ -84,27 +84,33 @@ contract RequestFactory { /// 1. Deploys Request proxy pointing to REQUEST_BEACON /// 2. Deploys PT Token proxy pointing to PT_TOKEN_BEACON /// 3. Deploys YT Token proxy pointing to YT_TOKEN_BEACON - /// 4. Initializes Request with owner, asset, token addresses, and metadata + /// 4. Initializes Request with owner, asset, token addresses, metadata, and repayment deadline /// 5. Initializes both tokens with the Request as their controller /// /// The Request becomes the controller for both tokens, managing minting and burning. /// Emits a {RequestCreated} event. /// @param owner The address that will own the Request (admin privileges) + /// @param puller The address that will have the puller role /// @param asset The underlying ERC20 asset address (e.g., USDC) /// @param name The base name for PT/YT tokens (prefixed with "PT-" / "YT-") /// @param symbol The base symbol for PT/YT tokens (prefixed with "PT-" / "YT-") + /// @param repaymentDeadline The timestamp after which withdrawals are automatically enabled, regardless of repaid status /// @return request The address of the newly deployed Request proxy /// @return ptToken The address of the newly deployed PT Token proxy /// @return ytToken The address of the newly deployed YT Token proxy - function createRequest(address owner, address asset, string memory name, string memory symbol) - external - returns (address request, address ptToken, address ytToken) - { + function createRequest( + address owner, + address puller, + address asset, + string memory name, + string memory symbol, + uint64 repaymentDeadline + ) external returns (address request, address ptToken, address ytToken) { request = REQUEST_BEACON.deployERC1967IBeaconProxy(); ptToken = PT_TOKEN_BEACON.deployERC1967IBeaconProxy(); ytToken = YT_TOKEN_BEACON.deployERC1967IBeaconProxy(); - Request(request).initialize(owner, asset, ptToken, ytToken, name, symbol); + Request(request).initialize(owner, puller, asset, ptToken, ytToken, name, symbol, repaymentDeadline); Vault(ptToken).initialize(request); Vault(ytToken).initialize(request); diff --git a/test/mock/request/MockPositionManagerRequestCallback.sol b/test/mock/request/MockPositionManagerRequestCallback.sol new file mode 100644 index 00000000..452bea46 --- /dev/null +++ b/test/mock/request/MockPositionManagerRequestCallback.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +import {IPositionManagerRequestCallback} from "../../../src/interfaces/request/IPositionManagerRequestCallback.sol"; + +/// @notice Mock contract for testing IPositionManagerRequestCallback functionality +/// @dev Implements the callback to handle pullFunds callbacks +contract MockPositionManagerRequestCallback is IPositionManagerRequestCallback { + bool public callbackCalled; + uint256 public lastAmount; + bytes public lastData; + bool public shouldRevert; + + function setShouldRevert(bool _shouldRevert) external { + shouldRevert = _shouldRevert; + } + + function onPullFunds(uint256 amount, bytes calldata data) external override { + if (shouldRevert) revert("MockPositionManagerRequestCallback: forced revert"); + + callbackCalled = true; + lastAmount = amount; + lastData = data; + } + + function reset() external { + callbackCalled = false; + lastAmount = 0; + lastData = ""; + } +} + diff --git a/test/request/Request.t.sol b/test/request/Request.t.sol index b6134bbf..eda09019 100644 --- a/test/request/Request.t.sol +++ b/test/request/Request.t.sol @@ -9,6 +9,7 @@ import {Vault} from "../../src/request/Vault.sol"; import {ControlledVault} from "../../src/request/abstract/vault/ControlledVault.sol"; import {MockERC20} from "../mock/MockERC20.sol"; import {MockRequestCallback} from "../mock/request/MockRequestCallback.sol"; +import {MockPositionManagerRequestCallback} from "../mock/request/MockPositionManagerRequestCallback.sol"; import {Offer} from "../../src/interfaces/request/IOfferReceiver.sol"; import {UpgradeableBeacon} from "lib/solady/src/utils/UpgradeableBeacon.sol"; @@ -21,6 +22,7 @@ contract RequestTest is Test { // Test addresses address public owner; + address public puller; address public borrower; address public beaconOwner; @@ -44,6 +46,7 @@ contract RequestTest is Test { function setUp() public { owner = makeAddr("owner"); + puller = makeAddr("puller"); borrower = makeAddr("borrower"); beaconOwner = makeAddr("beaconOwner"); maker = vm.createWallet("maker"); @@ -55,10 +58,10 @@ contract RequestTest is Test { // Deploy factory factory = new RequestFactory(beaconOwner); - // Create request via factory + // Create request via factory with far future deadline (effectively disabled for most tests) vm.prank(owner); (address reqAddr, address ptAddr, address ytAddr) = - factory.createRequest(owner, address(asset), "Test Request", "REQ"); + factory.createRequest(owner, puller, address(asset), "Test Request", "REQ", uint64(type(uint64).max)); request = Request(reqAddr); ptVault = Vault(ptAddr); @@ -86,7 +89,7 @@ contract RequestTest is Test { vm.expectEmit(false, true, false, false); emit RequestCreated(address(0), address(asset), address(0), address(0)); - factory.createRequest(owner, address(asset), "New Request", "NEW"); + factory.createRequest(owner, puller, address(asset), "New Request", "NEW", uint64(type(uint64).max)); } function test_factory_createRequest_initializesCorrectly() public view { @@ -119,7 +122,9 @@ contract RequestTest is Test { function test_initialize_cannotReinitialize() public { vm.expectRevert(); - request.initialize(owner, address(asset), address(ptVault), address(ytVault), "New", "NEW"); + request.initialize( + owner, puller, address(asset), address(ptVault), address(ytVault), "New", "NEW", uint64(type(uint64).max) + ); } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -294,11 +299,12 @@ contract RequestTest is Test { request.mint(); vm.stopPrank(); - // Now pull funds - vm.prank(owner); - request.pullFunds(borrower, amount); + // Now pull funds (puller receives funds, no callback) + asset.mint(puller, 0); // Ensure puller exists + vm.prank(puller); + request.pullFunds(amount, ""); - assertEq(asset.balanceOf(borrower), amount); + assertEq(asset.balanceOf(puller), amount); assertEq(asset.balanceOf(address(request)), 0); } @@ -316,28 +322,350 @@ contract RequestTest is Test { vm.stopPrank(); // Pull partial funds - vm.prank(owner); - request.pullFunds(borrower, 500_000e6); + vm.prank(puller); + request.pullFunds(500_000e6, ""); - assertEq(asset.balanceOf(borrower), 500_000e6); + assertEq(asset.balanceOf(puller), 500_000e6); assertEq(asset.balanceOf(address(request)), 500_000e6); } - function test_pullFunds_onlyOwner() public { - address notOwner = makeAddr("notOwner"); + function test_pullFunds_onlyPuller() public { + address notPuller = makeAddr("notPuller"); - vm.prank(notOwner); + vm.prank(notPuller); vm.expectRevert(Unauthorized.selector); - request.pullFunds(borrower, 1_000_000e6); + request.pullFunds(1_000_000e6, ""); } function test_pullFunds_revertsWhenRepaid() public { vm.prank(owner); request.setRepaid(); + vm.prank(puller); + vm.expectRevert(AlreadyRepaid.selector); + request.pullFunds(1_000_000e6, ""); + } + + function test_pullFunds_withCallback() public { + // First deposit some funds via mint + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Deploy callback contract + MockPositionManagerRequestCallback callback = new MockPositionManagerRequestCallback(); + + // Create a new request with callback as puller + vm.prank(owner); + (address reqAddr,,) = factory.createRequest( + owner, address(callback), address(asset), "Callback Request", "CALLBACK", uint64(type(uint64).max) + ); + + Request callbackRequest = Request(reqAddr); + + // Fund the callback request + vm.prank(owner); + callbackRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(callbackRequest), amount); + callbackRequest.mint(); + vm.stopPrank(); + + // Pull funds with callback data + bytes memory callbackData = abi.encode("test", 123); + vm.prank(address(callback)); + callbackRequest.pullFunds(amount, callbackData); + + // Verify callback was called + assertEq(callback.callbackCalled(), true); + assertEq(callback.lastAmount(), amount); + assertEq(callback.lastData(), callbackData); + assertEq(asset.balanceOf(address(callback)), amount); + } + + function test_pullFunds_withCallback_revertsIfCallbackReverts() public { + // First deposit some funds via mint + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Deploy callback contract that will revert + MockPositionManagerRequestCallback callback = new MockPositionManagerRequestCallback(); + callback.setShouldRevert(true); + + // Create a new request with callback as puller + vm.prank(owner); + (address reqAddr,,) = factory.createRequest( + owner, address(callback), address(asset), "Callback Request", "CALLBACK", uint64(type(uint64).max) + ); + + Request callbackRequest = Request(reqAddr); + + // Fund the callback request + vm.prank(owner); + callbackRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(callbackRequest), amount); + callbackRequest.mint(); + vm.stopPrank(); + + // Pull funds with callback data - should revert + bytes memory callbackData = abi.encode("test"); + vm.prank(address(callback)); + vm.expectRevert("MockPositionManagerRequestCallback: forced revert"); + callbackRequest.pullFunds(amount, callbackData); + } + + function test_pullFunds_withEmptyData_noCallback() public { + // First deposit some funds via mint + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Deploy callback contract + MockPositionManagerRequestCallback callback = new MockPositionManagerRequestCallback(); + + // Create a new request with callback as puller + vm.prank(owner); + (address reqAddr,,) = factory.createRequest( + owner, address(callback), address(asset), "Callback Request", "CALLBACK", uint64(type(uint64).max) + ); + + Request callbackRequest = Request(reqAddr); + + // Fund the callback request + vm.prank(owner); + callbackRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(callbackRequest), amount); + callbackRequest.mint(); + vm.stopPrank(); + + // Pull funds with empty data - callback should not be called + vm.prank(address(callback)); + callbackRequest.pullFunds(amount, ""); + + // Verify callback was NOT called + assertEq(callback.callbackCalled(), false); + assertEq(callback.lastAmount(), 0); + assertEq(asset.balanceOf(address(callback)), amount); + } + + function test_pullFunds_withCallback_differentData() public { + // First deposit some funds via mint + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Deploy callback contract + MockPositionManagerRequestCallback callback = new MockPositionManagerRequestCallback(); + + // Create a new request with callback as puller + vm.prank(owner); + (address reqAddr,,) = factory.createRequest( + owner, address(callback), address(asset), "Callback Request", "CALLBACK", uint64(type(uint64).max) + ); + + Request callbackRequest = Request(reqAddr); + + // Fund the callback request + vm.prank(owner); + callbackRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(callbackRequest), amount); + callbackRequest.mint(); + vm.stopPrank(); + + // Pull funds with different data types + bytes memory data1 = abi.encode("string data"); + vm.prank(address(callback)); + callbackRequest.pullFunds(500_000e6, data1); + + assertEq(callback.callbackCalled(), true); + assertEq(callback.lastAmount(), 500_000e6); + assertEq(callback.lastData(), data1); + + // Reset and pull again with different data + callback.reset(); + bytes memory data2 = abi.encode(12345, "test"); + vm.prank(address(callback)); + callbackRequest.pullFunds(500_000e6, data2); + + assertEq(callback.callbackCalled(), true); + assertEq(callback.lastAmount(), 500_000e6); + assertEq(callback.lastData(), data2); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* REPAY TESTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function test_repay_success() public { + // First deposit some funds via mint + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Pull funds - puller now has the funds + vm.prank(puller); + request.pullFunds(amount, ""); + + assertEq(asset.balanceOf(puller), amount); + assertEq(asset.balanceOf(address(request)), 0); + + // Repay funds - puller transfers back + vm.startPrank(puller); + asset.approve(address(request), amount); + request.repay(amount); + vm.stopPrank(); + + assertEq(asset.balanceOf(address(request)), amount); + assertEq(asset.balanceOf(puller), 0); + } + + function test_repay_partial() public { + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Pull funds - puller now has the funds + vm.prank(puller); + request.pullFunds(amount, ""); + + assertEq(asset.balanceOf(puller), amount); + + // Repay partial funds + vm.startPrank(puller); + asset.approve(address(request), 500_000e6); + request.repay(500_000e6); + vm.stopPrank(); + + assertEq(asset.balanceOf(address(request)), 500_000e6); + assertEq(asset.balanceOf(puller), 500_000e6); + } + + function test_repay_revertsWhenRepaid() public { + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Pull funds - puller now has the funds + vm.prank(puller); + request.pullFunds(amount, ""); + + // Repay funds + vm.startPrank(puller); + asset.approve(address(request), amount); + request.repay(amount); + vm.stopPrank(); + + // Mark as repaid + vm.prank(owner); + request.setRepaid(); + + // Try to repay again - should revert (even if puller has funds) + asset.mint(puller, amount); + vm.startPrank(puller); + asset.approve(address(request), amount); vm.expectRevert(AlreadyRepaid.selector); - request.pullFunds(borrower, 1_000_000e6); + request.repay(amount); + vm.stopPrank(); + } + + function test_repay_multipleTimes() public { + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + request.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(request), amount); + request.mint(); + vm.stopPrank(); + + // Pull funds - puller now has the funds + vm.prank(puller); + request.pullFunds(amount, ""); + + // Repay in multiple transactions + vm.startPrank(puller); + asset.approve(address(request), amount); + + request.repay(300_000e6); + assertEq(asset.balanceOf(address(request)), 300_000e6); + + request.repay(400_000e6); + assertEq(asset.balanceOf(address(request)), 700_000e6); + + request.repay(300_000e6); + assertEq(asset.balanceOf(address(request)), amount); + vm.stopPrank(); } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -396,16 +724,16 @@ contract RequestTest is Test { assertEq(ptVault.balanceOf(primeBroker), principal); assertEq(ytVault.balanceOf(primeBroker), expectedYield); - // 3. Owner pulls funds to borrower - vm.prank(owner); - request.pullFunds(borrower, principal); + // 3. Puller pulls funds + vm.prank(puller); + request.pullFunds(principal, ""); - assertEq(asset.balanceOf(borrower), principal); + assertEq(asset.balanceOf(puller), principal); - // 4. Borrower repays with profit + // 4. Puller repays with profit (simulating borrower repayment) uint256 repayAmount = principal + 50_000e6; // 50k profit (half of expected) - asset.mint(borrower, 50_000e6); - vm.prank(borrower); + asset.mint(puller, 50_000e6); + vm.prank(puller); asset.transfer(address(request), repayAmount); // 5. Owner marks as repaid @@ -457,11 +785,11 @@ contract RequestTest is Test { assertEq(ytVault.totalSupply(), 150_000e6); // Pull and repay with full expected return - vm.prank(owner); - request.pullFunds(borrower, 1_500_000e6); + vm.prank(puller); + request.pullFunds(1_500_000e6, ""); - asset.mint(borrower, 150_000e6); // Add the yield - vm.prank(borrower); + asset.mint(puller, 150_000e6); // Add the yield + vm.prank(puller); asset.transfer(address(request), 1_650_000e6); vm.prank(owner); @@ -502,11 +830,11 @@ contract RequestTest is Test { vm.stopPrank(); // Pull funds - vm.prank(owner); - request.pullFunds(borrower, principal); + vm.prank(puller); + request.pullFunds(principal, ""); - // Borrower only returns 900k (10% loss) - vm.prank(borrower); + // Puller only returns 900k (10% loss) + vm.prank(puller); asset.transfer(address(request), 900_000e6); vm.prank(owner); @@ -577,10 +905,10 @@ contract RequestTest is Test { request.mint(); vm.stopPrank(); - vm.prank(owner); - request.pullFunds(borrower, pullAmount); + vm.prank(puller); + request.pullFunds(pullAmount, ""); - assertEq(asset.balanceOf(borrower), pullAmount); + assertEq(asset.balanceOf(puller), pullAmount); assertEq(asset.balanceOf(address(request)), depositAmount - pullAmount); } @@ -603,8 +931,8 @@ contract RequestTest is Test { vm.stopPrank(); // Pull funds - vm.prank(owner); - request.pullFunds(borrower, principal); + vm.prank(puller); + request.pullFunds(principal, ""); // Repay (mint directly to request to simulate repayment) asset.mint(address(request), actualReturn); @@ -672,12 +1000,14 @@ contract RequestTest is Test { MockERC20 asset8 = new MockERC20("WBTC", "WBTC", 8); // Create request with 18 decimals - (, address pt18, address yt18) = factory.createRequest(owner, address(asset18), "DAI Request", "DAI-REQ"); + (, address pt18, address yt18) = + factory.createRequest(owner, puller, address(asset18), "DAI Request", "DAI-REQ", uint64(type(uint64).max)); assertEq(Vault(pt18).decimals(), 18); assertEq(Vault(yt18).decimals(), 18); // Create request with 8 decimals - (, address pt8, address yt8) = factory.createRequest(owner, address(asset8), "WBTC Request", "WBTC-REQ"); + (, address pt8, address yt8) = + factory.createRequest(owner, puller, address(asset8), "WBTC Request", "WBTC-REQ", uint64(type(uint64).max)); assertEq(Vault(pt8).decimals(), 8); assertEq(Vault(yt8).decimals(), 8); } @@ -731,5 +1061,212 @@ contract RequestTest is Test { vm.stopPrank(); } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* REPAYMENT DEADLINE TESTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function test_repaymentDeadline_enablesWithdrawalsAfterDeadline() public { + // Create a new request with a deadline in the future + uint64 deadline = uint64(block.timestamp + 30 days); + vm.prank(owner); + (address reqAddr, address ptAddr, address ytAddr) = + factory.createRequest(owner, puller, address(asset), "Deadline Request", "DEADLINE", deadline); + + Request deadlineRequest = Request(reqAddr); + Vault deadlinePtVault = Vault(ptAddr); + Vault deadlineYtVault = Vault(ytAddr); + + // Initially, withdrawals should be disabled + assertEq(deadlineRequest.canWithdraw(), false); + + // Deposit some funds + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + deadlineRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(deadlineRequest), amount); + deadlineRequest.mint(); + vm.stopPrank(); + + // Fast forward past the deadline + vm.warp(deadline + 1); + + // Withdrawals should now be enabled even though setRepaid() was never called + assertEq(deadlineRequest.canWithdraw(), true); + + // PT/YT holders should be able to redeem + vm.startPrank(primeBroker); + uint256 ptAssets = deadlinePtVault.redeem(amount, primeBroker, primeBroker); + uint256 ytAssets = deadlineYtVault.redeem(100_000e6, primeBroker, primeBroker); + vm.stopPrank(); + + assertEq(ptAssets, amount); + assertEq(ytAssets, 0); // No yield assets since nothing was repaid + } + + function test_repaymentDeadline_setRepaidStillWorks() public { + uint64 deadline = uint64(block.timestamp + 30 days); + vm.prank(owner); + (address reqAddr,,) = factory.createRequest(owner, puller, address(asset), "Deadline Request", "DEADLINE", deadline); + + Request deadlineRequest = Request(reqAddr); + + // Initially disabled + assertEq(deadlineRequest.canWithdraw(), false); + + // Call setRepaid before deadline + vm.prank(owner); + deadlineRequest.setRepaid(); + + // Should be enabled immediately + assertEq(deadlineRequest.canWithdraw(), true); + } + + function test_repaymentDeadline_blocksOperationsAfterDeadline() public { + uint64 deadline = uint64(block.timestamp + 30 days); + vm.prank(owner); + (address reqAddr,,) = factory.createRequest(owner, puller, address(asset), "Deadline Request", "DEADLINE", deadline); + + Request deadlineRequest = Request(reqAddr); + + // Deposit some funds + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + deadlineRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(deadlineRequest), amount); + deadlineRequest.mint(); + vm.stopPrank(); + + // Pull funds + vm.prank(puller); + deadlineRequest.pullFunds(amount, ""); + + // Fast forward past deadline + vm.warp(deadline + 1); + + // Operations should be blocked after deadline + vm.prank(owner); + vm.expectRevert(AlreadyRepaid.selector); + deadlineRequest.setRepaid(); + + vm.prank(puller); + vm.expectRevert(AlreadyRepaid.selector); + deadlineRequest.pullFunds(100e6, ""); + + vm.prank(puller); + vm.expectRevert(AlreadyRepaid.selector); + deadlineRequest.repay(100e6); + + // But withdrawals should work + assertEq(deadlineRequest.canWithdraw(), true); + } + + function test_repaymentDeadline_mintBlockedAfterDeadline() public { + uint64 deadline = uint64(block.timestamp + 30 days); + vm.prank(owner); + (address reqAddr,,) = factory.createRequest(owner, puller, address(asset), "Deadline Request", "DEADLINE", deadline); + + Request deadlineRequest = Request(reqAddr); + + // Authorize minting + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + deadlineRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + // Fast forward past deadline + vm.warp(deadline + 1); + + // Mint should be blocked + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(deadlineRequest), amount); + vm.expectRevert(AlreadyRepaid.selector); + deadlineRequest.mint(); + vm.stopPrank(); + } + + function test_repaymentDeadline_consumeBlockedAfterDeadline() public { + uint64 deadline = uint64(block.timestamp + 30 days); + vm.prank(owner); + (address reqAddr,,) = factory.createRequest(owner, puller, address(asset), "Deadline Request", "DEADLINE", deadline); + + Request deadlineRequest = Request(reqAddr); + + // Fast forward past deadline + vm.warp(deadline + 1); + + // Consume should be blocked + Offer memory offer = _createOffer(address(0x123), 1_000_000e6, 100_000e6, 1, block.timestamp + 1 days); + bytes memory signature = _signOffer(offer, maker); + + vm.prank(owner); + vm.expectRevert(AlreadyRepaid.selector); + deadlineRequest.consume(offer, signature, 1_000_000e6); + } + + function test_repaymentDeadline_beforeDeadlineOperationsWork() public { + uint64 deadline = uint64(block.timestamp + 30 days); + vm.prank(owner); + (address reqAddr,,) = factory.createRequest(owner, puller, address(asset), "Deadline Request", "DEADLINE", deadline); + + Request deadlineRequest = Request(reqAddr); + + // Before deadline, operations should work normally + assertEq(deadlineRequest.canWithdraw(), false); + + // Deposit funds + address primeBroker = makeAddr("primeBroker"); + uint128 amount = 1_000_000e6; + + vm.prank(owner); + deadlineRequest.authorizeMinting(primeBroker, amount, 100_000e6); + + asset.mint(primeBroker, amount); + vm.startPrank(primeBroker); + asset.approve(address(deadlineRequest), amount); + deadlineRequest.mint(); + vm.stopPrank(); + + // Pull funds should work + vm.prank(puller); + deadlineRequest.pullFunds(amount, ""); + + // Repay should work + asset.mint(puller, amount); + vm.startPrank(puller); + asset.approve(address(deadlineRequest), amount); + deadlineRequest.repay(amount); + vm.stopPrank(); + + // setRepaid should work + vm.prank(owner); + deadlineRequest.setRepaid(); + + assertEq(deadlineRequest.canWithdraw(), true); + } + + function test_repaymentDeadline_exactlyAtDeadline() public { + uint64 deadline = uint64(block.timestamp + 30 days); + vm.prank(owner); + (address reqAddr,,) = factory.createRequest(owner, puller, address(asset), "Deadline Request", "DEADLINE", deadline); + + Request deadlineRequest = Request(reqAddr); + + // At exactly the deadline, withdrawals should be enabled + vm.warp(deadline); + assertEq(deadlineRequest.canWithdraw(), true); + } } diff --git a/test/request/RequestConsume.t.sol b/test/request/RequestConsume.t.sol index 4263f0e6..40a2f696 100644 --- a/test/request/RequestConsume.t.sol +++ b/test/request/RequestConsume.t.sol @@ -22,6 +22,7 @@ contract RequestConsumeTest is Test { // Test addresses address public owner; + address public puller; address public borrower; address public beaconOwner; @@ -43,6 +44,7 @@ contract RequestConsumeTest is Test { function setUp() public { owner = makeAddr("owner"); + puller = makeAddr("puller"); borrower = makeAddr("borrower"); beaconOwner = makeAddr("beaconOwner"); callbackSigner = vm.createWallet("callbackSigner"); @@ -53,10 +55,10 @@ contract RequestConsumeTest is Test { // Deploy factory factory = new RequestFactory(beaconOwner); - // Create request via factory + // Create request via factory with far future deadline vm.prank(owner); (address reqAddr, address ptAddr, address ytAddr) = - factory.createRequest(owner, address(asset), "Test Request", "REQ"); + factory.createRequest(owner, puller, address(asset), "Test Request", "REQ", uint64(type(uint64).max)); request = Request(reqAddr); ptVault = Vault(ptAddr); @@ -363,13 +365,13 @@ contract RequestConsumeTest is Test { assertEq(ptVault.balanceOf(address(callback)), offerAmount); assertEq(ytVault.balanceOf(address(callback)), expectedReturn); - // 2. Pull funds to borrower - vm.prank(owner); - request.pullFunds(borrower, offerAmount); + // 2. Pull funds to puller + vm.prank(puller); + request.pullFunds(offerAmount, ""); - // 3. Borrower repays with full expected return - asset.mint(borrower, expectedReturn); - vm.prank(borrower); + // 3. Puller repays with full expected return (simulating borrower repayment) + asset.mint(puller, expectedReturn); + vm.prank(puller); asset.transfer(address(request), offerAmount + expectedReturn); // 4. Mark as repaid @@ -423,12 +425,12 @@ contract RequestConsumeTest is Test { assertEq(ytVault.totalSupply(), consumeReturn + mintYield); // 4. Pull, repay, redeem - vm.prank(owner); - request.pullFunds(borrower, consumeAmount + mintAmount); + vm.prank(puller); + request.pullFunds(consumeAmount + mintAmount, ""); // Full repayment with yield - asset.mint(borrower, consumeReturn + mintYield); - vm.prank(borrower); + asset.mint(puller, consumeReturn + mintYield); + vm.prank(puller); asset.transfer(address(request), consumeAmount + mintAmount + consumeReturn + mintYield); vm.prank(owner);