Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
607200e
using failedMessage.refundSendParam in refund
shankars99 Jul 3, 2025
f4f5703
using transfer on hub for retry and refund and slippage check on retr…
shankars99 Jul 3, 2025
281ef47
OFT.send(..,_refundAddress) comment on tx.origin
shankars99 Jul 3, 2025
d8c7279
using SafeTransfer
shankars99 Jul 3, 2025
431a008
rename: CanRetryWithSwap to CanRetryWithSwapOrRefund
shankars99 Jul 3, 2025
e4076b1
gas: optimized _failedGuidState() avoids reading from storage twice
shankars99 Jul 3, 2025
fae8a36
using OZ's convertToAsset and convertToShare
shankars99 Jul 3, 2025
69c03f0
chore: require OFTs and Vault to have the same tokens
shankars99 Jul 3, 2025
cea5b1d
fix comment #14
shankars99 Jul 3, 2025
4cc35eb
comments: make clear about slippage #11
shankars99 Jul 3, 2025
adcfe55
remove inaccessible functions from interface #9
shankars99 Jul 3, 2025
8dac577
improved comments on retry #7
shankars99 Jul 3, 2025
b7c7d54
adding nonReentrant on retryWithSwap #5
shankars99 Jul 3, 2025
78e4aad
remove unused imports #1
shankars99 Jul 3, 2025
ff16730
gas: do not store refundSendParam on retry #17
shankars99 Jul 3, 2025
e5c5193
update to match package changes
shankars99 Jul 3, 2025
2305ecd
fix: refund
shankars99 Jul 4, 2025
2ef95ce
performing refund testing
shankars99 Jul 4, 2025
a93eb55
event emission on retry, refund, retryWithSwap
shankars99 Jul 4, 2025
f109d4a
update to match packages
shankars99 Jul 4, 2025
7c70065
enforcedOptions in retry, refund, and retryWithSwap #19 + cleanup and…
shankars99 Jul 4, 2025
d6141e5
update to match package update
shankars99 Jul 4, 2025
3d1e8e5
retry and refund are atomic - revert when failedMessageTransfer reverts
shankars99 Jul 5, 2025
3983a95
accumulate msg.value supplied in retryWithSwap when sendFailedMessage…
shankars99 Jul 5, 2025
4ba8b55
setting prepaidMsgValue to 0 after sending the stored msgValue to REF…
shankars99 Jul 5, 2025
dc38170
using _removeDust in _checkSlippage() to be equivalent to OFTCore::_d…
shankars99 Jul 5, 2025
7c39294
decouple _checkSlippage() and make it virtual along with _send()
shankars99 Jul 5, 2025
55d7b26
remove unnecessary try...catch in retry() and refund()
shankars99 Jul 5, 2025
12bdda0
set SendParam.amountLD = vaultAmount globally in retryWitSwap for retry
shankars99 Jul 6, 2025
2655e7e
rename arg _oft to match lzCompose definition of _oft and refundOFT
shankars99 Jul 6, 2025
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
7 changes: 6 additions & 1 deletion examples/ovault-evm/contracts/MyOVaultComposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@ pragma solidity ^0.8.22;
import { OVaultComposer } from "@layerzerolabs/ovault-evm/contracts/OVaultComposer.sol";

contract MyOVaultComposer is OVaultComposer {
constructor(address _ovault, address _assetOFT, address _shareOFT) OVaultComposer(_ovault, _assetOFT, _shareOFT) {}
constructor(
address _ovault,
address _assetOFT,
address _shareOFT,
address _refundOverpayAddress
) OVaultComposer(_ovault, _assetOFT, _shareOFT, _refundOverpayAddress) {}
}
8 changes: 7 additions & 1 deletion examples/ovault-evm/deploy/MyOVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const tokenSymbol = 'MockShare'
const shareOFTAdapterContractName = 'MyShareOFTAdapter'
const composerContractName = 'MyOVaultComposer'

