Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy plugin #27

Merged
merged 6 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/v2-core
41 changes: 40 additions & 1 deletion src/SablierV2ProxyTarget.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ pragma solidity >=0.8.19;

import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import { IPRBProxy } from "@prb/proxy/interfaces/IPRBProxy.sol";
import { IPRBProxyPlugin } from "@prb/proxy/interfaces/IPRBProxyPlugin.sol";
import { ISablierV2Lockup } from "@sablier/v2-core/interfaces/ISablierV2Lockup.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupDynamic } from "@sablier/v2-core/interfaces/ISablierV2LockupDynamic.sol";
import { ISablierV2LockupSender } from "@sablier/v2-core/interfaces/hooks/ISablierV2LockupSender.sol";
import { LockupDynamic, LockupLinear } from "@sablier/v2-core/types/DataTypes.sol";
import { IAllowanceTransfer } from "permit2/interfaces/IAllowanceTransfer.sol";

Expand Down Expand Up @@ -34,7 +37,7 @@ import { Batch, Permit2Params } from "./types/DataTypes.sol";

/// @title SablierV2ProxyTarget
/// @notice Implements the {ISablierV2ProxyTarget} interface.
contract SablierV2ProxyTarget is ISablierV2ProxyTarget {
contract SablierV2ProxyTarget is ISablierV2ProxyTarget, IPRBProxyPlugin, ISablierV2LockupSender {
using SafeERC20 for IERC20;

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -109,6 +112,31 @@ contract SablierV2ProxyTarget is ISablierV2ProxyTarget {
_postCancelMultiple(initialBalances, assets);
}

/// @inheritdoc ISablierV2LockupSender
/// @dev This function is necessary to automatically redirect the funds to the sender, i.e. the proxy owner, when
/// recipients trigger cancellations.
function onStreamCanceled(
ISablierV2Lockup lockup,
uint256 streamId,
address recipient,
uint128 senderAmount,
uint128 recipientAmount
)
external
{
recipient; // silence the warning
recipientAmount; // silence the warning

IERC20 asset = lockup.getAsset(streamId);

// The `lockup` contract will have the proxy contract set as the sender.
address proxy = lockup.getSender(streamId);
address owner = IPRBProxy(proxy).owner();

// Transfer the funds from the proxy contract to the sender.
asset.safeTransfer({ to: owner, value: senderAmount });
}

/// @inheritdoc ISablierV2ProxyTarget
function renounce(ISablierV2Lockup lockup, uint256 streamId) external {
lockup.renounce(streamId);
Expand Down Expand Up @@ -561,6 +589,17 @@ contract SablierV2ProxyTarget is ISablierV2ProxyTarget {
streamId = dynamic.createWithMilestones(params);
}

/*//////////////////////////////////////////////////////////////////////////
PROXY-PLUGIN
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc IPRBProxyPlugin
function methodList() external pure returns (bytes4[] memory methods) {
bytes4[] memory functionSig = new bytes4[](1);
functionSig[0] = this.onStreamCanceled.selector;
methods = functionSig;
}

/*//////////////////////////////////////////////////////////////////////////
HELPER FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
12 changes: 10 additions & 2 deletions test/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.19 <0.9.0;
import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { IPRBProxy } from "@prb/proxy/interfaces/IPRBProxy.sol";
import { PRBProxyHelpers } from "@prb/proxy/PRBProxyHelpers.sol";
import { PRBProxyRegistry } from "@prb/proxy/PRBProxyRegistry.sol";
import { SablierV2Comptroller } from "@sablier/v2-core/SablierV2Comptroller.sol";
import { SablierV2LockupLinear } from "@sablier/v2-core/SablierV2LockupLinear.sol";
Expand Down Expand Up @@ -33,6 +34,7 @@ abstract contract Base_Test is Assertions, StdCheats {
//////////////////////////////////////////////////////////////////////////*/

bytes32 internal immutable DOMAIN_SEPARATOR;
bytes onStreamCanceledData;

/*//////////////////////////////////////////////////////////////////////////
VARIABLES
Expand All @@ -48,10 +50,11 @@ abstract contract Base_Test is Assertions, StdCheats {
IERC20 internal dai = new ERC20("Dai Stablecoin", "DAI");
Defaults internal defaults;
SablierV2LockupDynamic internal dynamic;
SablierV2LockupLinear internal linear;
SablierV2NFTDescriptor internal nftDescriptor = new SablierV2NFTDescriptor();
AllowanceTransfer internal permit2 = new AllowanceTransfer();
IPRBProxy internal proxy;
SablierV2LockupLinear internal linear;
PRBProxyHelpers internal proxyHelpers = new PRBProxyHelpers();
SablierV2ProxyTarget internal target = new SablierV2ProxyTarget(permit2);
WETH internal weth = new WETH();

Expand All @@ -61,6 +64,7 @@ abstract contract Base_Test is Assertions, StdCheats {

constructor() {
DOMAIN_SEPARATOR = permit2.DOMAIN_SEPARATOR();
onStreamCanceledData = abi.encodeCall(proxyHelpers.installPlugin, (target));
}

/*//////////////////////////////////////////////////////////////////////////
Expand All @@ -75,7 +79,11 @@ abstract contract Base_Test is Assertions, StdCheats {
users.sender = createUser("Sender");

// Deploy the sender's proxy contract.
proxy = new PRBProxyRegistry().deployFor(users.sender.addr);
(proxy,) = new PRBProxyRegistry().deployAndExecuteFor({
owner: users.sender.addr,
target: address(proxyHelpers),
data: onStreamCanceledData
});

// Deploy the defaults contract.
defaults = new Defaults(users, proxy, dai);
Expand Down
27 changes: 19 additions & 8 deletions test/helpers/Assertions.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ import { PRBTest } from "@prb/test/PRBTest.sol";
import { Lockup } from "@sablier/v2-core/types/DataTypes.sol";

abstract contract Assertions is PRBTest {
event LogArray(Lockup.Status[] value);
event LogNamedArray(string key, Lockup.Status[] value);
event LogArray(bytes4[] value);
event LogNamedArray(string key, bytes4[] value);

/// @dev Compares two `Lockup.Status` enum values.
function assertEq(Lockup.Status a, Lockup.Status b) internal {
assertEq(uint8(a), uint8(b), "status");
/// @dev Compares two `bytes4[]` arrays.
function assertEq(bytes4[] memory a, bytes4[] memory b) internal {
if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) {
emit Log("Error: a == b not satisfied [bytes4[]]");
emit LogNamedArray(" Left", a);
emit LogNamedArray(" Right", b);
fail();
}
}

/// @dev Compares two `Lockup.Status[]` enum arrays.
function assertEq(Lockup.Status[] memory a, Lockup.Status[] memory b) internal {
/// @dev Compares two `bytes4[]` arrays.
function assertEq(bytes4[] memory a, bytes4[] memory b, string memory err) internal {
if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) {
emit Log("Error: a == b not satisfied [Lockup.Status[]]");
emit LogNamedString("Error", err);
emit Log("Error: a == b not satisfied [bytes4[]]");
emit LogNamedArray(" Left", a);
emit LogNamedArray(" Right", b);
fail();
}
}

/// @dev Compares two `Lockup.Status` enum values.
function assertEq(Lockup.Status a, Lockup.Status b) internal {
assertEq(uint8(a), uint8(b), "status");
}

/// @dev Compares two `Lockup.Status` enum values.
function assertEq(Lockup.Status a, Lockup.Status b, string memory err) internal {
assertEq(uint8(a), uint8(b), err);
Expand Down
17 changes: 17 additions & 0 deletions test/unit/method-list/methodList.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <0.9.0;

import { ISablierV2LockupSender } from "@sablier/v2-core/interfaces/hooks/ISablierV2LockupSender.sol";

import { Base_Test } from "../../Base.t.sol";

contract MethodList_Unit_Test is Base_Test {
function test_MethodList() external {
bytes4[] memory functionSig = new bytes4[](1);
functionSig[0] = ISablierV2LockupSender.onStreamCanceled.selector;

bytes4[] memory actualMethodList = target.methodList();
bytes4[] memory expectedMethodList = functionSig;
assertEq(actualMethodList, expectedMethodList, "method list does not match");
}
}
30 changes: 30 additions & 0 deletions test/unit/on-stream-canceled/onStreamCanceled.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <0.9.0;

import { Base_Test } from "../../Base.t.sol";

contract OnStreamCanceled_Unit_Test is Base_Test {
function test_OnStreamCanceled() external {
uint256 streamId = createWithRange();

// Warp into the future.
vm.warp(defaults.WARP_26_PERCENT());

// Make the `recipient` the caller.
changePrank(users.recipient.addr);

uint256 balanceBefore = dai.balanceOf(users.sender.addr);

// Asset flow: Sablier contract → proxy → proxy owner
// Expect transfers from the Sablier contract to the proxy, and then from the proxy to the proxy owner.
expectCallToTransfer({ to: address(proxy), amount: defaults.REFUND_AMOUNT() });
expectCallToTransfer({ to: users.sender.addr, amount: defaults.REFUND_AMOUNT() });

// Cancel the stream.
linear.cancel(streamId);

uint256 actualBalance = dai.balanceOf(users.sender.addr);
uint256 expectedBalance = balanceBefore + defaults.REFUND_AMOUNT();
assertEq(actualBalance, expectedBalance, "balance does not match");
}
}