Skip to content

Commit

Permalink
Merge pull request #6 from Se7en-Seas/feat/swell-simple-staking-adaptor
Browse files Browse the repository at this point in the history
Feat/swell simple staking adaptor
  • Loading branch information
crispymangoes committed Jun 4, 2024
2 parents 897c9bf + 35e01cd commit e939808
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 0 deletions.
40 changes: 40 additions & 0 deletions src/interfaces/external/SwellSimpleStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

interface SimpleStakingERC20 {
struct Supported {
bool deposit;
bool withdraw;
}

error ADDRESS_NULL();
error AMOUNT_NULL();
error AddressEmptyCode(address target);
error AddressInsufficientBalance(address account);
error FailedInnerCall();
error INSUFFICIENT_BALANCE();
error OwnableInvalidOwner(address owner);
error OwnableUnauthorizedAccount(address account);
error ReentrancyGuardReentrantCall();
error SafeERC20FailedOperation(address token);
error TOKEN_NOT_ALLOWED(address token);

event Deposit(address indexed token, address indexed staker, uint256 amount);
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event SupportedToken(address indexed token, Supported supported);
event Withdraw(address indexed token, address indexed staker, uint256 amount);

function acceptOwnership() external;
function deposit(address _token, uint256 _amount, address _receiver) external;
function owner() external view returns (address);
function pendingOwner() external view returns (address);
function renounceOwnership() external;
function rescueERC20(address _token) external;
function stakedBalances(address, address) external view returns (uint256);
function supportToken(address _token, Supported memory _supported) external;
function supportedTokens(address) external view returns (bool deposit, bool withdraw);
function totalStakedBalance(address) external view returns (uint256);
function transferOwnership(address newOwner) external;
function withdraw(address _token, uint256 _amount, address _receiver) external;
}
155 changes: 155 additions & 0 deletions src/modules/adaptors/Staking/SwellSimpleStakingAdaptor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import {BaseAdaptor, ERC20, SafeTransferLib} from "src/modules/adaptors/BaseAdaptor.sol";
import {SimpleStakingERC20 as SwellSimpleStaking} from "src/interfaces/external/SwellSimpleStaking.sol";