const refundOverpayAddress = '0x0000000000000000000000000000000000000000'

const deploy: DeployFunction = async (hre) => {
const { getNamedAccounts, deployments } = hre

Expand All @@ -24,6 +26,10 @@ const deploy: DeployFunction = async (hre) => {

const assetOFTDeployment = await hre.deployments.get(assetOFTContractName)

if (refundOverpayAddress === '0x0000000000000000000000000000000000000000') {
throw new Error('Refund overpay address is not set')
}

const { address: ovaultAddress } = await deploy(ovaultContractName, {
from: deployer,
args: [tokenName, tokenSymbol, assetOFTDeployment.address],
Expand All @@ -48,7 +54,7 @@ const deploy: DeployFunction = async (hre) => {

const { address: composerAddress } = await deploy(composerContractName, {
from: deployer,
args: [ovaultAddress, assetOFTDeployment.address, shareOFTAdapterAddress],
args: [ovaultAddress, assetOFTDeployment.address, shareOFTAdapterAddress, refundOverpayAddress],
log: true,
skipIfAlreadyDeployed: true,
})
Expand Down
216 changes: 0 additions & 216 deletions examples/ovault-evm/test/OVault_ERC4626_Equivalence.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,222 +126,6 @@ contract OVaultERC4626EquivalenceTest is TestHelperOz5 {
assertEq(assetOFT.balanceOf(alice), alicePreDepositBal);
}

function test_ovault_MultipleMintDepositRedeemWithdraw() public {
// Scenario:
// A = Alice, B = Bob
// ________________________________________________________
// | Vault shares | A share | A assets | B share | B assets |
// |========================================================|
// | 1. Alice mints 2000 shares (costs 2000 tokens) |
// |--------------|---------|----------|---------|----------|
// | 2000 | 2000 | 2000 | 0 | 0 |
// |--------------|---------|----------|---------|----------|
// | 2. Bob deposits 4000 tokens (mints 4000 shares) |
// |--------------|---------|----------|---------|----------|
// | 6000 | 2000 | 2000 | 4000 | 4000 |
// |--------------|---------|----------|---------|----------|
// | 3. Vault mutates by +3000 tokens... |
// | (simulated yield returned from strategy)... |
// |--------------|---------|----------|---------|----------|
// | 6000 | 2000 | 3000 | 4000 | 6000 |
// |--------------|---------|----------|---------|----------|
// | 4. Alice deposits 2000 tokens (mints 1333 shares) |
// |--------------|---------|----------|---------|----------|
// | 7333 | 3333 | 4999 | 4000 | 6000 |
// |--------------|---------|----------|---------|----------|
// | 5. Bob mints 2000 shares (costs 3001 assets) |
// | NOTE: Bob's assets spent got rounded up |
// | NOTE: Alice's vault assets got rounded up |
// |--------------|---------|----------|---------|----------|
// | 9333 | 3333 | 5000 | 6000 | 9000 |
// |--------------|---------|----------|---------|----------|
// | 6. Vault mutates by +3000 tokens... |
// | (simulated yield returned from strategy) |
// | NOTE: Vault holds 17001 tokens, but sum of |
// | assetsOf() is 17000. |
// |--------------|---------|----------|---------|----------|
// | 9333 | 3333 | 6071 | 6000 | 10929 |
// |--------------|---------|----------|---------|----------|
// | 7. Alice redeem 1333 shares (2428 assets) |
// |--------------|---------|----------|---------|----------|
// | 8000 | 2000 | 3643 | 6000 | 10929 |
// |--------------|---------|----------|---------|----------|
// | 8. Bob withdraws 2928 assets (1608 shares) |
// |--------------|---------|----------|---------|----------|
// | 6392 | 2000 | 3643 | 4392 | 8000 |
// |--------------|---------|----------|---------|----------|
// | 9. Alice withdraws 3643 assets (2000 shares) |
// | NOTE: Bob's assets have been rounded back up |
// |--------------|---------|----------|---------|----------|
// | 4392 | 0 | 0 | 4392 | 8001 |
// |--------------|---------|----------|---------|----------|
// | 10. Bob redeem 4392 shares (8001 tokens) |
// |--------------|---------|----------|---------|----------|
// | 0 | 0 | 0 | 0 | 0 |
// |______________|_________|__________|_________|__________|

address alice = address(0xABCD);
address bob = address(0xDCBA);

uint256 mutationassetAmount = 3000;

assetOFT.mint(alice, 4000);

vm.prank(alice);
assetOFT.approve(address(vault), 4000);

assertEq(assetOFT.allowance(alice, address(vault)), 4000);

assetOFT.mint(bob, 7001);

vm.prank(bob);
assetOFT.approve(address(vault), 7001);

assertEq(assetOFT.allowance(bob, address(vault)), 7001);

// 1. Alice mints 2000 shares (costs 2000 tokens)
vm.prank(alice);
uint256 aliceassetAmount = vault.mint(2000, alice);

uint256 aliceShareAmount = vault.previewDeposit(aliceassetAmount);

// Expect to have received the requested mint amount.
assertEq(aliceShareAmount, 2000);
assertEq(vault.balanceOf(alice), aliceShareAmount);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), aliceassetAmount);
assertEq(vault.convertToShares(aliceassetAmount), vault.balanceOf(alice));

// Expect a 1:1 ratio before mutation.
assertEq(aliceassetAmount, 2000);

// Sanity check.
assertEq(vault.totalSupply(), aliceShareAmount);
assertEq(vault.totalAssets(), aliceassetAmount);

// 2. Bob deposits 4000 tokens (mints 4000 shares)
vm.prank(bob);
uint256 bobShareAmount = vault.deposit(4000, bob);
uint256 bobassetAmount = vault.previewWithdraw(bobShareAmount);

// Expect to have received the requested asset amount.
assertEq(bobassetAmount, 4000);
assertEq(vault.balanceOf(bob), bobShareAmount);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), bobassetAmount);
assertEq(vault.convertToShares(bobassetAmount), vault.balanceOf(bob));

// Expect a 1:1 ratio before mutation.
assertEq(bobShareAmount, bobassetAmount);

// Sanity check.
uint256 preMutationShareBal = aliceShareAmount + bobShareAmount;
uint256 preMutationBal = aliceassetAmount + bobassetAmount;
assertEq(vault.totalSupply(), preMutationShareBal);
assertEq(vault.totalAssets(), preMutationBal);
assertEq(vault.totalSupply(), 6000);
assertEq(vault.totalAssets(), 6000);

// 3. Vault mutates by +3000 tokens... |
// (simulated yield returned from strategy)...
// The Vault now contains more tokens than deposited which causes the exchange rate to change.
// Alice share is 33.33% of the Vault, Bob 66.66% of the Vault.
// Alice's share count stays the same but the asset amount changes from 2000 to 3000.
// Bob's share count stays the same but the asset amount changes from 4000 to 6000.
assetOFT.mint(address(vault), mutationassetAmount);
assertEq(vault.totalSupply(), preMutationShareBal);
assertEq(vault.totalAssets(), preMutationBal + mutationassetAmount);
assertEq(vault.balanceOf(alice), aliceShareAmount);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), aliceassetAmount + (mutationassetAmount / 3) * 1);
assertEq(vault.balanceOf(bob), bobShareAmount);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), bobassetAmount + (mutationassetAmount / 3) * 2);

