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
13 changes: 13 additions & 0 deletions packages/ovault-composer-evm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
out
cache

# artifacts; ignore all files except the local contract artifacts.
artifacts/*
# !artifacts/Fee.sol/
# !artifacts/IFee.sol/
# !artifacts/IOFT.sol/
# !artifacts/OFTComposeMsgCodec.sol/
# !artifacts/OFTMsgCodec.sol/
# !artifacts/OFT.sol/
# !artifacts/OFTAdapter.sol/
# !artifacts/OFTCore.sol/
31 changes: 31 additions & 0 deletions packages/ovault-composer-evm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<p align="center">
<a href="https://layerzero.network">
<img alt="LayerZero" style="max-width: 500px" src="https://d3a2dpnnrypp5h.cloudfront.net/bridge-app/lz.png"/>
</a>
</p>

<h1 align="center">@layerzerolabs/ovault-composer</h1>

<!-- The badges section -->
<p align="center">
<!-- Shields.io NPM published package version -->
<a href="https://www.npmjs.com/package/@layerzerolabs/ovault-composer"><img alt="NPM Version" src="https://img.shields.io/npm/v/@layerzerolabs/ovault-composer"/></a>
<!-- Shields.io NPM downloads -->
<a href="https://www.npmjs.com/package/@layerzerolabs/ovault-composer"><img alt="Downloads" src="https://img.shields.io/npm/dm/@layerzerolabs/ovault-composer"/></a>
<!-- Shields.io license badge -->
<a href="https://www.npmjs.com/package/@layerzerolabs/ovault-composer"><img alt="NPM License" src="https://img.shields.io/npm/l/@layerzerolabs/ovault-composer"/></a>
</p>

## Installation

```bash
pnpm install @layerzerolabs/ovault-composer
```

```bash
yarn install @layerzerolabs/ovault-composer
```

```bash
npm install @layerzerolabs/ovault-composer
```
29 changes: 29 additions & 0 deletions packages/ovault-composer-evm/contracts/OVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

contract OVault is ERC4626 {
using Math for uint256;
using SafeERC20 for IERC20;

constructor(string memory name, string memory symbol, address asset) ERC4626(IERC20(asset)) ERC20(name, symbol) {}

/// @dev Using solmate's implementation to work around rounding issues on initial minting
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.

return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), rounding);
}

/// @dev Using solmate's implementation to work around rounding issues on initial minting
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.

return supply == 0 ? shares : shares.mulDiv(totalAssets(), supply, rounding);
}
}
228 changes: 228 additions & 0 deletions packages/ovault-composer-evm/contracts/OVaultComposer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.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 { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";

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

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

address public immutable ASSET_OFT; // any OFT
address public immutable SHARE_OFT; // lockbox adapter
IERC4626 public immutable OVAULT; // IERC4626
address public immutable ENDPOINT;
uint32 public immutable HUB_EID;

mapping(bytes32 guid => FailedMessage) public failedMessages;

constructor(address _ovault, address _asset, address _share) {
OVAULT = IERC4626(_ovault);
ASSET_OFT = _asset;
SHARE_OFT = _share;

if (!IOFT(_share).approvalRequired()) {
revert ShareOFTShouldBeLockboxAdapter(address(_share));
}

ENDPOINT = address(IOAppCore(ASSET_OFT).endpoint());
HUB_EID = ILayerZeroEndpointV2(ENDPOINT).eid();

// Approve the adapter to spend the share tokens held by this contract
IERC20(IOFT(_share).token()).approve(address(_ovault), type(uint256).max);
IERC20(IOFT(_share).token()).approve(_share, type(uint256).max);
IERC20(IOFT(_asset).token()).approve(address(_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 ONLY when the target OFT does not have a peer set for the destination chain.
if (_isInvalidPeer(oft, sendParam.dstEid)) {
failedMessages[_guid] = FailedMessage(address(0), sendParam, _refundOFT, refundSendParam);
emit NoPeer(_guid, oft, sendParam.dstEid);
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 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);
if (_sendParam.dstEid == HUB_EID) {
address _receiver = _sendParam.to.bytes32ToAddress();
uint256 _amountLD = _sendParam.amountLD;
IERC20 token = IERC20(IOFT(_oft).token());
token.transfer(_receiver, _amountLD);
if (msg.value > 0) {
(bool sent, ) = _receiver.call{ value: msg.value }("");
require(sent, "Failed to send Ether");
}
emit SentOnHub(_receiver, _oft, _amountLD);
return;
}
_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 == address(ASSET_OFT)) {
vaultAmount = OVAULT.deposit(_amount, address(this));
} else {
vaultAmount = OVAULT.redeem(_amount, address(this), address(this));
}
}

/// @dev Helper to check if the target OFT does not have a peer set for the destination chain OR if our target chain is the not the same as the HUB chain
function _isInvalidPeer(address _oft, uint32 _dstEid) internal view returns (bool) {
return _dstEid != HUB_EID && IOAppCore(_oft).peers(_dstEid) == bytes32(0);
}

/// @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 {}
}
33 changes: 33 additions & 0 deletions packages/ovault-composer-evm/contracts/OVaultUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

contract OVaultUpgradeable is ERC4626Upgradeable {
using SafeERC20 for IERC20;
using Math for uint256;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @dev Using solmate's implementation to work around rounding issues on initial minting
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.

return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), rounding);
}

/// @dev Using solmate's implementation to work around rounding issues on initial minting
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.

return supply == 0 ? shares : shares.mulDiv(totalAssets(), supply, rounding);
}
}
Loading
Loading