Skip to content

Commit

Permalink
Implement osToken flashloans
Browse files Browse the repository at this point in the history
  • Loading branch information
tsudmi committed Sep 6, 2024
1 parent d40e7e3 commit 61fe542
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 24 deletions.
5 changes: 5 additions & 0 deletions abi/Errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
"name": "ExitRequestNotProcessed",
"type": "error"
},
{
"inputs": [],
"name": "FlashLoanFailed",
"type": "error"
},
{
"inputs": [],
"name": "HarvestFailed",
Expand Down
20 changes: 20 additions & 0 deletions abi/IOsTokenFlashLoanRecipient.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "userData",
"type": "bytes"
}
],
"name": "receiveFlashLoan",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
44 changes: 44 additions & 0 deletions abi/IOsTokenFlashLoans.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "OsTokenFlashLoan",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "userData",
"type": "bytes"
}
],
"name": "flashLoan",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
8 changes: 7 additions & 1 deletion abi/IOsTokenVaultEscrow.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,13 @@
}
],
"name": "claimExitedAssets",
"outputs": [],
"outputs": [
{
"internalType": "uint256",
"name": "claimedAssets",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
Expand Down
17 changes: 17 additions & 0 deletions contracts/interfaces/IOsTokenFlashLoanRecipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.22;

/**
* @title IOsTokenFlashLoanRecipient
* @author StakeWise
* @notice Interface for OsTokenFlashLoanRecipient contract
*/
interface IOsTokenFlashLoanRecipient {
/**
* @notice Receive flash loan hook
* @param amount The osToken flash loan amount
* @param userData Arbitrary data passed to the hook
*/
function receiveFlashLoan(uint256 amount, bytes memory userData) external;
}
25 changes: 25 additions & 0 deletions contracts/interfaces/IOsTokenFlashLoans.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.22;

/**
* @title IOsTokenFlashLoans
* @author StakeWise
* @notice Interface for OsTokenFlashLoans contract
*/
interface IOsTokenFlashLoans {
/**
* @notice Event emitted on position creation
* @param recipient The address of the recipient
* @param amount The flashLoan osToken shares amount
*/
event OsTokenFlashLoan(address indexed recipient, uint256 amount);

/**
* @notice Flash loan OsToken shares
* @param recipient The address of the recipient
* @param amount The flashLoan osToken shares amount
* @param userData Arbitrary data passed to the `IOsTokenFlashLoanRecipient.receiveFlashLoan` function
*/
function flashLoan(address recipient, uint256 amount, bytes memory userData) external;
}
3 changes: 2 additions & 1 deletion contracts/interfaces/IOsTokenVaultEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,13 @@ interface IOsTokenVaultEscrow is IMulticall {
* @param vault The address of the vault
* @param exitPositionTicket The exit position ticket
* @param osTokenShares The amount of osToken shares to burn
* @return claimedAssets The amount of assets claimed
*/
function claimExitedAssets(
address vault,
uint256 exitPositionTicket,
uint256 osTokenShares
) external;
) external returns (uint256 claimedAssets);

/**
* @notice Liquidates the osToken shares
Expand Down
1 change: 1 addition & 0 deletions contracts/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ library Errors {
error InvalidWithdrawalCredentials();
error EigenPodNotFound();
error InvalidQueuedShares();
error FlashLoanFailed();
}
63 changes: 63 additions & 0 deletions contracts/tokens/OsTokenFlashLoans.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.22;

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import {IOsTokenFlashLoans} from '../interfaces/IOsTokenFlashLoans.sol';
import {IOsTokenFlashLoanRecipient} from '../interfaces/IOsTokenFlashLoanRecipient.sol';
import {IOsToken} from '../interfaces/IOsToken.sol';
import {Errors} from '../libraries/Errors.sol';

/**
* @title OsTokenFlashLoans
* @author StakeWise
* @notice Handles OsToken flash loans
*/
contract OsTokenFlashLoans is ReentrancyGuard, IOsTokenFlashLoans {
uint256 private constant _maxFlashLoanAmount = 1_000_000 ether;
address private immutable _osToken;

/**
* @dev Constructor
* @param osToken The address of the OsToken contract
*/
constructor(address osToken) ReentrancyGuard() {
_osToken = osToken;
}

/// @inheritdoc IOsTokenFlashLoans
function flashLoan(
address recipient,
uint256 osTokenShares,
bytes memory userData
) external override nonReentrant {
// check if not more than max flash loan amount requested
if (osTokenShares == 0 || osTokenShares > _maxFlashLoanAmount) {
revert Errors.InvalidShares();
}

// get current balance
uint256 preLoanBalance = IERC20(_osToken).balanceOf(address(this));

// mint OsToken shares for the recipient
IOsToken(_osToken).mint(recipient, osTokenShares);

// execute callback
IOsTokenFlashLoanRecipient(recipient).receiveFlashLoan(osTokenShares, userData);

// get post loan balance
uint256 postLoanBalance = IERC20(_osToken).balanceOf(address(this));

// check if the amount was repaid
if (postLoanBalance < preLoanBalance + osTokenShares) {
revert Errors.FlashLoanFailed();
}

// burn OsToken shares
IOsToken(address(_osToken)).burn(address(this), osTokenShares);

// emit event
emit OsTokenFlashLoan(recipient, osTokenShares);
}
}
21 changes: 7 additions & 14 deletions contracts/tokens/OsTokenVaultEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ abstract contract OsTokenVaultEscrow is Ownable2Step, Multicall, IOsTokenVaultEs
address vault,
uint256 exitPositionTicket,
uint256 osTokenShares
) external override {
) external override returns (uint256 claimedAssets) {
// burn osToken shares
_osTokenVaultController.burnShares(msg.sender, osTokenShares);

Expand All @@ -167,33 +167,26 @@ abstract contract OsTokenVaultEscrow is Ownable2Step, Multicall, IOsTokenVaultEs
}

// calculate assets to withdraw
uint256 assetsToTransfer;
if (position.osTokenShares != osTokenShares) {
assetsToTransfer = Math.mulDiv(position.exitedAssets, osTokenShares, position.osTokenShares);
claimedAssets = Math.mulDiv(position.exitedAssets, osTokenShares, position.osTokenShares);

// update position osTokenShares
position.exitedAssets -= SafeCast.toUint96(assetsToTransfer);
position.exitedAssets -= SafeCast.toUint96(claimedAssets);
position.osTokenShares -= SafeCast.toUint128(osTokenShares);
_positions[vault][exitPositionTicket] = position;
} else {
assetsToTransfer = position.exitedAssets;
claimedAssets = position.exitedAssets;

// remove position as it is fully processed
delete _positions[vault][exitPositionTicket];
}
if (assetsToTransfer == 0) revert Errors.ExitRequestNotProcessed();
if (claimedAssets == 0) revert Errors.ExitRequestNotProcessed();

// transfer assets
_transferAssets(position.owner, assetsToTransfer);
_transferAssets(position.owner, claimedAssets);

// emit event
emit ExitedAssetsClaimed(
msg.sender,
vault,
exitPositionTicket,
osTokenShares,
assetsToTransfer
);
emit ExitedAssetsClaimed(msg.sender, vault, exitPositionTicket, osTokenShares, claimedAssets);
}