// 4. Alice deposits 2000 tokens (mints 1333 shares)
vm.prank(alice);
vault.deposit(2000, alice);

assertEq(vault.totalSupply(), 7333);
assertEq(vault.balanceOf(alice), 3333);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), 4999);
assertEq(vault.balanceOf(bob), 4000);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), 6000);

// 5. Bob mints 2000 shares (costs 3001 assets)
// NOTE: Bob's assets spent got rounded up
// NOTE: Alices's vault assets got rounded up
vm.prank(bob);
vault.mint(2000, bob);

assertEq(vault.totalSupply(), 9333);
assertEq(vault.balanceOf(alice), 3333);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), 5000);
assertEq(vault.balanceOf(bob), 6000);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), 9000);

// Sanity checks:
// Alice and bob should have spent all their tokens now
assertEq(assetOFT.balanceOf(alice), 0);
assertEq(assetOFT.balanceOf(bob), 0);
// Assets in vault: 4k (alice) + 7k (bob) + 3k (yield) + 1 (round up)
assertEq(vault.totalAssets(), 14001);

// 6. Vault mutates by +3000 tokens
// NOTE: Vault holds 17001 tokens, but sum of assetsOf() is 17000.
assetOFT.mint(address(vault), mutationassetAmount);
assertEq(vault.totalAssets(), 17001);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), 6071);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), 10929);

