Skip to content

Commit 5a16ea5

Browse files
authored
feat: OVault Composer (#1575)
Signed-off-by: shankar <shankar@layerzerolabs.org>
1 parent 84a2aaf commit 5a16ea5

6 files changed

Lines changed: 925 additions & 12 deletions

File tree

packages/ovault-composer/contracts/ERC4626Adapter.sol

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ contract ERC4626Adapter is IERC4626Adapter, IERC20 {
2121
string public symbol;
2222

2323
uint8 private immutable _underlyingDecimals;
24+
uint8 private immutable _shareDecimals;
2425

2526
/**
2627
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777).
2728
*/
2829
constructor(address asset_, address share_) {
2930
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
3031
_underlyingDecimals = success ? assetDecimals : 18;
32+
_shareDecimals = IERC20Metadata(share_).decimals();
33+
3134
_asset = IERC20(asset_);
3235
_share = IERC20MintBurnExtension(share_);
3336

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

52-
/**
53-
* @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
54-
* "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
55-
* asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.
56-
*
57-
* See {IERC20Metadata-decimals}.
58-
*/
59-
function decimals() public view virtual returns (uint8) {
60-
return _underlyingDecimals + _decimalsOffset();
61-
}
62-
6355
/** @dev See {IERC4626-asset}. */
6456
function asset() public view virtual returns (address) {
6557
return address(_asset);
@@ -70,6 +62,10 @@ contract ERC4626Adapter is IERC4626Adapter, IERC20 {
7062
return address(_share);
7163
}
7264

65+
function decimals() public view virtual returns (uint8) {
66+
return _shareDecimals;
67+
}
68+
7369
/// @dev Adding to proxy the share token's total supply
7470
function totalSupply() public view virtual returns (uint256) {
7571
return IERC20(share()).totalSupply();
@@ -286,6 +282,6 @@ contract ERC4626Adapter is IERC4626Adapter, IERC20 {
286282
}
287283

288284
function _decimalsOffset() internal view virtual returns (uint8) {
289-
return 0;
285+
return _shareDecimals - _underlyingDecimals;
290286
}
291287
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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 { IERC20MintBurnExtension } from "./interfaces/IERC20MintBurnExtension.sol";
6+
import { IOVault } from "./interfaces/IOVault.sol";
7+
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
8+
9+
import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
10+
import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol";
11+
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";
12+
13+
import { IOVaultComposer, FailedMessage, FailedState } from "./interfaces/IOVaultComposer.sol";
14+
import { IOVault } from "./interfaces/IOVault.sol";
15+
import { IERC4626Adapter } from "./interfaces/IERC4626Adapter.sol";
16+
17+
contract OVaultComposer is IOVaultComposer, ReentrancyGuard {
18+
using OFTComposeMsgCodec for bytes;
19+
using OFTComposeMsgCodec for bytes32;
20+
21+
address public immutable ASSET_OFT;
22+
address public immutable SHARE_OFT;
23+
address public immutable OVAULT;
24+
address public immutable ENDPOINT;
25+
26+
mapping(bytes32 guid => FailedMessage) public failedMessages;
27+
28+
constructor(address _ovault) {
29+
address share = IERC4626Adapter(_ovault).share();
30+
address asset = IERC4626Adapter(_ovault).asset();
31+
if (!IERC20MintBurnExtension(share).ERC4626AdapterCompliant()) {
32+
revert IOVault.ShareNotERC4626AdapterCompliant();
33+
}
34+
35+
OVAULT = _ovault;
36+
SHARE_OFT = IOVault(_ovault).SHARE_OFT();
37+
ASSET_OFT = IOVault(_ovault).ASSET_OFT();
38+
ENDPOINT = address(IOAppCore(ASSET_OFT).endpoint());
39+
40+
// Approve the adapter to spend the share tokens held by this contract
41+
IERC20(share).approve(OVAULT, type(uint256).max);
42+
IERC20(asset).approve(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 issues surrounding LayerZero config. This quoteSend catches issues like: invalid peer, dvn config, etc.
83+
try this.validateTargetOFTConfig(oft, sendParam) {} catch (bytes memory errMsg) {
84+
/// @dev When erroring out we want to NOT make a swap and the user can only go back to the source chain.
85+
failedMessages[_guid] = FailedMessage(address(0), sendParam, _refundOFT, refundSendParam);
86+
emit GenericError(_guid, oft, errMsg);
87+
return;
88+
}
89+
90+
/// @dev Try to execute the action on the target OFT. If we hit an issue then it rolls back the storage changes.
91+
try this.executeOVaultAction(_refundOFT, amount, sendParam) returns (uint256 vaultAmount) {
92+
sendParam.amountLD = vaultAmount;
93+
} catch (bytes memory errMsg) {
94+
failedMessages[_guid] = FailedMessage(oft, sendParam, _refundOFT, refundSendParam);
95+
emit GenericError(_guid, oft, errMsg);
96+
return;
97+
}
98+
99+
/// @dev Try sending the message to the target OFT
100+
try this.send{ value: msg.value }(oft, sendParam) {
101+
emit Sent(_guid, oft);
102+
} catch {
103+
/// @dev A failed send can happen due to not enough msg.value
104+
/// @dev Since we have the target tokens in the composer, we can retry with more gas.
105+
failedMessages[_guid] = FailedMessage(oft, sendParam, address(0), refundSendParam);
106+
emit SendFailed(_guid, oft);
107+
return;
108+
}
109+
}
110+
111+
/// @dev External call for try...catch logic in lzCompose()
112+
function decodeSendParam(bytes calldata sendParamBytes) external pure returns (SendParam memory sendParam) {
113+
sendParam = abi.decode(sendParamBytes, (SendParam));
114+
}
115+
116+
/// @dev External call for try...catch logic in lzCompose()
117+
function executeOVaultAction(
118+
address _oft,
119+
uint256 _amount,
120+
SendParam calldata _sendParam
121+
) external nonReentrant returns (uint256 vaultAmount) {
122+
if (msg.sender != address(this)) revert OnlySelf(msg.sender);
123+
vaultAmount = _executeOVaultAction(_oft, _amount);
124+
if (vaultAmount < _sendParam.minAmountLD) {
125+
/// @dev Will rollback on this function's storage changes (trade does not happen)
126+
revert NotEnoughTargetTokens(vaultAmount, _sendParam.minAmountLD);
127+
}
128+
}
129+
130+
/// @dev Dirty swapping amountLD and minAmountLD to 1e18 and 0 to avoid Slippage issue on the target OFT quoteSend()
131+
function validateTargetOFTConfig(address _oft, SendParam memory _sendParam) external view {
132+
_sendParam.amountLD = 1e18;
133+
_sendParam.minAmountLD = 0;
134+
135+
IOFT(_oft).quoteSend(_sendParam, false);
136+
}
137+
138+
/// @dev External call for try...catch logic in lzCompose()
139+
function send(address _oft, SendParam calldata _sendParam) external payable nonReentrant {
140+
if (msg.sender != address(this)) revert OnlySelf(msg.sender);
141+
_send(_oft, _sendParam);
142+
}
143+
144+
/// @dev Permissionless function to send back the message to the source chain
145+
/// @dev Always possible unless the lzCompose() fails due to an Out-Of-Gas panic
146+
function refund(bytes32 _guid, bytes calldata _extraOptions) external payable nonReentrant {
147+
FailedMessage memory failedMessage = failedMessages[_guid];
148+
SendParam memory refundSendParam = failedMessage.sendParam;
149+
if (failedGuidState(_guid) != FailedState.CanOnlyRefund) revert CanNotRefund(_guid);
150+
151+
refundSendParam.extraOptions = _extraOptions;
152+
153+
delete failedMessages[_guid];
154+
_send(failedMessage.refundOFT, refundSendParam);
155+
emit Refunded(_guid, failedMessage.refundOFT);
156+
}
157+
158+
/// @dev Permissionless function to retry the message with more gas
159+
/// @dev Probabilistically possible if the OFT.send() fails - ex: invalid peer
160+
function retry(bytes32 _guid, bytes calldata _extraOptions) external payable nonReentrant {
161+
FailedMessage memory failedMessage = failedMessages[_guid];
162+
if (failedGuidState(_guid) != FailedState.CanOnlyRetry) revert CanNotRetry(_guid);
163+
164+
SendParam memory sendParam = failedMessage.sendParam;
165+
166+
sendParam.extraOptions = _extraOptions;
167+
168+
delete failedMessages[_guid];
169+
_send(failedMessage.oft, sendParam);
170+
emit Retried(_guid, failedMessage.oft);
171+
}
172+
173+
/// @dev Retry mechanism for transactions that failed due to slippage. This can revert.
174+
function retryWithSwap(bytes32 _guid, bytes calldata _extraOptions) external payable {
175+
FailedMessage memory failedMessage = failedMessages[_guid];
176+
if (failedGuidState(_guid) != FailedState.CanRetryWithSwap) revert CanNotRetry(_guid);
177+
178+
SendParam memory sendParam = failedMessage.sendParam;
179+
sendParam.extraOptions = _extraOptions;
180+
181+
uint256 amountLd = failedMessage.refundSendParam.amountLD;
182+
183+
delete failedMessages[_guid];
184+
sendParam.amountLD = _executeOVaultAction(failedMessage.refundOFT, amountLd);
185+
186+
_send(failedMessage.oft, sendParam);
187+
emit Sent(_guid, failedMessage.oft);
188+
}
189+
190+
/// @dev Internal function to send the message to the target OFT
191+
function _send(address _oft, SendParam memory _sendParam) internal {
192+
IOFT(_oft).send{ value: msg.value }(_sendParam, MessagingFee(msg.value, 0), tx.origin);
193+
}
194+
195+
function _executeOVaultAction(address _oft, uint256 _amount) internal returns (uint256 vaultAmount) {
196+
if (_oft == ASSET_OFT) {
197+
vaultAmount = IERC4626Adapter(OVAULT).deposit(_amount, address(this));
198+
} else {
199+
vaultAmount = IERC4626Adapter(OVAULT).redeem(_amount, address(this), address(this));
200+
}
201+
}
202+
203+
/// @dev Helper to view the state of a failed message
204+
function failedGuidState(bytes32 _guid) public view returns (FailedState) {
205+
FailedMessage memory failedMessage = failedMessages[_guid];
206+
207+
if (failedMessage.refundOFT == address(0) && failedMessage.oft == address(0)) {
208+
return FailedState.NotFound;
209+
}
210+
if (failedMessage.refundOFT != address(0) && failedMessage.oft == address(0)) {
211+
return FailedState.CanOnlyRefund;
212+
}
213+
if (failedMessage.refundOFT == address(0) && failedMessage.oft != address(0)) {
214+
return FailedState.CanOnlyRetry;
215+
}
216+
217+
return FailedState.CanRetryWithSwap;
218+
}
219+
receive() external payable {}
220+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.22;
3+
4+
import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol";
5+
import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
6+
7+
struct FailedMessage {
8+
address oft;
9+
SendParam sendParam;
10+
address refundOFT;
11+
SendParam refundSendParam;
12+
}
13+
14+
enum FailedState {
15+
NotFound,
16+
CanOnlyRefund,
17+
CanOnlyRetry,
18+
CanRetryWithSwap
19+
}
20+
21+
interface IOVaultComposer is IOAppComposer {
22+
/// ========================== EVENTS =====================================
23+
event DecodeFailed(bytes32 indexed guid, address indexed oft, bytes message);
24+
event Sent(bytes32 indexed guid, address indexed oft);
25+
event SendFailed(bytes32 indexed guid, address indexed oft);
26+
event Refunded(bytes32 indexed guid, address indexed oft);
27+
event Retried(bytes32 indexed guid, address indexed oft);
28+
event GenericError(bytes32 indexed guid, address indexed oft, bytes errMsg);
29+
30+
/// ========================== Error Messages =====================================
31+
error InvalidAdapterMesh();
32+
error InvalidOFTMesh();
33+
34+
error OnlyEndpoint(address caller);
35+
error OnlySelf(address caller);
36+
error OnlyOFT(address oft);
37+
error OnlyAsset(address asset);
38+
error OnlyShare(address share);
39+
error CanNotRefund(bytes32 guid);
40+
error CanNotRetry(bytes32 guid);
41+
error CanNotWithdraw(bytes32 guid);
42+
error NotEnoughTargetTokens(uint256 amountLD, uint256 minAmountLD);
43+
44+
/// ========================== GLOBAL VARIABLE FUNCTIONS =====================================
45+
function ASSET_OFT() external view returns (address);
46+
function SHARE_OFT() external view returns (address);
47+
function ENDPOINT() external view returns (address);
48+
49+
/// ========================== FUNCTIONS =====================================
50+
function executeOVaultAction(
51+
address _oft,
52+
uint256 _amount,
53+
SendParam calldata _sendParam
54+
) external returns (uint256 vaultAmount);
55+
56+
function validateTargetOFTConfig(address _oft, SendParam memory _sendParam) external view;
57+
58+
function refund(bytes32 guid, bytes memory extraOptions) external payable;
59+
function retry(bytes32 guid, bytes memory extraOptions) external payable;
60+
function retryWithSwap(bytes32 guid, bytes memory extraOptions) external payable;
61+
function send(address _oft, SendParam calldata _sendParam) external payable;
62+
63+
function failedGuidState(bytes32 guid) external view returns (FailedState);
64+
65+
receive() external payable;
66+
}

0 commit comments

Comments
 (0)