Skip to content

Commit 8795d05

Browse files
authored
feat: ovault composer (#1583)
Signed-off-by: shankar <shankar@layerzerolabs.org>
1 parent 895f37e commit 8795d05

14 files changed

Lines changed: 1660 additions & 0 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
out
2+
cache
3+
4+
# artifacts; ignore all files except the local contract artifacts.
5+
artifacts/*
6+
# !artifacts/Fee.sol/
7+
# !artifacts/IFee.sol/
8+
# !artifacts/IOFT.sol/
9+
# !artifacts/OFTComposeMsgCodec.sol/
10+
# !artifacts/OFTMsgCodec.sol/
11+
# !artifacts/OFT.sol/
12+
# !artifacts/OFTAdapter.sol/
13+
# !artifacts/OFTCore.sol/
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<p align="center">
2+
<a href="https://layerzero.network">
3+
<img alt="LayerZero" style="max-width: 500px" src="https://d3a2dpnnrypp5h.cloudfront.net/bridge-app/lz.png"/>
4+
</a>
5+
</p>
6+
7+
<h1 align="center">@layerzerolabs/ovault-composer</h1>
8+
9+
<!-- The badges section -->
10+
<p align="center">
11+
<!-- Shields.io NPM published package version -->
12+
<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>
13+
<!-- Shields.io NPM downloads -->
14+
<a href="https://www.npmjs.com/package/@layerzerolabs/ovault-composer"><img alt="Downloads" src="https://img.shields.io/npm/dm/@layerzerolabs/ovault-composer"/></a>
15+
<!-- Shields.io license badge -->
16+
<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>
17+
</p>
18+
19+
## Installation
20+
21+
```bash
22+
pnpm install @layerzerolabs/ovault-composer
23+
```
24+
25+
```bash
26+
yarn install @layerzerolabs/ovault-composer
27+
```
28+
29+
```bash
30+
npm install @layerzerolabs/ovault-composer
31+
```
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7+
import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
8+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
9+
10+
contract OVault is ERC4626 {
11+
using Math for uint256;
12+
using SafeERC20 for IERC20;
13+
14+
constructor(string memory name, string memory symbol, address asset) ERC4626(IERC20(asset)) ERC20(name, symbol) {}
15+
16+
/// @dev Using solmate's implementation to work around rounding issues on initial minting
17+
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
18+
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.
19+
20+
return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), rounding);
21+
}
22+
23+
/// @dev Using solmate's implementation to work around rounding issues on initial minting
24+
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
25+
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.
26+
27+
return supply == 0 ? shares : shares.mulDiv(totalAssets(), supply, rounding);
28+
}
29+
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.22;
3+
4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
5+
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
6+
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
7+
8+
import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
9+
import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol";
10+
import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
11+
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";
12+
13+
import { IOVaultComposer, FailedMessage, FailedState } from "./interfaces/IOVaultComposer.sol";
14+
15+
contract OVaultComposer is IOVaultComposer, ReentrancyGuard {
16+
using OFTComposeMsgCodec for bytes;
17+
using OFTComposeMsgCodec for bytes32;
18+
19+
address public immutable ASSET_OFT; // any OFT
20+
address public immutable SHARE_OFT; // lockbox adapter
21+
IERC4626 public immutable OVAULT; // IERC4626
22+
address public immutable ENDPOINT;
23+
uint32 public immutable HUB_EID;
24+
25+
mapping(bytes32 guid => FailedMessage) public failedMessages;
26+
27+
constructor(address _ovault, address _asset, address _share) {
28+
OVAULT = IERC4626(_ovault);
29+
ASSET_OFT = _asset;
30+
SHARE_OFT = _share;
31+
32+
if (!IOFT(_share).approvalRequired()) {
33+
revert ShareOFTShouldBeLockboxAdapter(address(_share));
34+
}
35+
36+
ENDPOINT = address(IOAppCore(ASSET_OFT).endpoint());
37+
HUB_EID = ILayerZeroEndpointV2(ENDPOINT).eid();
38+
39+
// Approve the adapter to spend the share tokens held by this contract
40+
IERC20(IOFT(_share).token()).approve(address(_ovault), type(uint256).max);
41+
IERC20(IOFT(_share).token()).approve(_share, type(uint256).max);
42+
IERC20(IOFT(_asset).token()).approve(address(_ovault), type(uint256).max);
43+
}
44+
45+
function lzCompose(
46+
address _refundOFT,
47+
bytes32 _guid,
48+
bytes calldata _message,
49+
address /*_executor*/,
50+
bytes calldata /*_extraData*/
51+
) external payable virtual override {
52+
if (msg.sender != ENDPOINT) revert OnlyEndpoint(msg.sender);
53+
if (_refundOFT != ASSET_OFT && _refundOFT != SHARE_OFT) revert OnlyOFT(_refundOFT);
54+
55+
/// @dev Route to the correct target OFT
56+
address oft = _refundOFT == ASSET_OFT ? SHARE_OFT : ASSET_OFT;
57+
58+
/// @dev Extracted from the _message header. Will always be part of the _message since it is created by lzReceive
59+
uint256 amount = OFTComposeMsgCodec.amountLD(_message);
60+
bytes memory sendParamEncoded = OFTComposeMsgCodec.composeMsg(_message);
61+
62+
SendParam memory refundSendParam;
63+
refundSendParam.dstEid = OFTComposeMsgCodec.srcEid(_message);
64+
refundSendParam.to = OFTComposeMsgCodec.composeFrom(_message);
65+
refundSendParam.amountLD = amount;
66+
67+
SendParam memory sendParam;
68+
69+
/// @dev Try decoding the composeMsg as a SendParam
70+
try this.decodeSendParam(sendParamEncoded) returns (SendParam memory sendParamDecoded) {
71+
/// @dev In the case of a valid decode we have the raw SendParam to be forwarded to the target OFT (oft)
72+
sendParam = sendParamDecoded;
73+
sendParam.amountLD = 0;
74+
} catch {
75+
/// @dev In the case of a failed decode we store the failed message and emit an event.
76+
/// @dev This message can only be refunded back to the source chain.
77+
failedMessages[_guid] = FailedMessage(address(0), sendParam, _refundOFT, refundSendParam);
78+
emit DecodeFailed(_guid, _refundOFT, sendParamEncoded);
79+
return;
80+
}
81+
82+
/// @dev Try to early catch ONLY when the target OFT does not have a peer set for the destination chain.
83+
if (_isInvalidPeer(oft, sendParam.dstEid)) {
84+
failedMessages[_guid] = FailedMessage(address(0), sendParam, _refundOFT, refundSendParam);
85+
emit NoPeer(_guid, oft, sendParam.dstEid);
86+
return;
87+
}
88+
89+
/// @dev Try to execute the action on the target OFT. If we hit an issue then it rolls back the storage changes.
90+
try this.executeOVaultAction(_refundOFT, amount, sendParam) returns (uint256 vaultAmount) {
91+
sendParam.amountLD = vaultAmount;
92+
} catch (bytes memory errMsg) {
93+
failedMessages[_guid] = FailedMessage(oft, sendParam, _refundOFT, refundSendParam);
94+
emit GenericError(_guid, oft, errMsg);
95+
return;
96+
}
97+
98+
/// @dev Try sending the message to the target OFT
99+
try this.send{ value: msg.value }(oft, sendParam) {
100+
emit Sent(_guid, oft);
101+
} catch {
102+
/// @dev A failed send can happen due to not enough msg.value
103+
/// @dev Since we have the target tokens in the composer, we can retry with more gas.
104+
failedMessages[_guid] = FailedMessage(oft, sendParam, address(0), refundSendParam);
105+
emit SendFailed(_guid, oft);
106+
return;
107+
}
108+
}
109+
110+
/// @dev External call for try...catch logic in lzCompose()
111+
function decodeSendParam(bytes calldata sendParamBytes) external pure returns (SendParam memory sendParam) {
112+
sendParam = abi.decode(sendParamBytes, (SendParam));
113+
}
114+
115+
/// @dev External call for try...catch logic in lzCompose()
116+
function executeOVaultAction(
117+
address _oft,
118+
uint256 _amount,
119+
SendParam calldata _sendParam
120+
) external nonReentrant returns (uint256 vaultAmount) {
121+
if (msg.sender != address(this)) revert OnlySelf(msg.sender);
122+
vaultAmount = _executeOVaultAction(_oft, _amount);
123+
if (vaultAmount < _sendParam.minAmountLD) {
124+
/// @dev Will rollback on this function's storage changes (trade does not happen)
125+
revert NotEnoughTargetTokens(vaultAmount, _sendParam.minAmountLD);
126+
}
127+
}
128+
129+
/// @dev External call for try...catch logic in lzCompose()
130+
function send(address _oft, SendParam calldata _sendParam) external payable nonReentrant {
131+
if (msg.sender != address(this)) revert OnlySelf(msg.sender);
132+
if (_sendParam.dstEid == HUB_EID) {
133+
address _receiver = _sendParam.to.bytes32ToAddress();
134+
uint256 _amountLD = _sendParam.amountLD;
135+
IERC20 token = IERC20(IOFT(_oft).token());
136+
token.transfer(_receiver, _amountLD);
137+
if (msg.value > 0) {
138+
(bool sent, ) = _receiver.call{ value: msg.value }("");
139+
require(sent, "Failed to send Ether");
140+
}
141+
emit SentOnHub(_receiver, _oft, _amountLD);
142+
return;
143+
}
144+
_send(_oft, _sendParam);
145+
}
146+
147+
/// @dev Permissionless function to send back the message to the source chain
148+
/// @dev Always possible unless the lzCompose() fails due to an Out-Of-Gas panic
149+
function refund(bytes32 _guid, bytes calldata _extraOptions) external payable nonReentrant {
150+
FailedMessage memory failedMessage = failedMessages[_guid];
151+
SendParam memory refundSendParam = failedMessage.sendParam;
152+
if (failedGuidState(_guid) != FailedState.CanOnlyRefund) revert CanNotRefund(_guid);
153+
154+
refundSendParam.extraOptions = _extraOptions;
155+
156+
delete failedMessages[_guid];
157+
_send(failedMessage.refundOFT, refundSendParam);
158+
emit Refunded(_guid, failedMessage.refundOFT);
159+
}
160+
161+
/// @dev Permissionless function to retry the message with more gas
162+
/// @dev Probabilistically possible if the OFT.send() fails - ex: invalid peer
163+
function retry(bytes32 _guid, bytes calldata _extraOptions) external payable nonReentrant {
164+
FailedMessage memory failedMessage = failedMessages[_guid];
165+
if (failedGuidState(_guid) != FailedState.CanOnlyRetry) revert CanNotRetry(_guid);
166+
167+
SendParam memory sendParam = failedMessage.sendParam;
168+
169+
sendParam.extraOptions = _extraOptions;
170+
171+
delete failedMessages[_guid];
172+
_send(failedMessage.oft, sendParam);
173+
emit Retried(_guid, failedMessage.oft);
174+
}
175+
176+
/// @dev Retry mechanism for transactions that failed due to slippage. This can revert.
177+
function retryWithSwap(bytes32 _guid, bytes calldata _extraOptions) external payable {
178+
FailedMessage memory failedMessage = failedMessages[_guid];
179+
if (failedGuidState(_guid) != FailedState.CanRetryWithSwap) revert CanNotRetry(_guid);
180+
181+
SendParam memory sendParam = failedMessage.sendParam;
182+
sendParam.extraOptions = _extraOptions;
183+
184+
uint256 amountLd = failedMessage.refundSendParam.amountLD;
185+
186+
delete failedMessages[_guid];
187+
sendParam.amountLD = _executeOVaultAction(failedMessage.refundOFT, amountLd);
188+
189+
_send(failedMessage.oft, sendParam);
190+
emit Sent(_guid, failedMessage.oft);
191+
}
192+
193+
/// @dev Internal function to send the message to the target OFT
194+
function _send(address _oft, SendParam memory _sendParam) internal {
195+
IOFT(_oft).send{ value: msg.value }(_sendParam, MessagingFee(msg.value, 0), tx.origin);
196+
}
197+
198+
function _executeOVaultAction(address _oft, uint256 _amount) internal returns (uint256 vaultAmount) {
199+
if (_oft == address(ASSET_OFT)) {
200+
vaultAmount = OVAULT.deposit(_amount, address(this));
201+
} else {
202+
vaultAmount = OVAULT.redeem(_amount, address(this), address(this));
203+
}
204+
}
205+
206+
/// @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
207+
function _isInvalidPeer(address _oft, uint32 _dstEid) internal view returns (bool) {
208+
return _dstEid != HUB_EID && IOAppCore(_oft).peers(_dstEid) == bytes32(0);
209+
}
210+
211+
/// @dev Helper to view the state of a failed message
212+
function failedGuidState(bytes32 _guid) public view returns (FailedState) {
213+
FailedMessage memory failedMessage = failedMessages[_guid];
214+
215+
if (failedMessage.refundOFT == address(0) && failedMessage.oft == address(0)) {
216+
return FailedState.NotFound;
217+
}
218+
if (failedMessage.refundOFT != address(0) && failedMessage.oft == address(0)) {
219+
return FailedState.CanOnlyRefund;
220+
}
221+
if (failedMessage.refundOFT == address(0) && failedMessage.oft != address(0)) {
222+
return FailedState.CanOnlyRetry;
223+
}
224+
225+
return FailedState.CanRetryWithSwap;
226+
}
227+
receive() external payable {}
228+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
7+
import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
8+
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
9+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
10+
11+
contract OVaultUpgradeable is ERC4626Upgradeable {
12+
using SafeERC20 for IERC20;
13+
using Math for uint256;
14+
15+
/// @custom:oz-upgrades-unsafe-allow constructor
16+
constructor() {
17+
_disableInitializers();
18+
}
19+
20+
/// @dev Using solmate's implementation to work around rounding issues on initial minting
21+
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
22+
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.
23+
24+
return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), rounding);
25+
}
26+
27+
/// @dev Using solmate's implementation to work around rounding issues on initial minting
28+
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
29+
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.
30+
31+
return supply == 0 ? shares : shares.mulDiv(totalAssets(), supply, rounding);
32+
}
33+
}

0 commit comments

Comments
 (0)