// 7. Alice redeem 1333 shares (2428 assets)
vm.prank(alice);
vault.redeem(1333, alice, alice);

assertEq(assetOFT.balanceOf(alice), 2428);
assertEq(vault.totalSupply(), 8000);
assertEq(vault.totalAssets(), 14573);
assertEq(vault.balanceOf(alice), 2000);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), 3643);
assertEq(vault.balanceOf(bob), 6000);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), 10929);

// 8. Bob withdraws 2929 assets (1608 shares)
vm.prank(bob);
vault.withdraw(2929, bob, bob);

assertEq(assetOFT.balanceOf(bob), 2929);
assertEq(vault.totalSupply(), 6392);
assertEq(vault.totalAssets(), 11644);
assertEq(vault.balanceOf(alice), 2000);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), 3643);
assertEq(vault.balanceOf(bob), 4392);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), 8000);

// 9. Alice withdraws 3643 assets (2000 shares)
// NOTE: Bob's assets have been rounded back up
vm.prank(alice);
vault.withdraw(3643, alice, alice);

assertEq(assetOFT.balanceOf(alice), 6071);
assertEq(vault.totalSupply(), 4392);
assertEq(vault.totalAssets(), 8001);
assertEq(vault.balanceOf(alice), 0);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0);
assertEq(vault.balanceOf(bob), 4392);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), 8001);

// 10. Bob redeem 4392 shares (8001 tokens)
vm.prank(bob);
vault.redeem(4392, bob, bob);
assertEq(assetOFT.balanceOf(bob), 10930);
assertEq(vault.totalSupply(), 0);
assertEq(vault.totalAssets(), 0);
assertEq(vault.balanceOf(alice), 0);
assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0);
assertEq(vault.balanceOf(bob), 0);
assertEq(vault.convertToAssets(vault.balanceOf(bob)), 0);

// Sanity check
assertEq(assetOFT.balanceOf(address(vault)), 0);
}

