Skip to content
Draft
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
37 changes: 37 additions & 0 deletions script/DeployDummyDelayedStrategy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import "@balmy/earn-core/vault/EarnVault.sol";
import "src/global-registry/GlobalEarnRegistry.sol";

import "src/strategies/instances/erc4626/ERC4626DelayedStrategy.sol";
import "src/delayed-withdrawal-adapter/ERC4626DelayedWithdrawalAdapter.sol";

contract DeployDummyDelayedStrategy is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployer = 0xB86dA339B88D9697fa3ACC55EDd378e002676E01;
address admin = vm.envAddress("GOVERNOR");
vm.startBroadcast(deployerPrivateKey);

IEarnVault vault = IEarnVault(0x63a8aE714568EC8f8Ec14472674c68582b0B0458);
IGlobalEarnRegistry globalRegistry = IGlobalEarnRegistry(0x7DA14784E8F1fb71c23Ad6c6ac7f063Fdf098F38);

ERC4626DelayedWithdrawalAdapter delayedWithdrawalAdapter = new ERC4626DelayedWithdrawalAdapter(
globalRegistry,
0xBb287E6017d3DEb0e2E65061e8684eab21060123, // Yearn v3 USDT-A
7200 // 7200 seconds are 2 hours
);
ERC4626DelayedStrategy delayedStrategy = new ERC4626DelayedStrategy(
globalRegistry,
vault,
0xBb287E6017d3DEb0e2E65061e8684eab21060123,
"Delayed Yearn v3 USDT-A",
delayedWithdrawalAdapter
);

vault.STRATEGY_REGISTRY().registerStrategy(deployer, delayedStrategy);
vm.stopBroadcast();
}
}
139 changes: 139 additions & 0 deletions src/delayed-withdrawal-adapter/ERC4626DelayedWithdrawalAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.22;

import {
IDelayedWithdrawalAdapter,
IDelayedWithdrawalManager,
IEarnVault
} from "src/interfaces/IDelayedWithdrawalAdapter.sol";
import { IEarnStrategy, StrategyId } from "@balmy/earn-core/interfaces/IEarnStrategy.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IGlobalEarnRegistry } from "src/interfaces/IGlobalEarnRegistry.sol";
import { IERC4626, IERC20 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";

struct Request {
uint256 amount;
uint256 deadline;
}

contract ERC4626DelayedWithdrawalAdapter is IDelayedWithdrawalAdapter {
using Math for uint256;
using SafeERC20 for IERC20;

/// @notice The id for the Delayed Withdrawal Manager
bytes32 public constant DELAYED_WITHDRAWAL_MANAGER = keccak256("DELAYED_WITHDRAWAL_MANAGER");

IGlobalEarnRegistry public immutable registry;

address internal immutable _farmToken;
uint256 internal immutable _delay;
mapping(uint256 positionId => Request[] requestIds) internal _pendingWithdrawals;

constructor(IGlobalEarnRegistry _registry, address farmToken_, uint256 delay_) {
registry = _registry;
_farmToken = farmToken_;
_delay = delay_;
}

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IDelayedWithdrawalAdapter).interfaceId;
}

function estimatedPendingFunds(uint256 positionId, address) external view override returns (uint256 pendingAmount) {
Request[] memory requests = _pendingWithdrawals[positionId];
// slither-disable-next-line incorrect-equality
if (requests.length == 0) {
return 0;
}

for (uint256 i; i < requests.length; ++i) {
if (requests[i].deadline > block.timestamp) {
pendingAmount += IERC4626(_farmToken).previewRedeem(requests[i].amount);
}
}
}

function withdrawableFunds(uint256 positionId, address) external view override returns (uint256 withdrawableAmount) {
Request[] memory requests = _pendingWithdrawals[positionId];
// slither-disable-next-line incorrect-equality
if (requests.length == 0) {
return 0;
}

for (uint256 i; i < requests.length; ++i) {
if (requests[i].deadline <= block.timestamp) {
withdrawableAmount += IERC4626(_farmToken).previewRedeem(requests[i].amount);
}
}
}

function initiateDelayedWithdrawal(uint256 positionId, address, uint256 amount) external override {
IDelayedWithdrawalManager delayedWithdrawalManager = manager();
IEarnVault vault_ = delayedWithdrawalManager.VAULT();
StrategyId strategyId = vault_.positionsStrategy(positionId);
IEarnStrategy strategy = vault_.STRATEGY_REGISTRY().getStrategy(strategyId);
if (msg.sender != address(strategy)) {
revert UnauthorizedPositionStrategy();
}
Request[] storage requests = _pendingWithdrawals[positionId];
bool needsToRegister = requests.length == 0;
requests.push(Request({ amount: amount, deadline: block.timestamp + _delay }));
if (needsToRegister) {
delayedWithdrawalManager.registerDelayedWithdraw(positionId, (IERC4626(_farmToken).asset()));
}
}

