Skip to content
Merged
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
20 changes: 8 additions & 12 deletions packages/ovault-composer/contracts/ERC4626Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ contract ERC4626Adapter is IERC4626Adapter, IERC20 {
string public symbol;

uint8 private immutable _underlyingDecimals;
uint8 private immutable _shareDecimals;

/**
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777).
*/
constructor(address asset_, address share_) {
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
_underlyingDecimals = success ? assetDecimals : 18;
_shareDecimals = IERC20Metadata(share_).decimals();

_asset = IERC20(asset_);
_share = IERC20MintBurnExtension(share_);

Expand All @@ -49,17 +52,6 @@ contract ERC4626Adapter is IERC4626Adapter, IERC20 {
return (false, 0);
}

/**
* @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
* "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
* asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.
*
* See {IERC20Metadata-decimals}.
*/
function decimals() public view virtual returns (uint8) {
return _underlyingDecimals + _decimalsOffset();
}

/** @dev See {IERC4626-asset}. */
function asset() public view virtual returns (address) {
return address(_asset);
Expand All @@ -70,6 +62,10 @@ contract ERC4626Adapter is IERC4626Adapter, IERC20 {
return address(_share);
}

function decimals() public view virtual returns (uint8) {
return _shareDecimals;
}

/// @dev Adding to proxy the share token's total supply
function totalSupply() public view virtual returns (uint256) {
return IERC20(share()).totalSupply();
Expand Down Expand Up @@ -286,6 +282,6 @@ contract ERC4626Adapter is IERC4626Adapter, IERC20 {
}

function _decimalsOffset() internal view virtual returns (uint8) {
return 0;
return _shareDecimals - _underlyingDecimals;
}
}
220 changes: 220 additions & 0 deletions packages/ovault-composer/contracts/OVaultComposer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20MintBurnExtension } from "./interfaces/IERC20MintBurnExtension.sol";
import { IOVault } from "./interfaces/IOVault.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";

import { IOVaultComposer, FailedMessage, FailedState } from "./interfaces/IOVaultComposer.sol";
import { IOVault } from "./interfaces/IOVault.sol";
import { IERC4626Adapter } from "./interfaces/IERC4626Adapter.sol";

contract OVaultComposer is IOVaultComposer, ReentrancyGuard {
using OFTComposeMsgCodec for bytes;
using OFTComposeMsgCodec for bytes32;

address public immutable ASSET_OFT;
address public immutable SHARE_OFT;
address public immutable OVAULT;
address public immutable ENDPOINT;

mapping(bytes32 guid => FailedMessage) public failedMessages;

constructor(address _ovault) {
address share = IERC4626Adapter(_ovault).share();
address asset = IERC4626Adapter(_ovault).asset();
if (!IERC20MintBurnExtension(share).ERC4626AdapterCompliant()) {
revert IOVault.ShareNotERC4626AdapterCompliant();
}

OVAULT = _ovault;
SHARE_OFT = IOVault(_ovault).SHARE_OFT();
ASSET_OFT = IOVault(_ovault).ASSET_OFT();
ENDPOINT = address(IOAppCore(ASSET_OFT).endpoint());

// Approve the adapter to spend the share tokens held by this contract
IERC20(share).approve(OVAULT, type(uint256).max);
IERC20(asset).approve(OVAULT, type(uint256).max);
}

function lzCompose(
address _refundOFT,
bytes32 _guid,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) external payable virtual override {
if (msg.sender != ENDPOINT) revert OnlyEndpoint(msg.sender);
if (_refundOFT != ASSET_OFT && _refundOFT != SHARE_OFT) revert OnlyOFT(_refundOFT);

/// @dev Route to the correct target OFT
address oft = _refundOFT == ASSET_OFT ? SHARE_OFT : ASSET_OFT;

/// @dev Extracted from the _message header. Will always be part of the _message since it is created by lzReceive
uint256 amount = OFTComposeMsgCodec.amountLD(_message);
bytes memory sendParamEncoded = OFTComposeMsgCodec.composeMsg(_message);

SendParam memory refundSendParam;
refundSendParam.dstEid = OFTComposeMsgCodec.srcEid(_message);
refundSendParam.to = OFTComposeMsgCodec.composeFrom(_message);
refundSendParam.amountLD = amount;

SendParam memory sendParam;

/// @dev Try decoding the composeMsg as a SendParam
try this.decodeSendParam(sendParamEncoded) returns (SendParam memory sendParamDecoded) {
/// @dev In the case of a valid decode we have the raw SendParam to be forwarded to the target OFT (oft)
sendParam = sendParamDecoded;
sendParam.amountLD = 0;
} catch {
/// @dev In the case of a failed decode we store the failed message and emit an event.
/// @dev This message can only be refunded back to the source chain.
failedMessages[_guid] = FailedMessage(address(0), sendParam, _refundOFT, refundSendParam);
emit DecodeFailed(_guid, _refundOFT, sendParamEncoded);
return;
}

/// @dev Try to early catch issues surrounding LayerZero config. This quoteSend catches issues like: invalid peer, dvn config, etc.
try this.validateTargetOFTConfig(oft, sendParam) {} catch (bytes memory errMsg) {
/// @dev When erroring out we want to NOT make a swap and the user can only go back to the source chain.
failedMessages[_guid] = FailedMessage(address(0), sendParam, _refundOFT, refundSendParam);
emit GenericError(_guid, oft, errMsg);
return;
}

/// @dev Try to execute the action on the target OFT. If we hit an issue then it rolls back the storage changes.
try this.executeOVaultAction(_refundOFT, amount, sendParam) returns (uint256 vaultAmount) {
sendParam.amountLD = vaultAmount;
} catch (bytes memory errMsg) {
failedMessages[_guid] = FailedMessage(oft, sendParam, _refundOFT, refundSendParam);
emit GenericError(_guid, oft, errMsg);
return;
}

/// @dev Try sending the message to the target OFT
try this.send{ value: msg.value }(oft, sendParam) {
emit Sent(_guid, oft);
} catch {
/// @dev A failed send can happen due to not enough msg.value
/// @dev Since we have the target tokens in the composer, we can retry with more gas.
failedMessages[_guid] = FailedMessage(oft, sendParam, address(0), refundSendParam);
emit SendFailed(_guid, oft);
return;
}
}

/// @dev External call for try...catch logic in lzCompose()
function decodeSendParam(bytes calldata sendParamBytes) external pure returns (SendParam memory sendParam) {
sendParam = abi.decode(sendParamBytes, (SendParam));
}

/// @dev External call for try...catch logic in lzCompose()
function executeOVaultAction(
address _oft,
uint256 _amount,
SendParam calldata _sendParam
) external nonReentrant returns (uint256 vaultAmount) {
if (msg.sender != address(this)) revert OnlySelf(msg.sender);
vaultAmount = _executeOVaultAction(_oft, _amount);
if (vaultAmount < _sendParam.minAmountLD) {
/// @dev Will rollback on this function's storage changes (trade does not happen)
revert NotEnoughTargetTokens(vaultAmount, _sendParam.minAmountLD);
}
}

/// @dev Dirty swapping amountLD and minAmountLD to 1e18 and 0 to avoid Slippage issue on the target OFT quoteSend()
function validateTargetOFTConfig(address _oft, SendParam memory _sendParam) external view {
_sendParam.amountLD = 1e18;
_sendParam.minAmountLD = 0;

IOFT(_oft).quoteSend(_sendParam, false);
}

/// @dev External call for try...catch logic in lzCompose()
function send(address _oft, SendParam calldata _sendParam) external payable nonReentrant {
if (msg.sender != address(this)) revert OnlySelf(msg.sender);
_send(_oft, _sendParam);
}

/// @dev Permissionless function to send back the message to the source chain
/// @dev Always possible unless the lzCompose() fails due to an Out-Of-Gas panic
function refund(bytes32 _guid, bytes calldata _extraOptions) external payable nonReentrant {
FailedMessage memory failedMessage = failedMessages[_guid];
SendParam memory refundSendParam = failedMessage.sendParam;
if (failedGuidState(_guid) != FailedState.CanOnlyRefund) revert CanNotRefund(_guid);

refundSendParam.extraOptions = _extraOptions;

delete failedMessages[_guid];
_send(failedMessage.refundOFT, refundSendParam);
emit Refunded(_guid, failedMessage.refundOFT);
}

/// @dev Permissionless function to retry the message with more gas
/// @dev Probabilistically possible if the OFT.send() fails - ex: invalid peer
function retry(bytes32 _guid, bytes calldata _extraOptions) external payable nonReentrant {
FailedMessage memory failedMessage = failedMessages[_guid];
if (failedGuidState(_guid) != FailedState.CanOnlyRetry) revert CanNotRetry(_guid);

SendParam memory sendParam = failedMessage.sendParam;

sendParam.extraOptions = _extraOptions;

delete failedMessages[_guid];
_send(failedMessage.oft, sendParam);
emit Retried(_guid, failedMessage.oft);
}

/// @dev Retry mechanism for transactions that failed due to slippage. This can revert.
function retryWithSwap(bytes32 _guid, bytes calldata _extraOptions) external payable {
FailedMessage memory failedMessage = failedMessages[_guid];
if (failedGuidState(_guid) != FailedState.CanRetryWithSwap) revert CanNotRetry(_guid);

SendParam memory sendParam = failedMessage.sendParam;
sendParam.extraOptions = _extraOptions;

uint256 amountLd = failedMessage.refundSendParam.amountLD;

delete failedMessages[_guid];
sendParam.amountLD = _executeOVaultAction(failedMessage.refundOFT, amountLd);

_send(failedMessage.oft, sendParam);
emit Sent(_guid, failedMessage.oft);
}

/// @dev Internal function to send the message to the target OFT
function _send(address _oft, SendParam memory _sendParam) internal {
IOFT(_oft).send{ value: msg.value }(_sendParam, MessagingFee(msg.value, 0), tx.origin);
}

function _executeOVaultAction(address _oft, uint256 _amount) internal returns (uint256 vaultAmount) {
if (_oft == ASSET_OFT) {
vaultAmount = IERC4626Adapter(OVAULT).deposit(_amount, address(this));
} else {
vaultAmount = IERC4626Adapter(OVAULT).redeem(_amount, address(this), address(this));
}
}

/// @dev Helper to view the state of a failed message
function failedGuidState(bytes32 _guid) public view returns (FailedState) {
FailedMessage memory failedMessage = failedMessages[_guid];

if (failedMessage.refundOFT == address(0) && failedMessage.oft == address(0)) {
return FailedState.NotFound;
}
if (failedMessage.refundOFT != address(0) && failedMessage.oft == address(0)) {
return FailedState.CanOnlyRefund;
}
if (failedMessage.refundOFT == address(0) && failedMessage.oft != address(0)) {
return FailedState.CanOnlyRetry;
}

return FailedState.CanRetryWithSwap;
}
receive() external payable {}
}
66 changes: 66 additions & 0 deletions packages/ovault-composer/contracts/interfaces/IOVaultComposer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol";
import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";

struct FailedMessage {
address oft;
SendParam sendParam;
address refundOFT;
SendParam refundSendParam;
}

enum FailedState {
NotFound,
CanOnlyRefund,
CanOnlyRetry,
CanRetryWithSwap
}

interface IOVaultComposer is IOAppComposer {
/// ========================== EVENTS =====================================
event DecodeFailed(bytes32 indexed guid, address indexed oft, bytes message);
event Sent(bytes32 indexed guid, address indexed oft);
event SendFailed(bytes32 indexed guid, address indexed oft);
event Refunded(bytes32 indexed guid, address indexed oft);
event Retried(bytes32 indexed guid, address indexed oft);
event GenericError(bytes32 indexed guid, address indexed oft, bytes errMsg);

/// ========================== Error Messages =====================================
error InvalidAdapterMesh();
error InvalidOFTMesh();

error OnlyEndpoint(address caller);
error OnlySelf(address caller);
error OnlyOFT(address oft);
error OnlyAsset(address asset);
error OnlyShare(address share);
error CanNotRefund(bytes32 guid);
error CanNotRetry(bytes32 guid);
error CanNotWithdraw(bytes32 guid);
error NotEnoughTargetTokens(uint256 amountLD, uint256 minAmountLD);

/// ========================== GLOBAL VARIABLE FUNCTIONS =====================================
function ASSET_OFT() external view returns (address);
function SHARE_OFT() external view returns (address);
function ENDPOINT() external view returns (address);

/// ========================== FUNCTIONS =====================================
function executeOVaultAction(
address _oft,
uint256 _amount,
SendParam calldata _sendParam
) external returns (uint256 vaultAmount);

function validateTargetOFTConfig(address _oft, SendParam memory _sendParam) external view;

function refund(bytes32 guid, bytes memory extraOptions) external payable;
function retry(bytes32 guid, bytes memory extraOptions) external payable;
function retryWithSwap(bytes32 guid, bytes memory extraOptions) external payable;
function send(address _oft, SendParam calldata _sendParam) external payable;

function failedGuidState(bytes32 guid) external view returns (FailedState);

receive() external payable;
}
Loading