function test_ovault_FailDepositWithNotEnoughApproval() public {
assetOFT.mint(address(this), 0.5e18);
assetOFT.approve(address(vault), 0.5e18);
Expand Down
38 changes: 36 additions & 2 deletions examples/ovault-evm/test/composer/OVaultComposer_Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/Opti
// OFT imports
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";
import { SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppOptionsType3.sol";

import { OVaultComposer } from "@layerzerolabs/ovault-evm/contracts/OVaultComposer.sol";

Expand Down Expand Up @@ -45,9 +46,11 @@ contract OVaultComposerBaseTest is TestHelperOz5 {
address public userA = makeAddr("userA");
address public userB = makeAddr("userB");

address public refundOverpayAddress = makeAddr("refundOverpayAddress");

address public arbEndpoint;
address public arbExecutor = makeAddr("arbExecutor");
bytes public OPTIONS_LZRECEIVE_2M = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0);
bytes public OPTIONS_LZRECEIVE_100k = OptionsBuilder.newOptions().addExecutorLzReceiveOption(100_000, 0);

uint256 public constant INITIAL_BALANCE = 100 ether;
uint256 public constant TOKENS_TO_SEND = 1 ether;
Expand All @@ -73,7 +76,12 @@ contract OVaultComposerBaseTest is TestHelperOz5 {
/// Now the "expansion" is for the arb vault and share ofts on other networks.
oVault_arb = new MockOVault("arbShare", "arbShare", address(assetOFT_arb));
shareOFT_arb = new MockOFTAdapter(address(oVault_arb), address(endpoints[ARB_EID]), address(this));
OVaultComposerArb = new OVaultComposer(address(oVault_arb), address(assetOFT_arb), address(shareOFT_arb));
OVaultComposerArb = new OVaultComposer(
address(oVault_arb),
address(assetOFT_arb),
address(shareOFT_arb),
refundOverpayAddress
);

/// Deploy the Share OFTs on other networks - these are NOT lockbox adapters.
shareOFT_eth = new MockOFT("ethShare", "ethShare", address(endpoints[ETH_EID]), address(this));
Expand All @@ -98,6 +106,13 @@ contract OVaultComposerBaseTest is TestHelperOz5 {

deal(arbExecutor, INITIAL_BALANCE);
deal(arbEndpoint, INITIAL_BALANCE);

EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2);
enforcedOptions[0] = EnforcedOptionParam({ eid: ETH_EID, msgType: 1, options: OPTIONS_LZRECEIVE_100k });
enforcedOptions[1] = EnforcedOptionParam({ eid: POL_EID, msgType: 1, options: OPTIONS_LZRECEIVE_100k });

assetOFT_arb.setEnforcedOptions(enforcedOptions);
shareOFT_arb.setEnforcedOptions(enforcedOptions);
}

function _createComposePayload(
Expand Down Expand Up @@ -139,6 +154,25 @@ contract OVaultComposerBaseTest is TestHelperOz5 {
assetOFT_arb.mint(address(oVault_arb), mintAssets);
}

function _removeDustWithOffset(uint256 _amount, int128 _offset) internal pure returns (uint256, uint256) {
uint256 amountWithOffset = _amount;
if (_offset < 0) {
uint256 modOffset = uint128(-1 * _offset);
// If offset is negative, we need to ensure we don't underflow
require(_amount >= modOffset, "Offset too large");
amountWithOffset = _amount - modOffset;
} else {
// If offset is positive, we can safely add it
amountWithOffset = _amount + uint128(_offset);
}
return _removeDust(amountWithOffset);
}

function _removeDust(uint256 _amount) internal pure returns (uint256, uint256) {
uint256 dust = _amount % 10 ** 12;
return (_amount - dust, dust);
}

function _randomGUID() internal view returns (bytes32) {
return bytes32(vm.randomBytes(32));
}
Expand Down
5 changes: 3 additions & 2 deletions examples/ovault-evm/test/composer/OVaultComposer_E2E.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ contract OVaultComposerE2ETest is OVaultComposerBaseTest {
}

function test_E2E_ethereum_to_polygon() public {
uint256 shareTokensToReceive = TOKENS_TO_SEND * 2;
(uint256 shareTokensToReceive, ) = _removeDustWithOffset(TOKENS_TO_SEND * 2, -1);

deal(address(assetOFT_eth), userA, TOKENS_TO_SEND);

Expand Down Expand Up @@ -105,10 +105,11 @@ contract OVaultComposerE2ETest is OVaultComposerBaseTest {
address(this),
""
);

assertEq(
assetOFT_arb.balanceOf(composerAddress),
0,
"composerAddress should have no tokens after lzCompose on arb"
"composerAddress should have the no tokens after lzCompose on arb"
);

verifyPackets(POL_EID, addressToBytes32(address(shareOFT_pol)));
Expand Down
Loading