From e27b6b10c78903e4035e5166eb0c7e48703b2df5 Mon Sep 17 00:00:00 2001 From: Shriya Tyagi Date: Mon, 8 Jul 2024 02:39:28 +0530 Subject: [PATCH] feat: add fluid connectors on arbitrum --- .../connectors/fluid-staking/events.sol | 28 ++ .../connectors/fluid-staking/interface.sol | 13 + .../connectors/fluid-staking/main.sol | 116 ++++++++ .../arbitrum/connectors/fluid/events.sol | 20 ++ .../arbitrum/connectors/fluid/interface.sol | 47 +++ contracts/arbitrum/connectors/fluid/main.sol | 267 ++++++++++++++++++ 6 files changed, 491 insertions(+) create mode 100644 contracts/arbitrum/connectors/fluid-staking/events.sol create mode 100644 contracts/arbitrum/connectors/fluid-staking/interface.sol create mode 100644 contracts/arbitrum/connectors/fluid-staking/main.sol create mode 100644 contracts/arbitrum/connectors/fluid/events.sol create mode 100644 contracts/arbitrum/connectors/fluid/interface.sol create mode 100644 contracts/arbitrum/connectors/fluid/main.sol diff --git a/contracts/arbitrum/connectors/fluid-staking/events.sol b/contracts/arbitrum/connectors/fluid-staking/events.sol new file mode 100644 index 0000000..f42acd6 --- /dev/null +++ b/contracts/arbitrum/connectors/fluid-staking/events.sol @@ -0,0 +1,28 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +contract Events { + + event LogDeposit( + address indexed stakingPool, + uint256 amount, + uint getId, + uint setId + ); + + event LogWithdrawAndClaimedReward( + address indexed stakingPool, + uint256 amount, + uint256 rewardAmt, + uint getId, + uint setIdAmount, + uint setIdReward + ); + + event LogClaimedReward( + address indexed stakingPool, + address indexed rewardToken, + uint256 rewardAmt, + uint setId + ); +} \ No newline at end of file diff --git a/contracts/arbitrum/connectors/fluid-staking/interface.sol b/contracts/arbitrum/connectors/fluid-staking/interface.sol new file mode 100644 index 0000000..7323473 --- /dev/null +++ b/contracts/arbitrum/connectors/fluid-staking/interface.sol @@ -0,0 +1,13 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IStakingRewards { + function stake(uint256 amount) external; + function withdraw(uint256 amount) external; + function getReward() external; + function balanceOf(address account) external view returns(uint256); + function rewardsToken() external view returns (IERC20); + function stakingToken() external view returns (IERC20); +} diff --git a/contracts/arbitrum/connectors/fluid-staking/main.sol b/contracts/arbitrum/connectors/fluid-staking/main.sol new file mode 100644 index 0000000..eb8caea --- /dev/null +++ b/contracts/arbitrum/connectors/fluid-staking/main.sol @@ -0,0 +1,116 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +/** + * @title Fluid Staking. + * @dev Stake Fluid for earning rewards. + */ + +import { TokenInterface } from "../../common/interfaces.sol"; +import { Stores } from "../../common/stores.sol"; +import { Basic } from "../../common/basic.sol"; +import { Events } from "./events.sol"; +import { IStakingRewards } from "./interface.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Main is Basic, Events { + + /** + * @dev Deposit ERC20. + * @notice Deposit Tokens to staking pool. + * @param stakingPool staking pool address. + * @param amt staking token amount. + * @param getId ID to retrieve amount. + * @param setId ID stores the amount of staked tokens. + */ + function deposit( + address stakingPool, + uint amt, + uint getId, + uint setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amt); + + IStakingRewards stakingContract = IStakingRewards(stakingPool); + IERC20 stakingTokenContract = stakingContract.stakingToken(); + + _amt = _amt == type(uint256).max + ? stakingTokenContract.balanceOf(address(this)) + : _amt; + + approve(TokenInterface(address(stakingTokenContract)), address(stakingContract), _amt); + stakingContract.stake(_amt); + + setUint(setId, _amt); + _eventName = "LogDeposit(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(stakingPool, _amt, getId, setId); + } + + /** + * @dev Withdraw ERC20. + * @notice Withdraw Tokens from the staking pool. + * @param stakingPool staking pool address. + * @param amt staking token amount. + * @param getId ID to retrieve amount. + * @param setIdAmount ID stores the amount of stake tokens withdrawn. + * @param setIdReward ID stores the amount of reward tokens claimed. + */ + function withdraw( + address stakingPool, + uint amt, + uint getId, + uint setIdAmount, + uint setIdReward + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amt); + + IStakingRewards stakingContract = IStakingRewards(stakingPool); + IERC20 rewardsToken = stakingContract.rewardsToken(); + + _amt = _amt == type(uint256).max + ? stakingContract.balanceOf(address(this)) + : _amt; + + uint intialBal = rewardsToken.balanceOf(address(this)); + stakingContract.withdraw(_amt); + stakingContract.getReward(); + + uint rewardAmt = rewardsToken.balanceOf(address(this)) - intialBal; + + setUint(setIdAmount, _amt); + setUint(setIdReward, rewardAmt); + { + _eventName = "LogWithdrawAndClaimedReward(address,uint256,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(stakingPool, _amt, rewardAmt, getId, setIdAmount, setIdReward); + } + } + + /** + * @dev Claim Reward. + * @notice Claim Pending Rewards of tokens staked. + * @param stakingPool staking pool address. + * @param setId ID stores the amount of reward tokens claimed. + */ + function claimReward( + address stakingPool, + uint setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + IStakingRewards stakingContract = IStakingRewards(stakingPool); + IERC20 rewardsToken = stakingContract.rewardsToken(); + + uint intialBal = rewardsToken.balanceOf(address(this)); + stakingContract.getReward(); + uint finalBal = rewardsToken.balanceOf(address(this)); + + uint rewardAmt = finalBal - intialBal; + + setUint(setId, rewardAmt); + _eventName = "LogClaimedReward(address,address,uint256,uint256)"; + _eventParam = abi.encode(stakingPool, address(rewardsToken), rewardAmt, setId); + } + +} + +contract ConnectV2StakeFluidArbitrum is Main { + string public constant name = "Stake-Fluid-v1.0"; +} \ No newline at end of file diff --git a/contracts/arbitrum/connectors/fluid/events.sol b/contracts/arbitrum/connectors/fluid/events.sol new file mode 100644 index 0000000..c0055ef --- /dev/null +++ b/contracts/arbitrum/connectors/fluid/events.sol @@ -0,0 +1,20 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +contract Events { + event LogOperate( + address vaultAddress, + uint256 nftId, + int256 newCol, + int256 newDebt + ); + + event LogOperateWithIds( + address vaultAddress, + uint256 nftId, + int256 newCol, + int256 newDebt, + uint256[] getIds, + uint256[] setIds + ); +} diff --git a/contracts/arbitrum/connectors/fluid/interface.sol b/contracts/arbitrum/connectors/fluid/interface.sol new file mode 100644 index 0000000..0d1a73c --- /dev/null +++ b/contracts/arbitrum/connectors/fluid/interface.sol @@ -0,0 +1,47 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +interface IVault { + /// @dev Single function which handles supply, withdraw, borrow & payback + /// @param nftId_ NFT ID for interaction. If 0 then create new NFT/position. + /// @param newCol_ new collateral. If positive then deposit, if negative then withdraw, if 0 then do nohing + /// @param newDebt_ new debt. If positive then borrow, if negative then payback, if 0 then do nohing + /// @param to_ address where withdraw or borrow should go. If address(0) then msg.sender + /// @return nftId_ if 0 then this returns the newly created NFT Id else returns the same NFT ID + /// @return final supply amount. Mainly if max withdraw using type(int).min then this is useful to get perfect amount else remain same as newCol_ + /// @return final borrow amount. Mainly if max payback using type(int).min then this is useful to get perfect amount else remain same as newDebt_ + function operate( + uint256 nftId_, // if 0 then new position + int256 newCol_, // if negative then withdraw + int256 newDebt_, // if negative then payback + address to_ // address at which the borrow & withdraw amount should go to. If address(0) then it'll go to msg.sender + ) + external + payable + returns ( + uint256, // nftId_ + int256, // final supply amount if - then withdraw + int256 // final borrow amount if - then payback + ); + + struct ConstantViews { + address liquidity; + address factory; + address adminImplementation; + address secondaryImplementation; + address supplyToken; + address borrowToken; + uint8 supplyDecimals; + uint8 borrowDecimals; + uint vaultId; + bytes32 liquiditySupplyExchangePriceSlot; + bytes32 liquidityBorrowExchangePriceSlot; + bytes32 liquidityUserSupplySlot; + bytes32 liquidityUserBorrowSlot; + } + + function constantsView() + external + view + returns (ConstantViews memory constantsView_); +} diff --git a/contracts/arbitrum/connectors/fluid/main.sol b/contracts/arbitrum/connectors/fluid/main.sol new file mode 100644 index 0000000..803d23e --- /dev/null +++ b/contracts/arbitrum/connectors/fluid/main.sol @@ -0,0 +1,267 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +/** + * @title Fluid. + * @dev Lending & Borrowing. + */ + +import {Basic} from "../../common/basic.sol"; +import {TokenInterface} from "../../common/interfaces.sol"; + +import {Events} from "./events.sol"; +import {IVault} from "./interface.sol"; + +abstract contract FluidConnector is Events, Basic { + /** + * @dev Returns Eth address + */ + function getEthAddr() internal pure returns (address) { + return 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + } + + /** + * @dev Deposit, borrow, payback and withdraw asset from the vault. + * @notice Single function which handles supply, withdraw, borrow & payback + * @param vaultAddress_ Vault address. + * @param nftId_ NFT ID for interaction. If 0 then create new NFT/position. + * @param newCol_ New collateral. If positive then deposit, if negative then withdraw, if 0 then do nothing. + * For max deposit use type(uint25).max, for max withdraw use type(uint25).min. + * @param newDebt_ New debt. If positive then borrow, if negative then payback, if 0 then do nothing + * For max payback use type(uint25).min. + * @param repayApproveAmt_ In case of max amount for payback, this amount will be approved for spending. + * Should always be positive. + * @param getIds_ Array of 5 elements to retrieve IDs: + * Nft Id, Supply amount, Withdraw amount, Borrow Amount, Payback Amount + * @param setIds_ Array of 5 elements to store IDs generated: + * Nft Id, Supply amount, Withdraw amount, Borrow Amount, Payback Amount + */ + function operateWithIds( + address vaultAddress_, + uint256 nftId_, + int256 newCol_, + int256 newDebt_, + uint256 repayApproveAmt_, + uint256[] memory getIds_, + uint256[] memory setIds_ + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + if (getIds_[1] > 0 && getIds_[2] > 0) { + revert("Supply and withdraw get IDs cannot both be > 0."); + } + + if (getIds_[3] > 0 && getIds_[4] > 0) { + revert("Borrow and payback get IDs cannot both be > 0."); + } + + if (setIds_[1] > 0 && setIds_[2] > 0) { + revert("Supply and withdraw set IDs cannot both be > 0."); + } + + if (setIds_[3] > 0 && setIds_[4] > 0) { + revert("Borrow and payback set IDs cannot both be > 0."); + } + + nftId_ = getUint(getIds_[0], nftId_); + + newCol_ = getIds_[1] > 0 + ? int256(getUint(getIds_[1], uint256(newCol_))) + : getIds_[2] > 0 + ? -int256(getUint(getIds_[2], uint256(newCol_))) + : newCol_; + + newDebt_ = getIds_[3] > 0 + ? int256(getUint(getIds_[3], uint256(newDebt_))) + : getIds_[4] > 0 + ? -int256(getUint(getIds_[4], uint256(newDebt_))) + : newDebt_; + + IVault vault_ = IVault(vaultAddress_); + + IVault.ConstantViews memory vaultDetails_ = vault_.constantsView(); + + uint256 ethAmount_; + + bool isColMax_ = newCol_ == type(int256).max; + + // Deposit + if (newCol_ > 0) { + if (vaultDetails_.supplyToken == getEthAddr()) { + ethAmount_ = isColMax_ + ? address(this).balance + : uint256(newCol_); + + newCol_ = int256(ethAmount_); + } else { + if (isColMax_) { + newCol_ = int256( + TokenInterface(vaultDetails_.supplyToken).balanceOf( + address(this) + ) + ); + } + + approve( + TokenInterface(vaultDetails_.supplyToken), + vaultAddress_, + uint256(newCol_) + ); + } + } + + bool isPaybackMin_ = newDebt_ == type(int256).min; + + // Payback + if (newDebt_ < 0) { + if (vaultDetails_.borrowToken == getEthAddr()) { + // Needs to be positive as it will be send in msg.value + ethAmount_ = isPaybackMin_ + ? repayApproveAmt_ + : uint256(-newDebt_); + } else { + isPaybackMin_ + ? approve( + TokenInterface(vaultDetails_.borrowToken), + vaultAddress_, + repayApproveAmt_ + ) + : approve( + TokenInterface(vaultDetails_.borrowToken), + vaultAddress_, + uint256(-newDebt_) + ); + } + } + + // Note max withdraw will be handled by Fluid contract + (nftId_, newCol_, newDebt_) = vault_.operate{value: ethAmount_}( + nftId_, + newCol_, + newDebt_, + address(this) + ); + + setUint(setIds_[0], nftId_); + + setIds_[1] > 0 + ? setUint(setIds_[1], uint256(newCol_)) + : setUint(setIds_[2], uint256(newCol_)); // If setIds_[2] != 0, it will set the ID. + setIds_[3] > 0 + ? setUint(setIds_[3], uint256(newDebt_)) + : setUint(setIds_[4], uint256(newDebt_)); // If setIds_[4] != 0, it will set the ID. + + _eventName = "LogOperateWithIds(address,uint256,int256,int256,uint256[],uint256[])"; + _eventParam = abi.encode( + vaultAddress_, + nftId_, + newCol_, + newDebt_, + getIds_, + setIds_ + ); + } + + /** + * @dev Deposit, borrow, payback and withdraw asset from the vault. + * @notice Single function which handles supply, withdraw, borrow & payback + * @param vaultAddress_ Vault address. + * @param nftId_ NFT ID for interaction. If 0 then create new NFT/position. + * @param newCol_ New collateral. If positive then deposit, if negative then withdraw, if 0 then do nothing. + * For max deposit use type(uint25).max, for max withdraw use type(uint25).min. + * @param newDebt_ New debt. If positive then borrow, if negative then payback, if 0 then do nothing + * For max payback use type(uint25).min. + * @param repayApproveAmt_ In case of max amount for payback, this amount will be approved for spending. + * Should always be positive. + */ + function operate( + address vaultAddress_, + uint256 nftId_, + int256 newCol_, + int256 newDebt_, + uint256 repayApproveAmt_ + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + IVault vault_ = IVault(vaultAddress_); + + IVault.ConstantViews memory vaultDetails_ = vault_.constantsView(); + + uint256 ethAmount_; + + bool isColMax_ = newCol_ == type(int256).max; + + // Deposit + if (newCol_ > 0) { + if (vaultDetails_.supplyToken == getEthAddr()) { + ethAmount_ = isColMax_ + ? address(this).balance + : uint256(newCol_); + + newCol_ = int256(ethAmount_); + } else { + if (isColMax_) { + newCol_ = int256( + TokenInterface(vaultDetails_.supplyToken).balanceOf( + address(this) + ) + ); + } + + approve( + TokenInterface(vaultDetails_.supplyToken), + vaultAddress_, + uint256(newCol_) + ); + } + } + + bool isPaybackMin_ = newDebt_ == type(int256).min; + + // Payback + if (newDebt_ < 0) { + if (vaultDetails_.borrowToken == getEthAddr()) { + // Needs to be positive as it will be send in msg.value + ethAmount_ = isPaybackMin_ + ? repayApproveAmt_ + : uint256(-newDebt_); + } else { + isPaybackMin_ + ? approve( + TokenInterface(vaultDetails_.borrowToken), + vaultAddress_, + repayApproveAmt_ + ) + : approve( + TokenInterface(vaultDetails_.borrowToken), + vaultAddress_, + uint256(-newDebt_) + ); + } + } + + // Note max withdraw will be handled by Fluid contract + (nftId_, newCol_, newDebt_) = vault_.operate{value: ethAmount_}( + nftId_, + newCol_, + newDebt_, + address(this) + ); + + _eventName = "LogOperate(address,uint256,int256,int256)"; + _eventParam = abi.encode( + vaultAddress_, + nftId_, + newCol_, + newDebt_ + ); + } +} + +contract ConnectV2FluidArbitrum is FluidConnector { + string public constant name = "Fluid-v1.0"; +}