/**
* @title Swell Simple Staking Adaptor
* @notice Allows Cellars to stake with Swell Simple Staking.
* @author crispymangoes
*/
contract SwellSimpleStakingAdaptor is BaseAdaptor {
using SafeTransferLib for ERC20;

//==================== Adaptor Data Specification ====================
// adaptorData = abi.encode(ERC20 token)
// Where:
// `token` is the underlying ERC20 used with the Swell Simple Staking contract
//================= Configuration Data Specification =================
// configurationData = abi.encode(bool isLiquid)
// Where:
// `isLiquid` dictates whether the position is liquid or not
// If true:
// position can support use withdraws
// else:
// position can not support user withdraws
//
//====================================================================

/**
* @notice The Swell Simple Staking contract staking calls are made to.
*/
SwellSimpleStaking internal immutable swellSimpleStaking;

constructor(address _swellSimpleStaking) {
swellSimpleStaking = SwellSimpleStaking(_swellSimpleStaking);
}

//============================================ Global Functions ===========================================
/**
* @dev Identifier unique to this adaptor for a shared registry.
* Normally the identifier would just be the address of this contract, but this
* Identifier is needed during Cellar Delegate Call Operations, so getting the address
* of the adaptor is more difficult.
*/
function identifier() public pure virtual override returns (bytes32) {
return keccak256(abi.encode("Swell Simple Staking Adaptor V 0.0"));
}

//============================================ Implement Base Functions ===========================================
/**
* @notice Not supported.
*/
function deposit(uint256, bytes memory, bytes memory) public virtual override {
revert BaseAdaptor__UserDepositsNotAllowed();
}

/**
* @notice Cellar needs to withdraw ERC20's from SwellSimpleStaking.
* @dev Important to verify that external receivers are allowed if receiver is not Cellar address.
* @param assets the amount of assets to withdraw from the SwellSimpleStaking position
* @param receiver address to send assets to'
* @param adaptorData data needed to withdraw from the SwellSimpleStaking position
* @param configurationData abi encoded bool indicating whether the position is liquid or not
*/
function withdraw(uint256 assets, address receiver, bytes memory adaptorData, bytes memory configurationData)
public
virtual
override
{
// Check that position is setup to be liquid.
bool isLiquid = abi.decode(configurationData, (bool));
if (!isLiquid) revert BaseAdaptor__UserWithdrawsNotAllowed();

// Run external receiver check.
_externalReceiverCheck(receiver);

address token = abi.decode(adaptorData, (address));

// Withdraw assets from `Vault`.
swellSimpleStaking.withdraw(token, assets, receiver);
}

/**
* @notice Check if position is liquid, then return the amount of assets that can be withdrawn.
*/
function withdrawableFrom(bytes memory adaptorData, bytes memory configurationData)
public
view
virtual
override
returns (uint256)
{
bool isLiquid = abi.decode(configurationData, (bool));
if (isLiquid) {
address token = abi.decode(adaptorData, (address));
return swellSimpleStaking.stakedBalances(msg.sender, token);
} else {
return 0;
}
}

/**
* @notice Call `stakedBalances` to get the balance of the Cellar.
*/
function balanceOf(bytes memory adaptorData) public view virtual override returns (uint256) {
address token = abi.decode(adaptorData, (address));
return swellSimpleStaking.stakedBalances(msg.sender, token);
}

/**
* @notice Returns the token in the adaptorData
*/
function assetOf(bytes memory adaptorData) public view virtual override returns (ERC20) {
ERC20 token = abi.decode(adaptorData, (ERC20));
return token;
}

/**
* @notice This adaptor returns collateral, and not debt.
*/
function isDebt() public pure virtual override returns (bool) {
return false;
}

//============================================ Strategist Functions ===========================================

/**
* @notice Deposits ERC20 tokens into the Swell Simple Staking contract.
* @dev We are not checking if the position is tracked for simplicity,
* this is safe to do since the SwellSimpleStaking contract is immutable,
* so we know the Cellar is interacting with a safe contract.
*/
function depositIntoSimpleStaking(ERC20 token, uint256 amount) external {
amount = _maxAvailable(token, amount);
token.safeApprove(address(swellSimpleStaking), amount);
swellSimpleStaking.deposit(address(token), amount, address(this));

// Zero out approvals if necessary.
_revokeExternalApproval(token, address(swellSimpleStaking));
}

/**
* @notice Withdraws ERC20 tokens from the Swell Simple Staking contract.
* @dev We are not checking if the position is tracked for simplicity,
* this is safe to do since the SwellSimpleStaking contract is immutable,
* so we know the Cellar is interacting with a safe contract.
*/
function withdrawFromSimpleStaking(ERC20 token, uint256 amount) external {
if (amount == type(uint256).max) {
amount = swellSimpleStaking.stakedBalances(address(this), address(token));
}
swellSimpleStaking.withdraw(address(token), amount, address(this));
}
}
3 changes: 3 additions & 0 deletions test/resources/MainnetAddresses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,7 @@ contract MainnetAddresses {
address public pendleEethYtDecember = 0x129e6B5DBC0Ecc12F9e486C5BC9cDF1a6A80bc6A;

address public pendleSwethMarket = 0x0e1C5509B503358eA1Dac119C1D413e28Cc4b303;

// Swell Simple Staking
address public swellSimpleStaking = 0x38D43a6Cb8DA0E855A42fB6b0733A0498531d774;
}
175 changes: 175 additions & 0 deletions test/testAdaptors/SwellSimpleStakingAdaptor.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {SwellSimpleStakingAdaptor} from "src/modules/adaptors/Staking/SwellSimpleStakingAdaptor.sol";
import {SimpleStakingERC20 as SwellSimpleStaking} from "src/interfaces/external/SwellSimpleStaking.sol";

