Skip to content

Commit

Permalink
Add controlled async redeem implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
hieronx committed Aug 21, 2024
1 parent e2f5842 commit 185ff42
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 64 deletions.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ Reference implementations for [ERC-7540](https://eips.ethereum.org/EIPS/eip-7540

This code is unaudited.

#### Controlled Async Deposits ([code](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/src/ControlledAsyncDeposits.sol))
- Async deposits are subject to approval by an owner account
#### Controlled Async Deposit ([code](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/src/ControlledAsyncDeposit.sol))
- Deposits are asynchronous and subject to fulfillment by an owner account
- Redemptions are synchronous (standard ERC4626)

#### Timelocked Async Withdrawals ([code](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/src/TimelockedAsyncWithdrawals.sol))
- Async redemptions are subject to a 3 day delay
#### Controlled Async Redeem ([code](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/src/ControlledAsyncDeposit.sol))
- Deposits are synchronous (standard ERC4626)
- Redemptions are asynchronous and subject to fulfillment by an owner account

#### Fully Async Vault ([code](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/src/FullyAsyncVault.sol))
Inherits from Controlled Async Deposit and Controlled Async Redeem

- Both deposits and redemptions are asynchronous and subject to fulfillment by an owner account

#### Timelocked Async Redeem ([code](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/src/TimelockedAsyncRedeem.sol))
- Deposits are synchronous (standard ERC4626)
- Redemptions are asynchronous and subject to a 3 day delay
- New redemptions restart the 3 day delay even if the prior redemption is claimable.
- The redemption exchange rate is locked in immediately upon request.

#### Fully Async Vault ([code](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/src/FullyAsyncVault.sol))
- Inherits from Controlled Async Deposits and Timelocked Async Withdrawals

## License
This codebase is licensed under [MIT license](https://github.com/ERC4626-Alliance/ERC-7540-Reference/blob/main/LICENSE).
4 changes: 4 additions & 0 deletions src/BaseERC7540.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ abstract contract BaseERC7540 is ERC4626, Owned, IERC7540Operator {
ERC4626(_asset, _name, _symbol)
{}

function totalAssets() public view virtual override returns (uint256) {
return ERC20(asset).balanceOf(address(this));
}

/*//////////////////////////////////////////////////////////////
ERC7540 LOGIC
//////////////////////////////////////////////////////////////*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {ERC20} from "solmate/tokens/ERC20.sol";
* - yield for the underlying asset is assumed to be transferred directly into the vault by some arbitrary mechanism
* - async deposits are subject to approval by an owner account
*/
abstract contract BaseControlledAsyncDeposits is BaseERC7540, IERC7540Deposit {
abstract contract BaseControlledAsyncDeposit is BaseERC7540, IERC7540Deposit {
using FixedPointMathLib for uint256;

uint256 internal _totalPendingDepositAssets;
Expand Down Expand Up @@ -159,6 +159,6 @@ abstract contract BaseControlledAsyncDeposits is BaseERC7540, IERC7540Deposit {
}
}

contract ControlledAsyncDeposits is BaseControlledAsyncDeposits {
contract ControlledAsyncDeposit is BaseControlledAsyncDeposit {
constructor(ERC20 _asset, string memory _name, string memory _symbol) BaseERC7540(_asset, _name, _symbol) {}
}
155 changes: 155 additions & 0 deletions src/ControlledAsyncRedeem.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {BaseERC7540} from "src/BaseERC7540.sol";
import {IERC7540Redeem} from "src/interfaces/IERC7540.sol";
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";

// THIS VAULT IS AN UNOPTIMIZED, POTENTIALLY UNSECURE REFERENCE EXAMPLE AND IN NO WAY MEANT TO BE USED IN PRODUCTION

/**
* @notice ERC7540 Implementing Controlled Async Redeem
*
* This Vault has the following properties:
* - yield for the underlying asset is assumed to be transferred directly into the vault by some arbitrary mechanism
* - async redemptions are subject to approval by an owner account
*/
abstract contract BaseControlledAsyncRedeem is BaseERC7540, IERC7540Redeem {
using FixedPointMathLib for uint256;

mapping(address => PendingRedeem) internal _pendingRedeem;
mapping(address => ClaimableRedeem) internal _claimableRedeem;

struct PendingRedeem {
uint256 shares;
}

struct ClaimableRedeem {
uint256 assets;
uint256 shares;
}

/*//////////////////////////////////////////////////////////////
ERC7540 LOGIC
//////////////////////////////////////////////////////////////*/

/// @notice this deposit request is added to any pending deposit request
function requestRedeem(uint256 shares, address controller, address owner) external returns (uint256 requestId) {
require(owner == msg.sender || isOperator[owner][msg.sender], "ERC7540Vault/invalid-owner");
require(ERC20(address(this)).balanceOf(owner) >= shares, "ERC7540Vault/insufficient-balance");
require(shares != 0, "ZERO_SHARES");

SafeTransferLib.safeTransferFrom(this, owner, address(this), shares);

uint256 currentPendingShares = _pendingRedeem[controller].shares;
_pendingRedeem[controller] = PendingRedeem(shares + currentPendingShares);

emit RedeemRequest(controller, owner, REQUEST_ID, msg.sender, shares);
return REQUEST_ID;
}

function pendingRedeemRequest(uint256, address controller) public view returns (uint256 pendingShares) {
pendingShares = _pendingRedeem[controller].shares;
}

function claimableRedeemRequest(uint256, address controller) public view returns (uint256 claimableShares) {
claimableShares = _claimableRedeem[controller].shares;
}

/*//////////////////////////////////////////////////////////////
DEPOSIT FULFILLMENT LOGIC
//////////////////////////////////////////////////////////////*/

function fulfillRedeem(address controller, uint256 shares) public onlyOwner returns (uint256 assets) {
PendingRedeem storage request = _pendingRedeem[controller];
require(request.shares != 0 && shares <= request.shares, "ZERO_SHARES");

assets = convertToAssets(shares);

_claimableRedeem[controller] =
ClaimableRedeem(_claimableRedeem[controller].assets + assets, _claimableRedeem[controller].shares + shares);

request.shares -= shares;
}

/*//////////////////////////////////////////////////////////////
ERC4626 OVERRIDDEN LOGIC
//////////////////////////////////////////////////////////////*/

function withdraw(uint256 assets, address receiver, address controller)
public
virtual
override
returns (uint256 shares)
{
require(controller == msg.sender || isOperator[controller][msg.sender], "ERC7540Vault/invalid-caller");
require(assets != 0, "Must claim nonzero amount");

// Claiming partially introduces precision loss. The user therefore receives a rounded down amount,
// while the claimable balance is reduced by a rounded up amount.
ClaimableRedeem storage claimable = _claimableRedeem[controller];
shares = assets.mulDivDown(claimable.shares, claimable.assets);
uint256 sharesUp = assets.mulDivUp(claimable.shares, claimable.assets);

claimable.assets -= assets;
claimable.shares = claimable.shares > sharesUp ? claimable.shares - sharesUp : 0;

SafeTransferLib.safeTransfer(asset, receiver, assets);

emit Withdraw(msg.sender, receiver, controller, assets, shares);
}

function redeem(uint256 shares, address receiver, address controller)
public
virtual
override
returns (uint256 assets)
{
require(controller == msg.sender || isOperator[controller][msg.sender], "ERC7540Vault/invalid-caller");
require(shares != 0, "Must claim nonzero amount");

// Claiming partially introduces precision loss. The user therefore receives a rounded down amount,
// while the claimable balance is reduced by a rounded up amount.
ClaimableRedeem storage claimable = _claimableRedeem[controller];
assets = shares.mulDivDown(claimable.assets, claimable.shares);
uint256 assetsUp = shares.mulDivUp(claimable.assets, claimable.shares);

claimable.assets = claimable.assets > assetsUp ? claimable.assets - assetsUp : 0;
claimable.shares -= shares;

SafeTransferLib.safeTransfer(asset, receiver, assets);

emit Withdraw(msg.sender, receiver, controller, assets, shares);
}

function maxWithdraw(address controller) public view virtual override returns (uint256) {
return _claimableRedeem[controller].assets;
}

function maxRedeem(address controller) public view virtual override returns (uint256) {
return _claimableRedeem[controller].shares;
}

// Preview functions always revert for async flows
function previewWithdraw(uint256) public pure virtual override returns (uint256) {
revert("ERC7540Vault/async-flow");
}

function previewRedeem(uint256) public pure virtual override returns (uint256) {
revert("ERC7540Vault/async-flow");
}

/*//////////////////////////////////////////////////////////////
ERC165 LOGIC
//////////////////////////////////////////////////////////////*/

function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) {
return interfaceId == type(IERC7540Redeem).interfaceId || super.supportsInterface(interfaceId);
}
}

contract ControlledAsyncRedeem is BaseControlledAsyncRedeem {
constructor(ERC20 _asset, string memory _name, string memory _symbol) BaseERC7540(_asset, _name, _symbol) {}
}
68 changes: 28 additions & 40 deletions src/FullyAsyncVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,36 @@ pragma solidity ^0.8.15;
import {IERC7540Deposit, IERC7540Redeem} from "src/interfaces/IERC7540.sol";
import {BaseERC7540} from "src/BaseERC7540.sol";
import {ERC4626} from "solmate/mixins/ERC4626.sol";
import {BaseControlledAsyncDeposits} from "src/ControlledAsyncDeposits.sol";
import {BaseTimelockedAsyncWithdrawals} from "src/TimelockedAsyncWithdrawals.sol";
import {BaseControlledAsyncDeposit} from "src/ControlledAsyncDeposit.sol";
import {BaseControlledAsyncRedeem} from "src/ControlledAsyncRedeem.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";

contract FullyAsyncVault is BaseControlledAsyncDeposits, BaseTimelockedAsyncWithdrawals {
contract FullyAsyncVault is BaseControlledAsyncDeposit, BaseControlledAsyncRedeem {
constructor(ERC20 _asset, string memory _name, string memory _symbol)
BaseControlledAsyncDeposits()
BaseTimelockedAsyncWithdrawals()
BaseControlledAsyncDeposit()
BaseControlledAsyncRedeem()
BaseERC7540(_asset, _name, _symbol)
{}

function totalAssets()
public
view
virtual
override(BaseControlledAsyncDeposits, BaseTimelockedAsyncWithdrawals)
returns (uint256)
{
return ERC20(asset).balanceOf(address(this)) - _totalPendingDepositAssets - _totalPendingRedeemAssets;
function totalAssets() public view virtual override(BaseControlledAsyncDeposit, BaseERC7540) returns (uint256) {
return ERC20(asset).balanceOf(address(this)) - _totalPendingDepositAssets;
}

function maxDeposit(address controller)
public
view
virtual
override(BaseControlledAsyncDeposits, ERC4626)
override(BaseControlledAsyncDeposit, ERC4626)
returns (uint256)
{
return BaseControlledAsyncDeposits.maxDeposit(controller);
return BaseControlledAsyncDeposit.maxDeposit(controller);
}

function previewDeposit(uint256)
public
pure
virtual
override(BaseControlledAsyncDeposits, ERC4626)
override(BaseControlledAsyncDeposit, ERC4626)
returns (uint256)
{
revert("ERC7540Vault/async-flow");
Expand All @@ -48,56 +42,50 @@ contract FullyAsyncVault is BaseControlledAsyncDeposits, BaseTimelockedAsyncWith
function deposit(uint256 assets, address receiver)
public
virtual
override(BaseControlledAsyncDeposits, ERC4626)
override(BaseControlledAsyncDeposit, ERC4626)
returns (uint256 shares)
{
shares = BaseControlledAsyncDeposits.deposit(assets, receiver, receiver);
shares = BaseControlledAsyncDeposit.deposit(assets, receiver, receiver);
}

function maxMint(address controller)
public
view
virtual
override(BaseControlledAsyncDeposits, ERC4626)
override(BaseControlledAsyncDeposit, ERC4626)
returns (uint256)
{
return BaseControlledAsyncDeposits.maxMint(controller);
return BaseControlledAsyncDeposit.maxMint(controller);
}

function previewMint(uint256)
public
pure
virtual
override(BaseControlledAsyncDeposits, ERC4626)
returns (uint256)
{
function previewMint(uint256) public pure virtual override(BaseControlledAsyncDeposit, ERC4626) returns (uint256) {
revert("ERC7540Vault/async-flow");
}

function mint(uint256 shares, address receiver)
public
virtual
override(BaseControlledAsyncDeposits, ERC4626)
override(BaseControlledAsyncDeposit, ERC4626)
returns (uint256 assets)
{
assets = BaseControlledAsyncDeposits.mint(shares, receiver, receiver);
assets = BaseControlledAsyncDeposit.mint(shares, receiver, receiver);
}

function maxWithdraw(address controller)
public
view
virtual
override(BaseTimelockedAsyncWithdrawals, ERC4626)
override(BaseControlledAsyncRedeem, ERC4626)
returns (uint256)
{
return BaseTimelockedAsyncWithdrawals.maxWithdraw(controller);
return BaseControlledAsyncRedeem.maxWithdraw(controller);
}

function previewWithdraw(uint256)
public
pure
virtual
override(BaseTimelockedAsyncWithdrawals, ERC4626)
override(BaseControlledAsyncRedeem, ERC4626)
returns (uint256)
{
revert("ERC7540Vault/async-flow");
Expand All @@ -106,27 +94,27 @@ contract FullyAsyncVault is BaseControlledAsyncDeposits, BaseTimelockedAsyncWith
function withdraw(uint256 assets, address receiver, address controller)
public
virtual
override(BaseTimelockedAsyncWithdrawals, ERC4626)
override(BaseControlledAsyncRedeem, ERC4626)
returns (uint256 shares)
{
shares = BaseTimelockedAsyncWithdrawals.withdraw(assets, receiver, controller);
shares = BaseControlledAsyncRedeem.withdraw(assets, receiver, controller);
}

function maxRedeem(address controller)
public
view
virtual
override(BaseTimelockedAsyncWithdrawals, ERC4626)
override(BaseControlledAsyncRedeem, ERC4626)
returns (uint256)
{
return BaseTimelockedAsyncWithdrawals.maxRedeem(controller);
return BaseControlledAsyncRedeem.maxRedeem(controller);
}

function previewRedeem(uint256)
public
pure
virtual
override(BaseTimelockedAsyncWithdrawals, ERC4626)
override(BaseControlledAsyncRedeem, ERC4626)
returns (uint256)
{
revert("ERC7540Vault/async-flow");
Expand All @@ -135,17 +123,17 @@ contract FullyAsyncVault is BaseControlledAsyncDeposits, BaseTimelockedAsyncWith
function redeem(uint256 shares, address receiver, address controller)
public
virtual
override(BaseTimelockedAsyncWithdrawals, ERC4626)
override(BaseControlledAsyncRedeem, ERC4626)
returns (uint256 assets)
{
assets = BaseTimelockedAsyncWithdrawals.redeem(shares, receiver, controller);
assets = BaseControlledAsyncRedeem.redeem(shares, receiver, controller);
}

function supportsInterface(bytes4 interfaceId)
public
pure
virtual
override(BaseControlledAsyncDeposits, BaseTimelockedAsyncWithdrawals)
override(BaseControlledAsyncDeposit, BaseControlledAsyncRedeem)
returns (bool)
{
return interfaceId == type(IERC7540Deposit).interfaceId || interfaceId == type(IERC7540Redeem).interfaceId
Expand Down
Loading

0 comments on commit 185ff42

Please sign in to comment.