Skip to content

Commit 006a72f

Browse files
maxencerbmaximebrugel
andauthored
Added request interfaces for position manager + callback (#2)
* feat: added interfaces for position manager + callback * chore: update README * chore: fix type in Request.sol Co-authored-by: maximebrugel <maxime.brugel@kiln.fi> * feat: added error on repayment when repayment is complete * feat: added repayment deadline that automatically unlocks the deadline --------- Co-authored-by: maximebrugel <maxime.brugel@kiln.fi>
1 parent a5937ca commit 006a72f

9 files changed

Lines changed: 761 additions & 90 deletions

File tree

‎README.md‎

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ The `Request` contract is the core contract managing funding requests with dual-
6767

6868
- **OfferReceiver**: Validates and processes signed offers using EIP-712 signatures
6969
- **VaultController**: Manages PT/YT tokens with ERC4626-style redemptions
70-
- **Ownable**: Restricts admin functions to the contract owner
70+
- **OwnableRoles**: Restricts admin functions to the contract owner and allows for the "puller" role to pull funds from the contract
7171

7272
### Request Lifecycle
7373

@@ -78,7 +78,7 @@ The `Request` contract is the core contract managing funding requests with dual-
7878
7979
1. DEPLOYMENT
8080
└─> Factory creates Request + PT Vault + YT Vault (beacon proxies)
81-
└─> Owner is set, contract initialized
81+
└─> Owner and puller role are set, contract initialized
8282
8383
2. FUNDING PHASE (canWithdraw = false)
8484
├─> 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-
8787
└─> Owner authorizes → Prime broker calls mint() → PT/YT minted
8888
8989
3. FUND UTILIZATION
90-
└─> Owner calls pullFunds() to transfer assets to borrower
90+
└─> Puller calls pullFunds(amount, data) to transfer assets to themselves
91+
└─> If data is provided, onPullFunds() callback is invoked on the puller
9192
9293
4. REPAYMENT
93-
└─> Borrower transfers assets back to Request contract
94+
└─> Puller (or borrower) transfers assets back to Request contract
95+
└─> Optional: Use repay(amount) helper function
9496
9597
5. REDEMPTION PHASE (canWithdraw = true)
9698
└─> Owner calls setRepaid()
@@ -110,7 +112,7 @@ The owner broadcasts a signed EIP-712 offer to the contract. This method is idea
110112
2. Owner calls `consume(offer, signature, ptAmount)`
111113
3. Contract validates the signature and offer parameters
112114
4. Contract calls `onRequestConsumed()` callback on the maker's contract
113-
5. Contract pulls `ptAmount` of underlying asset from the owner (msg.sender)
115+
5. Contract pulls `ptAmount` of underlying asset from the maker (offer.maker)
114116
6. PT and YT tokens are minted to the maker
115117
7. The maker's offer nonce is updated
116118

@@ -176,20 +178,52 @@ request.mint(); // Receives 1M PT + 100k YT
176178

177179
After offers are consumed or minting is complete:
178180

179-
1. **Pull Funds**: Owner calls `pullFunds(receiver, amount)` to transfer collected assets to the borrower
180-
2. **Borrower Utilizes Funds**: The borrower uses the funds for their intended purpose
181-
3. **Repayment**: Borrower transfers assets back to the Request contract
182-
4. **Enable Redemptions**: Owner calls `setRepaid()` to unlock withdrawals
181+
1. **Pull Funds**: Address with puller role calls `pullFunds(amount, data)` to transfer collected assets to themselves
182+
2. **Callback Invocation**: If `data.length > 0`, the contract calls `onPullFunds(amount, data)` on the puller address
183+
3. **Fund Utilization**: The puller uses the funds for their intended purpose
184+
4. **Repayment**: Puller (or borrower) transfers assets back to the Request contract
185+
5. **Enable Redemptions**: Owner calls `setRepaid()` to unlock withdrawals
183186

184187
```solidity
185-
// After funding phase
186-
request.pullFunds(borrowerAddress, totalFunded);
188+
// After funding phase - puller pulls funds
189+
// Funds are transferred to msg.sender (the puller)
190+
request.pullFunds(totalFunded, ""); // No callback
187191
188-
// After borrower repays
192+
// With callback data - useful for automated position management
193+
bytes memory positionData = abi.encode(positionId, strategy);
194+
request.pullFunds(totalFunded, positionData); // Calls onPullFunds() on puller
195+
196+
// After repayment (optional helper function)
197+
request.repay(repaymentAmount);
198+
// Or direct transfer:
199+
asset.transfer(address(request), repaymentAmount);
200+
201+
// Enable redemptions
189202
request.setRepaid(); // Enables PT/YT holders to redeem
190203
```
191204

192-
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.
205+
**Puller Role:**
206+
- The puller role is set during contract initialization via the factory
207+
- Only addresses with the puller role can call `pullFunds()`
208+
- Funds are always transferred to `msg.sender` (the puller), not a separate recipient
209+
- The puller can be a smart contract implementing `IPositionManagerRequestCallback` for automated fund management
210+
211+
**Callback Interface:**
212+
213+
Position managers implementing automated strategies can implement `IPositionManagerRequestCallback`:
214+
215+
```solidity
216+
interface IPositionManagerRequestCallback {
217+
function onPullFunds(uint256 amount, bytes calldata data) external;
218+
}
219+
```
220+
221+
The callback is invoked **after** funds are transferred, allowing the position manager to:
222+
- Open positions in DeFi protocols
223+
- Update internal accounting
224+
- Execute automated strategies based on the provided data
225+
226+
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.
193227

194228
## RequestFactory
195229

@@ -231,6 +265,7 @@ RequestFactory factory = new RequestFactory(beaconOwner);
231265
// Create a new request with PT/YT vaults
232266
(address request, address ptVault, address ytVault) = factory.createRequest(
233267
owner, // Request owner (admin)
268+
puller, // Address with puller role (can call pullFunds)
234269
address(usdc), // Underlying asset
235270
"USDC Request", // Base name (becomes "PT-USDC Request" / "YT-USDC Request")
236271
"USDC-REQ" // Base symbol (becomes "PT-USDC-REQ" / "YT-USDC-REQ")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.20;
3+
4+
/// @title IPositionManagerRequest
5+
/// @notice Interface for the PositionManagerRequest contract that pulls funds from the contract.
6+
interface IPositionManagerRequest {
7+
/// @notice Transfers underlying assets from the contract to the puller.
8+
/// @dev This function is used after offers are consumed to transfer the collected funds
9+
/// to the borrower. The borrower then repays by transferring assets back to the
10+
/// contract before `setRepaid()` is called to enable PT/YT holder withdrawals.
11+
/// @dev This function should trigger a callback to the puller if data is provided.
12+
/// This should only be called by the puller role.
13+
/// @param amount The amount of underlying assets to transfer
14+
/// @param data Additional data to be passed to the puller callback
15+
function pullFunds(uint256 amount, bytes calldata data) external;
16+
17+
/// @notice Repays the request by transferring the underlying assets back to the contract.
18+
/// @dev This function is used to repay the request by transferring the underlying assets back to the contract.
19+
/// It assumes caller has enough allowance to transfer the underlying assets back to the contract.
20+
/// Depending on the request contract implementation, this is optional and may be done via a simple transfer.
21+
/// @param amount The amount of underlying assets to transfer
22+
function repay(uint256 amount) external;
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.20;
3+
4+
/// @title IPositionManagerRequestCallback
5+
/// @notice Interface for contracts that want to receive callbacks when their requests are being consumed.
6+
interface IPositionManagerRequestCallback {
7+
/// @notice Called when the PositionManagerRequest contract pulls funds from the contract with non-null data.
8+
/// @param amount The amount of underlying assets to transfer
9+
/// @param data Additional data to be passed to the callback
10+
function onPullFunds(uint256 amount, bytes calldata data) external;
11+
}

‎src/interfaces/request/IRequest.sol‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ pragma solidity ^0.8.20;
33

44
import {Offer} from "./IOfferReceiver.sol";
55
import {IVaultController} from "./IVaultController.sol";
6+
import {IPositionManagerRequest} from "./IPositionManagerRequest.sol";
67

78
/// @title IRequest
89
/// @notice Interface for the Request contract that manages funding requests with dual-token (PT/YT) issuance.
9-
interface IRequest is IVaultController {
10+
interface IRequest is IVaultController, IPositionManagerRequest {
1011
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
1112
/* EVENTS */
1213
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -33,14 +34,6 @@ interface IRequest is IVaultController {
3334
/// @param ytAmount The amount of YT tokens the address can mint
3435
function authorizeMinting(address to, uint128 ptAmount, uint128 ytAmount) external;
3536

36-
/// @notice Transfers underlying assets from the contract to a specified receiver.
37-
/// @dev This function is used after offers are consumed to transfer the collected funds
38-
/// to the borrower. The borrower then repays by transferring assets back to the
39-
/// contract before `setRepaid()` is called to enable PT/YT holder withdrawals.
40-
/// @param receiver The address to receive the transferred assets
41-
/// @param amount The amount of underlying assets to transfer
42-
function pullFunds(address receiver, uint256 amount) external;
43-
4437
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
4538
/* MINTING */
4639
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

‎src/request/Request.sol‎

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {TokenController} from "./abstract/tokens/TokenController.sol";
77
import {LibMintAuth} from "../libs/request/LibMintAuth.sol";
88
import {IERC20} from "../interfaces/integrations/IERC20.sol";
99
import {IRequest} from "../interfaces/request/IRequest.sol";
10+
import {IPositionManagerRequest} from "../interfaces/request/IPositionManagerRequest.sol";
1011
import {IRequestCallback} from "../interfaces/request/IRequestCallback.sol";
12+
import {IPositionManagerRequestCallback} from "../interfaces/request/IPositionManagerRequestCallback.sol";
1113
import {ITokenController} from "../interfaces/request/ITokenController.sol";
1214
import {Offer} from "../interfaces/request/IOfferReceiver.sol";
1315
import {Initializable} from "lib/solady/src/utils/Initializable.sol";
14-
import {Ownable} from "lib/solady/src/auth/Ownable.sol";
16+
import {OwnableRoles} from "lib/solady/src/auth/OwnableRoles.sol";
1517
import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol";
1618
import {ReentrancyGuardTransient} from "lib/solady/src/utils/ReentrancyGuardTransient.sol";
1719
import {SafeTransferLib} from "lib/solady/src/utils/SafeTransferLib.sol";
@@ -23,7 +25,7 @@ import {EIP712} from "lib/solady/src/utils/EIP712.sol";
2325
/// - **OfferReceiver**: Validates and processes signed offers using EIP-712 signatures
2426
/// - **VaultController**: Manages PT/YT tokens with ERC4626-style redemptions
2527
/// - **Initializable**: Supports initialization for proxy deployments
26-
/// - **Ownable**: Restricts admin functions to the contract owner
28+
/// - **OwnableRoles**: Restricts admin functions to the contract owner and allows for the "puller" role to pull funds from the contract.
2729
/// - **ReentrancyGuard**: Prevents reentrancy attacks during offer consumption
2830
///
2931
/// Deployment Options:
@@ -44,16 +46,22 @@ import {EIP712} from "lib/solady/src/utils/EIP712.sol";
4446
/// 2. Owner calls `consume()` with the offer, signature, and PT amount to fulfill
4547
/// 3. The maker's `onRequestConsumed` callback is invoked to prepare funds
4648
/// 4. Assets are transferred from owner and PT/YT tokens are minted to the maker
47-
contract Request is IRequest, OfferReceiver, VaultController, Initializable, Ownable, ReentrancyGuardTransient {
49+
contract Request is IRequest, OfferReceiver, VaultController, Initializable, OwnableRoles, ReentrancyGuardTransient {
4850
using FixedPointMathLib for uint256;
4951
using SafeTransferLib for address;
5052
using LibMintAuth for address;
5153

54+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
55+
/* CONSTANTS */
56+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
57+
58+
uint256 internal constant _ROLE_PULLER = _ROLE_0;
59+
5260
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
5361
/* ERRORS */
5462
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
5563

56-
/// @dev The request has already been repaid, preventing further calls to `setRepaid()`.
64+
/// @dev The request has already been repaid, preventing further calls to `setRepaid()`, `pullFunds()`, and `repay()`.
5765
error AlreadyRepaid();
5866

5967
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
@@ -64,13 +72,15 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own
6472
/// @dev Uses ERC-7201 namespaced storage pattern for proxy compatibility. All fields are grouped
6573
/// and accessed via a fixed storage slot to prevent collisions with inherited contracts.
6674
/// @param asset The address of the underlying ERC20 asset (e.g., USDC)
75+
/// @param repaymentDeadline The time at which repayments are unlocked regardless of whether repaid is true or false
6776
/// @param repaid Whether the request has been repaid, enabling withdrawals
6877
/// @param ptToken The address of the Principal Token contract
6978
/// @param ytToken The address of the Yield Token contract
7079
/// @param name The base name for the PT/YT tokens (prefixed with "PT-" / "YT-")
7180
/// @param symbol The base symbol for the PT/YT tokens (prefixed with "PT-" / "YT-")
7281
struct RequestStorage {
7382
address asset;
83+
uint64 repaymentDeadline;
7484
bool repaid;
7585
address ptToken;
7686
address ytToken;
@@ -101,28 +111,34 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own
101111
/// @notice Initializes the Request contract with all required parameters.
102112
/// @dev Can only be called once due to the `initializer` modifier. Sets up the contract owner,
103113
/// underlying asset, PT/YT token addresses, and metadata. The contract starts in a non-repaid
104-
/// state where withdrawals are disabled.
114+
/// state where withdrawals are disabled until either setRepaid() is called or the repayment deadline passes.
105115
/// @param owner_ The address that will own the contract and have admin privileges
116+
/// @param puller_ The address that will have the puller role
106117
/// @param asset_ The address of the underlying ERC20 asset (e.g., USDC)
107118
/// @param ptToken_ The address of the deployed Principal Token contract
108119
/// @param ytToken_ The address of the deployed Yield Token contract
109120
/// @param name_ The base name for the tokens (will be prefixed with "PT-" / "YT-")
110121
/// @param symbol_ The base symbol for the tokens (will be prefixed with "PT-" / "YT-")
122+
/// @param repaymentDeadline_ The timestamp after which withdrawals are automatically enabled, regardless of repaid status
111123
function initialize(
112124
address owner_,
125+
address puller_,
113126
address asset_,
114127
address ptToken_,
115128
address ytToken_,
116129
string memory name_,
117-
string memory symbol_
130+
string memory symbol_,
131+
uint64 repaymentDeadline_
118132
) public initializer {
119133
RequestStorage storage req = _requestStorage();
120134
req.asset = asset_;
135+
req.repaymentDeadline = repaymentDeadline_;
121136
req.ptToken = ptToken_;
122137
req.ytToken = ytToken_;
123138
req.name = name_;
124139
req.symbol = symbol_;
125140
_initializeOwner(owner_);
141+
_setRoles(puller_, _ROLE_PULLER);
126142
}
127143

128144
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
@@ -135,8 +151,11 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own
135151
}
136152

137153
/// @inheritdoc VaultController
154+
/// @dev Returns true if either the request has been marked as repaid OR the repayment deadline has passed.
155+
/// This allows withdrawals to be enabled automatically after a deadline, even if setRepaid() was never called.
138156
function _canWithdraw() internal view override returns (bool) {
139-
return _requestStorage().repaid;
157+
RequestStorage storage req = _requestStorage();
158+
return req.repaid || block.timestamp >= req.repaymentDeadline;
140159
}
141160

142161
/// @inheritdoc ITokenController
@@ -189,16 +208,29 @@ contract Request is IRequest, OfferReceiver, VaultController, Initializable, Own
189208
emit AuthorizedMinting(to, ptAmount, ytAmount);
190209
}
191210

192-
/// @inheritdoc IRequest
193-
/// @dev Only callable by the owner. This function is used after offers are consumed to
194-
/// transfer the collected funds to the borrower (or any designated receiver). The borrower
211+
/// @inheritdoc IPositionManagerRequest
212+
/// @dev Only callable by the puller role. This function is used after offers are consumed to
213+
/// transfer the collected funds to the puller. The puller
195214
/// is then expected to repay by transferring assets back to the contract before
196215
/// `setRepaid()` is called to enable PT/YT holder withdrawals.
197216
/// Emits a Transfer event from the underlying asset contract.
198217
/// @custom:reverts If the request has been repaid
199-
function pullFunds(address receiver, uint256 amount) external onlyOwner {
218+
function pullFunds(uint256 amount, bytes calldata data) external onlyRoles(_ROLE_PULLER) {
219+
if (_canWithdraw()) revert AlreadyRepaid();
220+
_asset().safeTransfer(msg.sender, amount);
221+
if (data.length > 0) {
222+
IPositionManagerRequestCallback(msg.sender).onPullFunds(amount, data);
223+
}
224+
}
225+
226+
/// @inheritdoc IPositionManagerRequest
227+
/// @dev Transfers the underlying assets back to the contract. This is purely optional
228+
/// with the given implementation and may be done via a simple transfer.
229+
/// Cannot be called after the request has been repaid (when withdrawals are enabled).
230+
/// @custom:reverts If the request has been repaid (canWithdraw is true)
231+
function repay(uint256 amount) external {
200232
if (_canWithdraw()) revert AlreadyRepaid();
201-
_asset().safeTransfer(receiver, amount);
233+
_asset().safeTransferFrom(msg.sender, address(this), amount);
202234
}
203235

204236
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/

0 commit comments

Comments
 (0)