// Import Everything from Starter file.
import "test/resources/MainnetStarter.t.sol";

import {AdaptorHelperFunctions} from "test/resources/AdaptorHelperFunctions.sol";

contract SwellSimpleStakingAdaptorTest is MainnetStarterTest, AdaptorHelperFunctions {
using SafeTransferLib for ERC20;
using Math for uint256;
using stdStorage for StdStorage;
using Address for address;

// LiquidV1
Cellar private cellar = Cellar(0xeA1A6307D9b18F8d1cbf1c3Dd6aad8416C06a221);

SwellSimpleStakingAdaptor private swellSimpleStakingAdaptor;

address private cellarOwner;
address private registryOwner;

uint32 private ptEEthSwellPosition = 1_000_001;
uint32 private weEthSwellPosition = 1_000_002;

uint256 initialAssets;

function setUp() external {
// Setup forked environment.
string memory rpcKey = "MAINNET_RPC_URL";
uint256 blockNumber = 19969336;
_startFork(rpcKey, blockNumber);

swellSimpleStakingAdaptor = new SwellSimpleStakingAdaptor(swellSimpleStaking);

registry = Registry(0x37912f4c0F0d916890eBD755BF6d1f0A0e059BbD);
priceRouter = PriceRouter(cellar.priceRouter());
cellarOwner = cellar.owner();
registryOwner = registry.owner();

vm.startPrank(registryOwner);
registry.trustAdaptor(address(swellSimpleStakingAdaptor));
registry.trustPosition(ptEEthSwellPosition, address(swellSimpleStakingAdaptor), abi.encode(pendleEethPt));
registry.trustPosition(weEthSwellPosition, address(swellSimpleStakingAdaptor), abi.encode(WEETH));
vm.stopPrank();

vm.startPrank(cellarOwner);
cellar.addAdaptorToCatalogue(address(swellSimpleStakingAdaptor));
cellar.addPositionToCatalogue(ptEEthSwellPosition);
cellar.addPositionToCatalogue(weEthSwellPosition);
vm.stopPrank();

initialAssets = cellar.totalAssets();
}

function testLogic() external {
// Add both positions to the cellar, making the pt eETH one illiquid, but the weETH one liquid.
vm.startPrank(cellarOwner);
cellar.addPosition(0, ptEEthSwellPosition, abi.encode(false), false);
cellar.addPosition(0, weEthSwellPosition, abi.encode(true), false);
vm.stopPrank();

uint256 ptEthInCellar = ERC20(pendleEethPt).balanceOf(address(cellar));
uint256 weETHInCellar = WEETH.balanceOf(address(cellar));

// Move 100 eETH and 100 weETH into Swell Simple Staking.
Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1);
bytes[] memory adaptorCalls = new bytes[](2);
adaptorCalls[0] =
abi.encodeWithSelector(SwellSimpleStakingAdaptor.depositIntoSimpleStaking.selector, pendleEethPt, 100e18);
adaptorCalls[1] =
abi.encodeWithSelector(SwellSimpleStakingAdaptor.depositIntoSimpleStaking.selector, WEETH, 100e18);

data[0] = Cellar.AdaptorCall({adaptor: address(swellSimpleStakingAdaptor), callData: adaptorCalls});
vm.startPrank(cellarOwner);
cellar.callOnAdaptor(data);
vm.stopPrank();

uint256 expectedWithdrawableAssets = priceRouter.getValue(WEETH, 100e18, WETH);

assertEq(
cellar.totalAssetsWithdrawable(),
expectedWithdrawableAssets,
"Only assets in the weETH Simple Staking position should be withdrawable"
);

assertApproxEqAbs(cellar.totalAssets(), initialAssets, 1, "The total assets in the cellar should be unchanged");

// Use max available to deposit all assets.
data = new Cellar.AdaptorCall[](1);
adaptorCalls = new bytes[](2);
adaptorCalls[0] = abi.encodeWithSelector(
SwellSimpleStakingAdaptor.depositIntoSimpleStaking.selector, pendleEethPt, type(uint256).max
);
adaptorCalls[1] = abi.encodeWithSelector(
SwellSimpleStakingAdaptor.depositIntoSimpleStaking.selector, WEETH, type(uint256).max
);

data[0] = Cellar.AdaptorCall({adaptor: address(swellSimpleStakingAdaptor), callData: adaptorCalls});
vm.startPrank(cellarOwner);
cellar.callOnAdaptor(data);
vm.stopPrank();

expectedWithdrawableAssets = priceRouter.getValue(WEETH, weETHInCellar, WETH);

assertEq(
cellar.totalAssetsWithdrawable(),
expectedWithdrawableAssets,
"Only assets in the weETH Simple Staking position should be withdrawable"
);

assertApproxEqAbs(cellar.totalAssets(), initialAssets, 1, "The total assets in the cellar should be unchanged");

// Confirm all were deposited.
assertEq(ERC20(pendleEethPt).balanceOf(address(cellar)), 0, "Cellar should have no pt eETH.");
assertEq(WEETH.balanceOf(address(cellar)), 0, "Cellar should have no weETH.");

address user = vm.addr(1);

// Have user withdraw and make sure they get weETH.
deal(address(cellar), user, 100e18, true);

vm.startPrank(user);
cellar.redeem(100e18, user, user);
vm.stopPrank();

uint256 expectedWeEthToUser = priceRouter.getValue(WETH, cellar.previewRedeem(100e18), WEETH);

assertApproxEqAbs(WEETH.balanceOf(user), expectedWeEthToUser, 1, "User should have received weETH");

// Use withdraw to withdraw 100 of each
data = new Cellar.AdaptorCall[](1);
adaptorCalls = new bytes[](2);
adaptorCalls[0] =
abi.encodeWithSelector(SwellSimpleStakingAdaptor.withdrawFromSimpleStaking.selector, pendleEethPt, 100e18);
adaptorCalls[1] =
abi.encodeWithSelector(SwellSimpleStakingAdaptor.withdrawFromSimpleStaking.selector, WEETH, 100e18);

data[0] = Cellar.AdaptorCall({adaptor: address(swellSimpleStakingAdaptor), callData: adaptorCalls});
vm.startPrank(cellarOwner);
cellar.callOnAdaptor(data);
vm.stopPrank();

// Confirm 100 was withdrawn
assertEq(ERC20(pendleEethPt).balanceOf(address(cellar)), 100e18, "Cellar should have 100e18 pt eETH.");
assertEq(WEETH.balanceOf(address(cellar)), 100e18, "Cellar should have 100e18 weETH.");

// Use max available to withdraw all assets
data = new Cellar.AdaptorCall[](1);
adaptorCalls = new bytes[](2);
adaptorCalls[0] = abi.encodeWithSelector(
SwellSimpleStakingAdaptor.withdrawFromSimpleStaking.selector, pendleEethPt, type(uint256).max
);
adaptorCalls[1] = abi.encodeWithSelector(
SwellSimpleStakingAdaptor.withdrawFromSimpleStaking.selector, WEETH, type(uint256).max
);

data[0] = Cellar.AdaptorCall({adaptor: address(swellSimpleStakingAdaptor), callData: adaptorCalls});
vm.startPrank(cellarOwner);
cellar.callOnAdaptor(data);
vm.stopPrank();

// Confirm all assets were withdrawn
assertEq(ERC20(pendleEethPt).balanceOf(address(cellar)), ptEthInCellar, "Cellar should have starting pt eETH.");
assertEq(
WEETH.balanceOf(address(cellar)),
weETHInCellar - expectedWeEthToUser,
"Cellar should have starting weETH, minus withdrawn amount"
);
}
}

0 comments on commit e939808

Please sign in to comment.