Skip to content

Targets

Paul Razvan Berg edited this page Aug 10, 2023 · 6 revisions

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.

Diagram

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];
Loading

ABI Encoding

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.

Solidity

pragma solidity >=0.8.19;

function encode() pure returns (bytes memory) {
    bytes memory data = abi.encodeWithSignature("foo(uint256)", 1337);
    return data;
}

Cast

$ cast abi-encode "foo(uint256)" 1337

Viem

TODO

Examples

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.

Withdraw ETH and ERC-20

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 and Deposit

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);
}

Batch Create Sablier V2 Streams

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.

Clone this wiki locally