-
-
Notifications
You must be signed in to change notification settings - Fork 48
Targets
Targets are stateless contracts designed to interact with multiple functions of your protocol. The point is to compose multiple calls into one function so that your users need to perform fewer interactions. To do anything useful with PRBProxy, you need to either write a target contract or use one written by the community.
Here's a diagram that shows a typical user flow that interacts with a proxy target:
flowchart LR;
PO((Proxy Owner))-- "execute" -->P;
P[Proxy];
PT[Proxy Target]
P-- "delegate call" -->PT;
PT-- "foo logic" -->P
P-- "foo" -->DP[DeFi Protocol];
On prbproxy.com you should find all the ABIs and interfaces needed for interacting with Targets.
You interact with Targets via the execute
function:
function execute(address target, bytes calldata data) external payable override returns (bytes memory response) {
// <snip> ...
}
How you ABI-encode the data
depends upon the development environment you are in.
pragma solidity >=0.8.19;
function encode() pure returns (bytes memory) {
bytes memory data = abi.encodeWithSignature("foo(uint256)", 1337);
return data;
}
$ cast abi-encode "foo(uint256)" 1337
TODO
An example is worth a thousand words. Rather than bore you with more long-winded descriptions, let's see a few real-world use cases for target contracts.
The proxy can accumulate ETH and hold ERC-20 balances. The following target shows can be used to withdraw funds from the proxy contract to the proxy owner:
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IPRBProxy } from "./interfaces/IPRBProxy.sol";
interface IWrappedNativeAsset is IERC20 {
function deposit() external payable;
function withdraw(uint256 amount) external;
}
contract WithdrawerTarget {
using SafeERC20 for IERC20;
error NativeWithdrawalFailed();
function withdrawERC20(IERC20 asset, uint256 amount) external {
address owner = _getOwner();
asset.safeTransfer({ to: owner, value: amount });
}
function withdrawNative(uint256 amount) external {
address owner = _getOwner();
(bool sent,) = owner.call{ value: amount }("");
if (!sent) {
revert NativeWithdrawalFailed();
}
}
function _getOwner() internal view returns (address) {
return IPRBProxy(address(this)).owner();
}
}
Wrap ETH into WETH (the ERC-20 version of ETH) and deposit the resulting WETH into a DeFi protocol called Acme:
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19;
interface AcmeLike {
function depositCollateral(address token, uint256 collateralAmount);
}
interface WethLike {
function deposit() external payable;
}
function wrapEthAndDepositCollateral(AcmeLike acme) external payable override {
uint256 depositAmount = msg.value;
// Convert the received ETH to WETH.
WethLike weth = WethLike(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
weth.deposit{ value: depositAmount }();
// Deposit the WETH as collateral into the Acme DeFi protocol.
acme.depositCollateral(address(weth), depositAmount);
}
Sablier V2 Periphery has been designed to work with PRBProxy. Let's use the batchCreateWithDurations
function from Sablier's target contract as an example:
- The goal is to create a batch of streams
- This is achieved by having the user pass a data array called
batch
- The total ERC-20 transfer amount is calculated so that only one transfer is made
- The ERC-20 tokens are transferred to the proxy
- The streams are created sequentially by interacting with Sablier V2 Core
- For each stream, the ERC-20 tokens are sourced from the proxy
function batchCreateWithDurations(
ISablierV2LockupLinear lockupLinear,
IERC20 asset,
Batch.CreateWithDurations[] calldata batch,
Permit2Params calldata permit2Params
)
external
override
onlyDelegateCall
returns (uint256[] memory streamIds)
{
// Check that the batch size is not zero.
uint256 batchSize = batch.length;
if (batchSize == 0) {
revert Errors.SablierV2ProxyTarget_BatchSizeZero();
}
// Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create
// transactions will revert if there is overflow.
uint256 i;
uint160 transferAmount;
for (i = 0; i < batchSize;) {
unchecked {
transferAmount += batch[i].totalAmount;
i += 1;
}
}
// Transfers the assets to the proxy and approves the Sablier contract to spend them.
_transferAndApprove(address(lockupLinear), asset, transferAmount, permit2Params);
// Create a stream for each element in the parameter array.
streamIds = new uint256[](batchSize);
for (i = 0; i < batchSize;) {
// Create the stream.
streamIds[i] = lockupLinear.createWithDurations(
LockupLinear.CreateWithDurations({
asset: asset,
broker: batch[i].broker,
cancelable: batch[i].cancelable,
durations: batch[i].durations,
recipient: batch[i].recipient,
sender: batch[i].sender,
totalAmount: batch[i].totalAmount
})
);
// Increment the for loop iterator.
unchecked {
i += 1;
}
}
}
For more proxy-related Sablier examples, see the SablierV2ProxyTarget contract.
For more details about Sablier, see the docs.