/// @inheritdoc IOsTokenVaultEscrow
Expand Down
2 changes: 1 addition & 1 deletion contracts/vaults/ethereum/EthGenesisVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ contract EthGenesisVault is Initializable, EthVault, IEthGenesisVault {
// calculate max osToken shares that user can mint based on its current staked balance and osToken position
uint256 userMaxOsTokenShares = _calcMaxOsTokenShares(userAssets);
unchecked {
// cannot underflow because userOsTokenShares < userMaxOsTokenShares
// cannot underflow because mintedShares < userMaxOsTokenShares
return mintedShares < userMaxOsTokenShares ? userMaxOsTokenShares - mintedShares : 0;
}
}
Expand Down
1 change: 0 additions & 1 deletion contracts/vaults/ethereum/EthVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

pragma solidity ^0.8.22;

import {Math} from '@openzeppelin/contracts/utils/math/Math.sol';
import {Initializable} from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import {IEthVault} from '../../interfaces/IEthVault.sol';
import {IEthVaultFactory} from '../../interfaces/IEthVaultFactory.sol';
Expand Down
2 changes: 1 addition & 1 deletion contracts/vaults/gnosis/GnoGenesisVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ contract GnoGenesisVault is Initializable, GnoVault, IGnoGenesisVault {
// calculate max osToken shares that user can mint based on its current staked balance and osToken position
uint256 userMaxOsTokenShares = _calcMaxOsTokenShares(userAssets);
unchecked {
// cannot underflow because userOsTokenShares < userMaxOsTokenShares
// cannot underflow because mintedShares < userMaxOsTokenShares
return mintedShares < userMaxOsTokenShares ? userMaxOsTokenShares - mintedShares : 0;
}
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/vaults/modules/VaultOsToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ abstract contract VaultOsToken is VaultImmutables, VaultState, VaultEnterExit, I
}
// calculate max OsToken shares that can be minted
unchecked {
// cannot underflow because userOsTokenShares < userMaxOsTokenShares
// cannot underflow because position.shares < userMaxOsTokenShares
osTokenShares = userMaxOsTokenShares - position.shares;
}
}
Expand Down
8 changes: 4 additions & 4 deletions contracts/vaults/modules/VaultState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultAdmin, Vaul
}

/**
* @dev Internal function that must be used to process exit queue
* @dev Internal function that must be used to process exit queue
* @dev Make sure that sufficient time passed between exit queue updates (at least 1 day).
Currently it's restricted by the keeper's harvest interval
* Currently it's restricted by the keeper's harvest interval
* @return burnedShares The total amount of burned shares
*/
function _updateExitQueue() internal virtual returns (uint256 burnedShares) {
Expand Down Expand Up @@ -334,8 +334,8 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultAdmin, Vaul
) internal virtual returns (int256, bool);

/**
* @dev Internal function for retrieving the total assets stored in the Vault.
NB! Assets can be forcibly sent to the vault, the returned value must be used with caution
* @dev Internal function for retrieving the total assets stored in the Vault.
* NB! Assets can be forcibly sent to the vault, the returned value must be used with caution
* @return The total amount of assets stored in the Vault
*/
function _vaultAssets() internal view virtual returns (uint256);
Expand Down

0 comments on commit 61fe542

Please sign in to comment.