-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from Se7en-Seas/feat/swell-simple-staking-adaptor
Feat/swell simple staking adaptor
- Loading branch information
Showing
4 changed files
with
373 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
155
src/modules/adaptors/Staking/SwellSimpleStakingAdaptor.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
); | ||
} | ||
} |