// slither-disable-start assembly
function withdraw(
uint256 positionId,
address,
address recipient
)
external
override
onlyManager
returns (uint256 withdrawn, uint256 stillPending)
{
Request[] memory requests = _pendingWithdrawals[positionId];
if (requests.length == 0) {
return (0, 0);
}
uint256 numberOfRequestsPending;
uint256 farmTokenAmountToWithdraw;
for (uint256 i; i < requests.length; ++i) {
if (requests[i].deadline > block.timestamp) {
stillPending += IERC4626(_farmToken).previewRedeem(requests[i].amount);
if (numberOfRequestsPending != i) {
requests[numberOfRequestsPending] = requests[i];
}
++numberOfRequestsPending;
} else {
farmTokenAmountToWithdraw += requests[i].amount;
}
}

if (numberOfRequestsPending != requests.length) {
// Resize the array
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(requests, numberOfRequestsPending)
}
}
_pendingWithdrawals[positionId] = requests;
withdrawn = IERC4626(_farmToken).redeem(farmTokenAmountToWithdraw, recipient, address(this));
}

function manager() public view returns (IDelayedWithdrawalManager) {
return IDelayedWithdrawalManager(registry.getAddressOrFail(DELAYED_WITHDRAWAL_MANAGER));
}

function vault() public view override returns (IEarnVault) {
return manager().VAULT();
}

modifier onlyManager() {
if (msg.sender != address(manager())) revert UnauthorizedDelayedWithdrawalManager();
_;
}
}
86 changes: 86 additions & 0 deletions src/strategies/instances/erc4626/ERC4626DelayedStrategy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.22;

import {
IEarnStrategy, StrategyId, IEarnVault, SpecialWithdrawalCode
} from "@balmy/earn-core/interfaces/IEarnStrategy.sol";
import { IDelayedWithdrawalAdapter } from "src/delayed-withdrawal-manager/DelayedWithdrawalManager.sol";
import { IGlobalEarnRegistry } from "src/interfaces/IGlobalEarnRegistry.sol";
import { ExternalFees } from "../../layers/fees/ExternalFees.sol";
import { ExternalTOSCreationValidation } from "../../layers/creation-validation/ExternalTOSCreationValidation.sol";
import { ExternalLiquidityMining } from "../../layers/liquidity-mining/ExternalLiquidityMining.sol";
import { BaseDelayedStrategy } from "../base/BaseDelayedStrategy.sol";
import { IERC4626, IERC20 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import { ERC4626DelayedConnector } from "../../layers/connector/ERC4626DelayedConnector.sol";

contract ERC4626DelayedStrategy is
BaseDelayedStrategy,
ERC4626DelayedConnector,
ExternalLiquidityMining,
ExternalFees,
ExternalTOSCreationValidation
{
/// @inheritdoc IEarnStrategy
string public description;

// slither-disable-next-line naming-convention
IDelayedWithdrawalAdapter internal immutable __delayedWithdrawalAdapter;
IGlobalEarnRegistry internal immutable _globalRegistry;
IEarnVault internal immutable _vault;
address internal immutable _farmToken;

constructor(
// General
IGlobalEarnRegistry globalRegistry_,
IEarnVault vault_,
address farmToken_,
string memory description_,
IDelayedWithdrawalAdapter delayedWithdrawalAdapter_
) {
_globalRegistry = globalRegistry_;
_vault = vault_;
_farmToken = farmToken_;
description = description_;
__delayedWithdrawalAdapter = delayedWithdrawalAdapter_;
maxApproveVault();
}

// slither-disable-next-line naming-convention,dead-code
function _fees_underlying_asset() internal view override returns (address asset) {
return _connector_asset();
}

function globalRegistry()
public
view
override(ExternalFees, ExternalLiquidityMining, ExternalTOSCreationValidation)
returns (IGlobalEarnRegistry)
{
return _globalRegistry;
}

function strategyId()
public
view
override(BaseDelayedStrategy, ExternalFees, ExternalLiquidityMining, ExternalTOSCreationValidation)
returns (StrategyId)
{
return BaseDelayedStrategy.strategyId();
}

function _earnVault() internal view virtual override returns (IEarnVault) {
return _vault;
}

function ERC4626Vault() public view virtual override returns (IERC4626) {
return IERC4626(_farmToken);
}

function _asset() internal view virtual override returns (IERC20) {
return IERC20(ERC4626Vault().asset());
}

function _delayedWithdrawalAdapter() internal view virtual override returns (IDelayedWithdrawalAdapter) {
return __delayedWithdrawalAdapter;
}
}
Loading